Spring Security登录优雅集成图形验证码

前言

在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配置

@EnableWebSecurity public 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

提供图形验证码接口

@Controller public 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)输出到客户端。

至此整个图形验证码认证的全流程已经结束。

Spring Security登录优雅集成图形验证码

您可能还会对下面的文章感兴趣: