前言

在SpringSecurity的默认登录反对组件formLogin中没有提供图形验证码的反对,目前大多数的计划都是通过新增Filter来实现。filter的形式能够实现性能,然而没有优雅的解决, 须要从新保护一套和登录相干的url,例如:loginProccessUrl,loginFailUrl,loginSuccessUrl,从软件设计角度来讲性能没有内聚。
上面为大家介绍一种优雅的解决方案。

解决思路

先获取验证码

判断图形验证码先要获取到验证码,在UsernamePasswordAuthenticationToken(UPAT)中没有字段来存储验证码,重写UPAT老本太高。能够从details字段中动手,将验证码放在details中。

判断验证码是否正确

UPAT的认证是在DaoAuthenticationProvider中实现的,如果须要判断验证码间接批改是老本比拟大的形式,能够新增AuthenticationProvider来对验证码新增验证。

输入验证码

惯例超过能够通过Controller来输入,然而验证码的治理须要对立,避免各种sessionKey乱飞。

代码实现

新增验证码容器:CaptchaAuthenticationDetails

public class CaptchaAuthenticationDetails extends WebAuthenticationDetails {    private final String DEFAULT_CAPTCHA_PARAMETER_NAME = "captcha"; private String captchaParameter = DEFAULT_CAPTCHA_PARAMETER_NAME;/** * 用户提交的验证码 */private String committedCaptcha;/** * 预设的验证码 */private String presetCaptcha; private final WebAuthenticationDetails webAuthenticationDetails; public CaptchaAuthenticationDetails(HttpServletRequest request) {        super(request); this.committedCaptcha = request.getParameter(captchaParameter); this.webAuthenticationDetails = new WebAuthenticationDetails(request); }    public boolean isCaptchaMatch() {        if (this.presetCaptcha == null || this.committedCaptcha == null) {            return false; }        return this.presetCaptcha.equalsIgnoreCase(committedCaptcha); } getter ... setter ... }

这个类次要是用于保留验证码

验证码获取:CaptchaAuthenticationDetailsSource

public class CaptchaAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, CaptchaAuthenticationDetails> {    private final CaptchaRepository<HttpServletRequest> captchaRepository; public CaptchaAuthenticationDetailsSource(CaptchaRepository<HttpServletRequest> captchaRepository) {        this.captchaRepository = captchaRepository; }    @Override public CaptchaAuthenticationDetails buildDetails(HttpServletRequest httpServletRequest) {        CaptchaAuthenticationDetails captchaAuthenticationDetails = new CaptchaAuthenticationDetails(httpServletRequest); captchaAuthenticationDetails.setPresetCaptcha(captchaRepository.load(httpServletRequest)); return captchaAuthenticationDetails; }}

依据提交的参数构建CaptchaAuthenticationDetails,用户提交的验证码(committedCaptcha)从request中获取,预设的验证码(presetCaptcha)从验证码仓库(CaptchaRepostory)获取

验证码仓库实现SessionCaptchaRepository

public class SessionCaptchaRepository implements CaptchaRepository<HttpServletRequest> {    private static final String CAPTCHA_SESSION_KEY = "captcha"; /** * the key of captcha in session attributes */ private String captchaSessionKey = CAPTCHA_SESSION_KEY; @Override public String load(HttpServletRequest request) {        return (String) request.getSession().getAttribute(captchaSessionKey); }    @Override public void save(HttpServletRequest request, String captcha) {        request.getSession().setAttribute(captchaSessionKey, captcha); }    /** * @return sessionKey */ public String getCaptchaSessionKey() {        return captchaSessionKey; }    /** * @param captchaSessionKey sessionKey */ public void setCaptchaSessionKey(String captchaSessionKey) {        this.captchaSessionKey = captchaSessionKey; }}

这个验证码仓库是基于Session的,如果想要基于Redis只有实现CaptchaRepository即可。

对验证码进行认证CaptchaAuthenticationProvider

public class CaptchaAuthenticationProvider implements AuthenticationProvider {    private final Logger log = LoggerFactory.getLogger(this.getClass()); @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException {        UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication; CaptchaAuthenticationDetails details = (CaptchaAuthenticationDetails) authenticationToken.getDetails(); if (!details.isCaptchaMatch()) {            //验证码不匹配抛出异样,退出认证 if (log.isDebugEnabled()) {                log.debug("认证失败:验证码不匹配"); }            throw new CaptchaIncorrectException("验证码谬误"); }        //替换details authenticationToken.setDetails(details.getWebAuthenticationDetails()); //返回空交给下一个provider进行认证 return null; }    @Override public boolean supports(Class<?> aClass) {        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass); }}

SpringSecurity配置

@EnableWebSecuritypublic class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {    @Bean public CaptchaRepository<HttpServletRequest> sessionCaptchaRepository() {        return new SessionCaptchaRepository(); }    @Override protected void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()                .anyRequest()                .authenticated()                .and()                .formLogin()                .loginProcessingUrl("/login")                .loginPage("/login.html")                .authenticationDetailsSource(new CaptchaAuthenticationDetailsSource(sessionCaptchaRepository()))                .failureUrl("/login.html?error=true")                .defaultSuccessUrl("/index.html")                .and()                .authenticationProvider(new CaptchaAuthenticationProvider())                .csrf()                .disable(); }    @Override public void configure(WebSecurity web) {        web.ignoring()                .mvcMatchers("/captcha", "/login.html"); }}

将CaptchaAuthenticationProvider退出到认证链条中,重新配置authenticationDetailsSource

提供图形验证码接口

@Controllerpublic class CaptchaController {    private final CaptchaRepository<HttpServletRequest> captchaRepository; @Autowired public CaptchaController(CaptchaRepository<HttpServletRequest> captchaRepository) {        this.captchaRepository = captchaRepository; }    @RequestMapping("/captcha")    public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {        RandomCaptcha randomCaptcha = new RandomCaptcha(4); captchaRepository.save(request, randomCaptcha.getValue()); CaptchaImage captchaImage = new DefaultCaptchaImage(200, 60, randomCaptcha.getValue()); captchaImage.write(response.getOutputStream()); }}

将生成的随机验证码(RandomCaptcha)保留到验证码仓库(CaptchaRepository)中,并将验证码图片(CaptchaImage)输入到客户端。
至此整个图形验证码认证的全流程曾经完结。

代码仓库

https://gitee.com/leecho/spri...