獻上一句格言,來自馬克·扎克伯格的座右銘: Stay foucsed, Keep shipping(保持專一,持續交付)html
回到本章節咱們將要學習的內容,如今使用驗證碼登陸方式是再常見不過了,圖形驗證碼,手機短信,郵箱驗證碼啊諸如此類的。今天咱們以圖形驗證碼爲例,介紹下如何在Spring Security中添加驗證碼。與以前文章不一樣的是,這篇文章也將與數據庫結合,模擬真實的開發環境。java
1.首先使用spring boot starter jpa 幫助咱們經過實體類在數據庫中簡歷對應的表結構,以及插入用戶一條數據。git
在前面兩篇文章中都有詳細介紹過如何配置UserDetails以及UserDetailsService,這裏也就不在贅述了github
在生成驗證碼的同時,將驗證碼放入session中。spring
/** * @author developlee * @since 2019/1/14 16:23 */ @RestController public class CaptchaController { /** * 用於生成驗證碼圖片 * * @param request * @param response */ @GetMapping("/code/image") public void createCode(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpSession httpSession = request.getSession(); Object[] objects = ValidateUtil.createImage(); httpSession.setAttribute("imageCode", objects[0]); BufferedImage bufferedImage = (BufferedImage) objects[1]; response.setContentType("image/png"); OutputStream os = response.getOutputStream(); ImageIO.write(bufferedImage, "png", os); } }
工具類的實現,這個網上有不少種,你們能夠搜一下看看數據庫
/** * @author developlee * @since 2019/1/18 17:24 */ public class ValidateUtil { // 驗證碼字符集 private static final char[] chars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; // 字符數量 private static final int SIZE = 4; // 干擾線數量 private static final int LINES = 5; // 寬度 private static final int WIDTH = 80; // 高度 private static final int HEIGHT = 40; // 字體大小 private static final int FONT_SIZE = 30; /** * 生成隨機驗證碼及圖片 * Object[0]:驗證碼字符串; * Object[1]:驗證碼圖片。 */ public static Object[] createImage() { StringBuffer sb = new StringBuffer(); // 1.建立空白圖片 BufferedImage image = new BufferedImage( WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); // 2.獲取圖片畫筆 Graphics graphic = image.getGraphics(); // 3.設置畫筆顏色 graphic.setColor(Color.LIGHT_GRAY); // 4.繪製矩形背景 graphic.fillRect(0, 0, WIDTH, HEIGHT); // 5.畫隨機字符 Random ran = new Random(); for (int i = 0; i <SIZE; i++) { // 取隨機字符索引 int n = ran.nextInt(chars.length); // 設置隨機顏色 graphic.setColor(getRandomColor()); // 設置字體大小 graphic.setFont(new Font( null, Font.BOLD + Font.ITALIC, FONT_SIZE)); // 畫字符 graphic.drawString( chars[n] + "", i * WIDTH / SIZE, HEIGHT*2/3); // 記錄字符 sb.append(chars[n]); } // 6.畫干擾線 for (int i = 0; i < LINES; i++) { // 設置隨機顏色 graphic.setColor(getRandomColor()); // 隨機畫線 graphic.drawLine(ran.nextInt(WIDTH), ran.nextInt(HEIGHT), ran.nextInt(WIDTH), ran.nextInt(HEIGHT)); } // 7.返回驗證碼和圖片 return new Object[]{sb.toString(), image}; } /** * 隨機取色 */ public static Color getRandomColor() { Random ran = new Random(); Color color = new Color(ran.nextInt(256), ran.nextInt(256), ran.nextInt(256)); return color; } }
配置好以後,在頁面加上咱們的驗證碼json
<input name="validateCode" type="text" placeholder="請輸入驗證碼"> <input type=image src="http://localhost:8080/code/image"/>
而後咱們寫一個filter攔截器,用來實現驗證碼的驗證。網絡
/** * @author developlee * @since 2019/1/14 16:42 */ @Slf4j public class CaptchaFilter extends OncePerRequestFilter { @Autowired private AppConfig appConfig; private AuthenticationFailureHandler authenticationFailureHandler; // 注入appConfig public CaptchaFilter (AppConfig appConfig, AuthenticationFailureHandler authenticationFailureHandler) { this.appConfig = appConfig; this.authenticationFailureHandler = authenticationFailureHandler; } @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { if(httpServletRequest.getRequestURI().equals(appConfig.getLoginUri().trim()) && httpServletRequest.getMethod().equals(RequestMethod.POST.name())) { try { validateCode(httpServletRequest); } catch (ValidateException e) { authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } filterChain.doFilter(httpServletRequest, httpServletResponse); } /** * 驗證碼的認證 * @param userValidateCode * @throws ValidateException */ private void validateCode(HttpServletRequest httpServletRequest) throws ValidateException { // 若是是登陸請求,而且是post方式訪問,則校驗驗證碼 String userValidateCode = httpServletRequest.getParameter("validateCode"); String sysValidateCode = (String) httpServletRequest.getSession().getAttribute("imageCode"); log.info("用戶輸入的驗證碼是:{},系統保存的驗證碼是:{}", userValidateCode, sysValidateCode); // 和咱們保存的驗證碼進行比較 if(StringUtils.isEmpty(userValidateCode)) { throw new ValidateException("驗證碼信息不能爲空"); } if(!StringUtils.equalsIgnoreCase(userValidateCode, sysValidateCode)) { throw new ValidateException("驗證碼不正確"); } // TODO 可加上對驗證碼有效時間的驗證,有興趣的話能夠本身實現下。其實就在生成驗證碼時,記錄下生成的時間戳就行了。 } }
這個類中定義了一個ValidateException,這個exception擴展了Spring Security 中的 AuthentionException,當拋出ValidateException,確保咱們的異常能被Spring Security正常捕獲。session
public class ValidateException extends AuthenticationException { @Getter @Setter private String code; @Getter @Setter private String msg; @Getter @Setter private Exception exception; public ValidateException(String msg) { super(msg); } public ValidateException(String msg, Throwable t) { super(msg, t); } }
OK,到這裏咱們還缺最後一步,那就是將ValidateFilter添加到Spring Security 的攔截器鏈中,先看下過濾器鏈的執行順序: app
圖片來源網絡。
咱們應該在驗證用戶名和密碼以前先對驗證碼進行校驗,所以咱們的CaptchaFilter應該在UsernamePasswordAuthenticationFilter以前執行。
@Slf4j @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private AppConfig appConfig; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().loginPage("/sign_in").loginProcessingUrl(appConfig.getLoginUri()) .defaultSuccessUrl("/welcome").permitAll() .failureHandler(new MyFailureHandler()) .and().authorizeRequests().antMatchers("/code/image").permitAll() .and().addFilterBefore(new CaptchaFilter(appConfig, new MyFailureHandler()), UsernamePasswordAuthenticationFilter.class) // 驗證碼過濾器加入過濾器鏈 .logout().logoutUrl("/auth/logout").clearAuthentication(true) .and().authorizeRequests().anyRequest().authenticated(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } }
到這裏以後,咱們已經完成了對驗證碼的驗證,而後要處理當驗證不經過,也就是拋出ValidateException時,返回信息給頁面。 注意到,SecurityConfig中的MyFailureHandler這個類,AuthentionException異常將會在這個類中處理。
/** * 登陸失敗處理邏輯 */ @Slf4j public class MyFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { if (e instanceof ValidateException) { log.info("用戶輸入驗證碼錯誤,返回錯誤信息" + e.getMessage()); } httpServletResponse.setHeader("content-type", "application/json"); httpServletResponse.setCharacterEncoding("UTF-8"); Writer writer = httpServletResponse.getWriter(); writer.write(e.getMessage()); } }
到這編碼部分基本就結束了。下面咱們在頁面作個測試
測試驗證碼爲空的狀況
看到log窗口打印的日誌以下:提示返回驗證信息不能爲空
界面顯示錯誤信息也是同樣。
測試下驗證碼錯誤的狀況
返回的是驗證碼不正確
這裏的錯誤提示信息咱們能夠作個優化,讓其在登陸頁面時就顯示,能夠本身實現下,在MyFailureHandler中用response.forward並攜帶錯誤信息跳轉到登陸頁,而後在登陸頁面顯示異常信息便可。
另外也能夠看到,驗證碼不正確時,咱們並無對用戶信息進行驗證。因此SecurityConfig中的addFilterBefore是生效的。
這篇文中,主要介紹了Spring Security整合驗證碼實現登陸的功能。要注意的地方就是CaptchaFilter是擴展OncePerRequestFilter,而後要將該Filter放在Spring Security 的過濾器鏈中,並在UsernamePasswordAuthenticationFilter以前執行,以及異常的處理是使用自定義的FailureHandler。具體代碼可參看個人github.com,歡迎你們star和follow,感謝觀看。