献上一句格言,来自马克·扎克伯格的座右铭: 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,感谢观看。