在SpringSecurity的默認登陸支持組件formLogin中沒有提供圖形驗證碼的支持,目前大多數的方案都是經過新增Filter來實現。filter的方式能夠實現功能,可是沒有優雅的解決, 須要從新維護一套和登陸相關的url,例如:loginProccessUrl,loginFailUrl,loginSuccessUrl,從軟件設計角度來說功能沒有內聚。
下面爲你們介紹一種優雅的解決方案。html
判斷圖形驗證碼先要獲取到驗證碼,在UsernamePasswordAuthenticationToken(UPAT)中沒有字段來存儲驗證碼,重寫UPAT成本過高。能夠從details字段中入手,將驗證碼放在details中。git
UPAT的認證是在DaoAuthenticationProvider中完成的,若是須要判斷驗證碼直接修改是成本比較大的方式,能夠新增AuthenticationProvider來對驗證碼新增驗證。web
常規超過能夠經過Controller來輸出,可是驗證碼的管理須要統一,防止各類sessionKey亂飛。spring
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 ... }
這個類主要是用於保存驗證碼session
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)獲取mvc
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便可。app
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); } }
@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加入到認證鏈條中,從新配置authenticationDetailsSourcedom
@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)輸出到客戶端。
至此整個圖形驗證碼認證的全流程已經結束。ide