本文共 17004 字,大约阅读时间需要 56 分钟。
验证码校验也是通过Spring Security中的过滤器链来进行校验的。
该实体类用于存储验证码的相关数据。
@Datapublic class ImageCode { /** * 图形验证码 */ private BufferedImage imageCode; /** * 验证码 */ private String code; /** * 过期时间 */ private LocalDateTime expireTime; public ImageCode(BufferedImage imageCode, String code, int expireTime) { this.imageCode = imageCode; this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireTime); } public ImageCode(BufferedImage imageCode, String code, LocalDateTime expireTime) { this.imageCode = imageCode; this.code = code; this.expireTime = expireTime; } public boolean isExpire() { return LocalDateTime.now().compareTo(this.expireTime) > 0; }}
@RestController@RequestMappingpublic class ValidateCodeController { private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); /** ImageCode在session中的key */ public static final String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE"; @GetMapping("/code/image") public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { // 生成imageCode ImageCode imageCode = createImageCode(request); // 将imageCode 保存在session中 sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode); ImageIO.write(imageCode.getImageCode(), "JPEG", response.getOutputStream()); } public ImageCode createImageCode(HttpServletRequest request) { // 在内存中创建图像 int width = 65, height = 20; BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics graphics = bufferedImage.getGraphics(); // 设定背景色 graphics.setColor(getRandColor(230, 255)); graphics.fillRect(0, 0, 100, 25); // 设定字体 graphics.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18)); // 产生0条干扰线, graphics.drawLine(0, 0, 0, 0); // 随机产生四位验证码 String sRand = ""; Random random = new Random(); for (int i = 0; i < 4; i++) { String rand = String.valueOf(random.nextInt(10)); sRand += rand; // 将认证码显示到图象中 graphics.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成 graphics.drawString(rand, 15 * i + 6, 16); } graphics.dispose(); return new ImageCode(bufferedImage, sRand, 60); } /** * 给定范围获得随机颜色 * * @param fc * @param bc * @return */ Color getRandColor(int fc, int bc) { Random random = new Random(); if (fc > 255) { fc = 255; } if (bc > 255) { bc = 255; } int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); }}
.antMatchers("/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAl()
通过Filter实现,验证码的校验逻辑。自定义的验证码过滤器,前置到UsernamePasswordAuthenticationFilter之前。
public class ValidateCodeFilter extends OncePerRequestFilter { private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); private AuthenticationFailureHandler authenticationFailureHandler; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { if("/authentication/form".equals(httpServletRequest.getRequestURI()) && "POST".equalsIgnoreCase(httpServletRequest.getMethod())) { try { validate(new ServletWebRequest(httpServletRequest)); } catch (ValidateCodeException e) { // 调用失败处理器 authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } filterChain.doFilter(httpServletRequest, httpServletResponse); } private void validate(ServletWebRequest servletWebRequest) throws ServletRequestBindingException, ValidateCodeException { ImageCode imageCodeSession = (ImageCode)sessionStrategy.getAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY_IMAGE_CODE); String code = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(),"imageCode"); if (StrUtil.isBlank(code)) { throw new ValidateCodeException("验证码的值不能为空"); } if (imageCodeSession == null) { throw new ValidateCodeException("验证码不存在"); } if (imageCodeSession.isExpire()) { sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY_IMAGE_CODE); throw new ValidateCodeException("验证码已过期"); } if (!StrUtil.equals(imageCodeSession.getCode(), code)) { throw new ValidateCodeException("验证码不匹配"); } sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY_IMAGE_CODE); } public AuthenticationFailureHandler getAuthenticationFailureHandler() { return authenticationFailureHandler; } public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) { this.authenticationFailureHandler = authenticationFailureHandler; }}
public class ValidateCodeException extends AuthenticationException { public ValidateCodeException(String msg) { super(msg); }}
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SecurityProperties securityProperties; @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailureHandler loginFailureHandler; /** * 为减少代码重复开发,多个应用使用同一个认证中心,每个应用需要自己指定登录页面。 * 这里需要将 loginpage 指向一个controlelr地址。 * 如果是html页面,就跳转到指定的登录页。 * 如果不是html页面,就提示401 没有认证信息。 * 如果有应用有指定的就使用自己的。如果没指定就使用本认证模块默认的登录页。 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 引入验证码过滤器 ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); validateCodeFilter.setAuthenticationFailureHandler(loginFailureHandler); // 配置过滤器的位置 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class); http.formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/authentication/form") .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler) .successForwardUrl("/index")// .defaultSuccessUrl("/index") .and() .authorizeRequests() .antMatchers("/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAll() .anyRequest().authenticated() .and().csrf().disable(); } }
重构代码将一些参数,改为可配置的,降低代码耦合度; 在这个Spring Security的案例中,是把当前应用作为可以重用的应用架构,进而可以引入到其他应用中去,减少用户认证的重复开发。每个应用可能可能自己的验证码生成逻辑不同,可以通过代码重构,让第三方应用实现自己的生成验证码的逻辑。
应用级别配置:配置在引用的第三方项目中
默认级别配置:给配置值指定设置默认值1、验证码参数配置如下:
security: browser: logintype: html validate_code: image: width: 80 height: 40 length: 5 expire_time: 100
2、对应的配置实体类
@ConfigurationProperties(prefix = "security")public class SecurityProperties { private BrowserProperties browser = new BrowserProperties(); private ValidateCodeProperties validateCode = new ValidateCodeProperties(); // 省略getter和setter方法}
@Datapublic class ValidateCodeProperties { ImageCodeProperties image = new ImageCodeProperties();}
@Datapublic class ImageCodeProperties { private int width = 67; private int height = 23; private int length = 4; // 验证码长度 private int expireTime = 60; // 过期时间}
3、在ValidateCodeController中,将对应的验证码参数改为从配置类中读取。
public ImageCode createImageCode(HttpServletRequest request) { ImageCodeProperties image = securityProperties.getValidateCode().getImage(); // 在内存中创建图像// int width = 65, height = 20; int width = ServletRequestUtils.getIntParameter(request, "width", image.getWidth()); int height = ServletRequestUtils.getIntParameter(request, "height", image.getHeight()); int length = ServletRequestUtils.getIntParameter(request, "length", image.getLength()); int expireTime = ServletRequestUtils.getIntParameter(request, "expireTime", image.getExpireTime()); BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics graphics = bufferedImage.getGraphics(); // 设定背景色 graphics.setColor(getRandColor(230, 255)); graphics.fillRect(0, 0, 100, 25); // 设定字体 graphics.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18)); // 产生0条干扰线, graphics.drawLine(0, 0, 0, 0); // 随机产生四位验证码 String sRand = ""; Random random = new Random(); for (int i = 0; i < length; i++) { String rand = String.valueOf(random.nextInt(10)); sRand += rand; // 将认证码显示到图象中 graphics.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成 graphics.drawString(rand, 15 * i + 6, 16); } graphics.dispose(); return new ImageCode(bufferedImage, sRand, expireTime); }
如果 第三方应用认证的接口和默认的路径不一样呢,这时候就需要改为可配置的了。
在上面的配置类中添加url属性。
在验证码校验过滤器的 ValidateCodeFilter
类中,实现 接口 InitializingBean
的方法afterPropertiesSet()
实现了 InitializingBean
接口的类,实例化bean时,会自动执行,afterPropertiesSet()
方法。
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean { private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); private AuthenticationFailureHandler authenticationFailureHandler; private Seturls; // 存储需要拦截的url private SecurityProperties securityProperties; @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); String url = securityProperties.getValidateCode().getImage().getUrl(); String[] configUrl = url.split(","); urls = Stream.of(configUrl).collect(Collectors.toSet()); // 添加默认的地址 urls.add("/authentication/form"); } /** * 重构 * @param httpServletRequest * @param httpServletResponse * @param filterChain * @throws ServletException * @throws IOException */ @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { boolean action = false; for (String url : urls) { if (url.equals(httpServletRequest.getRequestURI())) { action = true; break; } } if (action) { try { validate(new ServletWebRequest(httpServletRequest)); } catch (ValidateCodeException e) { // 调用失败处理器 authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } filterChain.doFilter(httpServletRequest, httpServletResponse); }}
ValidateCodeFilter
类没有通过Spring Bean的方式注入,还是需要在SecurityConfig
中进行手动调用 afterPropertiesSet()
protected void configure(HttpSecurity http) throws Exception { ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); validateCodeFilter.setAuthenticationFailureHandler(loginFailureHandler); validateCodeFilter.setSecurityProperties(securityProperties); validateCodeFilter.afterPropertiesSet(); // 配置过滤器的位置 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class); http.formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/authentication/form") .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler) .successForwardUrl("/index")// .defaultSuccessUrl("/index") .and() .authorizeRequests() .antMatchers("/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAll() .anyRequest().authenticated() .and().csrf().disable(); }
思路: 逻辑可配,就是抽象成接口,实现由客户端实现。
1、 定义生成验证码逻辑的接口
/** * @Author L.jg * @Title 抽象接口,让客户端可配置接口 * @Date 2021/5/24 11:42 */public interface ValidateCodeGenerate { ImageCode generate(HttpServletRequest request);}
2、实现接口,自定义验证码的实现逻辑
public class ImageCodeGenerate implements ValidateCodeGenerate { private ImageCodeProperties imageCodeProperties; public ImageCodeGenerate(ImageCodeProperties imageCodeProperties) { this.imageCodeProperties = imageCodeProperties; } @Override public ImageCode generate(HttpServletRequest request) { int width = ServletRequestUtils.getIntParameter(request, "width", imageCodeProperties.getWidth()); int height = ServletRequestUtils.getIntParameter(request, "height", imageCodeProperties.getHeight()); int length = ServletRequestUtils.getIntParameter(request, "length", imageCodeProperties.getLength()); int expireTime = ServletRequestUtils.getIntParameter(request, "expireTime", imageCodeProperties.getExpireTime()); return createImageCode(width, height, length, expireTime); } public ImageCode createImageCode(int width, int height, int length, int expireTime) { // 在内存中创建图像 // int width = 65, height = 20; BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics graphics = bufferedImage.getGraphics(); // 设定背景色 graphics.setColor(getRandColor(230, 255)); graphics.fillRect(0, 0, 100, 25); // 设定字体 graphics.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18)); // 产生0条干扰线, graphics.drawLine(0, 0, 0, 0); // 随机产生四位验证码 String sRand = ""; Random random = new Random(); for (int i = 0; i < length; i++) { String rand = String.valueOf(random.nextInt(10)); sRand += rand; // 将认证码显示到图象中 graphics.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成 graphics.drawString(rand, 15 * i + 6, 16); } graphics.dispose(); return new ImageCode(bufferedImage, sRand, expireTime); } /** * 给定范围获得随机颜色 * * @param fc * @param bc * @return */ Color getRandColor(int fc, int bc) { Random random = new Random(); if (fc > 255) { fc = 255; } if (bc > 255) { bc = 255; } int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); }}
3、 在Spring boot中注入Bean
@Configurationpublic class VlidateCodeConfig { @Autowired private SecurityProperties securityProperties; @Bean // 如果Spring 容易中存在 imageCodeGenerate 的bean就不会再初始化该bean了 @ConditionalOnMissingBean(name = "imageCodeGenerate") public ValidateCodeGenerate imageCodeGenerate() { ImageCodeGenerate imageCodeGenerate = new ImageCodeGenerate(securityProperties.getValidateCode().getImage()); return imageCodeGenerate; }}
4、 在验证码controller中,注入验证码实现类
@RestController@RequestMappingpublic class ValidateCodeController { private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Autowired private SecurityProperties securityProperties; @Autowired private ValidateCodeGenerate validateCodeGenerate; /** ImageCode在session中的key */ public static final String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE"; @GetMapping("/code/image") public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { // 生成imageCode// ImageCode imageCode = createImageCode(request); ImageCode imageCode = validateCodeGenerate.generate(request); // 将imageCode 保存在session中 sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode); ImageIO.write(imageCode.getImageCode(), "JPEG", response.getOutputStream()); } }
转载地址:http://udfoi.baihongyu.com/