在上一节的基础上, 我们再给项目加入验证码模块, security 并没有现成的给我们实现这部分功能, 所以我们就需要手写过滤器来实现它这节题目看上去和第一节没什么关系, 但是思想大同小异, 希望可以耐心的看, 毕竟我尽力的去往清楚的写看完可能会感觉很乱, 这很正常, 因为封装的缘故, 但是耐下心看完的我相信技术会得道很大的提升
第一章顺风车: SpringBoot 整合 Security(一)实现用户认证并判断返回 json 还是 view
第二章顺风车: SpringBoot 整合 Security(二)实现验证码登录
好了, 开始正文
再第一节教程的基础上, 我们新加了
并对核心配置类 BrowserSecurityConfig 添加了一些代码
我像以往一样把各个类先做一大概介绍:
- ImageCodeProperties:
- ValidateCodeProperties:
这两个类在 properties 包下, 因为它们是来获取 application 配置文件中的配置的
ImageCodeGenerator: 生成验证码实现类
ValidateCodeBeanConfig: 注入 ImageCodeGenerator 到 spring 容器为什么不直接再 ImageCodeGenerator 上添加 @Component 注入呢? 请看下面详细解释
ValidateCodeGenerator: 生成验证码接口
ImageCode: 验证码实体类
ValidateCodeController:controller 类, 用来将验证码返回给用户
ValidateCodeException: 自定义异常
ValidateCodeFilter: 验证码过滤器
1. 首先我们需要手写一个过滤器
那么问题来了, 手写什么样的过滤器呢, 过滤器写在项目启动的哪个阶段调用呢? 第一节大家能看到, 其实在返回 UserDetail 对象的时候, 下面的操作都是 security 的暗箱操作(基本信息的校验), 所以我们必须再此过滤器之前将验证码做一处理(生成和判断), 如果验证码不符合要求, 直接扔给 登录失败处理 器中, 反之亦然
1.1 我们需要在核心配置类 BrowserSecurityConfig 中加一段代码
- /**
- * 创建 验证码 过滤器 , 并将该过滤器的 Handler 设置成自定义登录失败处理器
- */
- ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
- validateCodeFilter.setFailureHandler(myFailHandler);
- // 将 securityproperties 设置进去
- validateCodeFilter.setSecurityProperties(securityProperties);
- // 调用 装配 需要图片验证码的 url 的初始化方法
- validateCodeFilter.afterPropertiesSet();
- http
- // 在 UsernamePasswordAuthenticationFilter 过滤器前 加一个过滤器 来搞验证码
- .addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)
- .formLogin()
- ....
首先, 我创建了一个 ValidateCodeFilter 对象, 设置它的失败处理器 和 securityProperties 配置类, 调用它中的 afterPropertiesSet()方法 总的来说, 就是调用了该对象的三个方法至于方法是干什么, 慢慢往下看
其次, 在 UsernamePasswordAuthenticationFilter 过滤器前 加一个过滤器 来搞验证码即:
- .addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)
- 1.2 ValidateCodeFilter .java
- package com.fantJ.core.validate;
- /**
- * 验证码 过滤器
- * Created by Fant.J.
- */
- @Component
- public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean{
- private Logger logger = LoggerFactory.getLogger(getClass());
- /**
- * 登录失败处理器
- */
- @Autowired
- private AuthenticationFailureHandler failureHandler;
- /**
- * Session 对象
- */
- private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
- /**
- * 创建一个 Set 集合 存放 需要验证码的 urls
- */
- private Set<String> urls = new HashSet<>();
- /**
- * security applicaiton 配置属性
- */
- @Autowired
- private SecurityProperties securityProperties;
- /**
- * spring 的一个工具类: 用来判断 两字符串 是否匹配
- */
- private AntPathMatcher pathMatcher = new AntPathMatcher();
- /**
- * 这个方法是 InitializingBean 接口下的一个方法, 在初始化配置完成后 运行此方法
- */
- @Override
- public void afterPropertiesSet() throws ServletException {
- super.afterPropertiesSet();
- ValidateCodeProperties code = securityProperties.getCode();
- logger.info(String.valueOf(code));
- // 将 application 配置中的 url 属性进行 切割
- String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(), ",");
- // 添加到 Set 集合里
- urls.addAll(Arrays.asList(configUrls));
- // 因为登录请求一定要有验证码 , 所以直接 add 到 set 集合中
- urls.add("/authentication/form");
- }
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- boolean action = false;
- for (String url:urls){
- // 如果请求的 url 和 配置中的 url 相匹配
- if (pathMatcher.match(url,request.getRequestURI())){
- action = true;
- }
- }
- // 拦截请求
- if (action){
- logger.info("拦截成功"+request.getRequestURI());
- // 如果是登录请求
- try {
- validate(new ServletWebRequest(request));
- }catch (ValidateCodeException exception){
- // 返回错误信息给 失败处理器
- failureHandler.onAuthenticationFailure(request,response,exception);
- return;
- }
- }else {
- // 不做任何处理, 调用后面的 过滤器
- filterChain.doFilter(request,response);
- }
- }
- private void validate(ServletWebRequest request) throws ServletRequestBindingException {
- // 从 session 中取出 验证码
- ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request,ValidateCodeController.SESSION_KEY);
- // 从 request 请求中 取出 验证码
- String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),"imageCode");
- if (StringUtils.isBlank(codeInRequest)){
- logger.info("验证码不能为空");
- throw new ValidateCodeException("验证码不能为空");
- }
- if (codeInSession == null){
- logger.info("验证码不存在");
- throw new ValidateCodeException("验证码不存在");
- }
- if (codeInSession.isExpried()){
- logger.info("验证码已过期");
- sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
- throw new ValidateCodeException("验证码已过期");
- }
- if (!StringUtils.equals(codeInSession.getCode(),codeInRequest)){
- logger.info("验证码不匹配"+"codeInSession:"+codeInSession.getCode() +", codeInRequest:"+codeInRequest);
- throw new ValidateCodeException("验证码不匹配");
- }
- // 把对应 的 session 信息 删掉
- sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
- }
- /**
- * 失败 过滤器 getter and setter 方法
- */
- public AuthenticationFailureHandler getFailureHandler() {
- return failureHandler;
- }
- public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
- this.failureHandler = failureHandler;
- }
- /**
- * SecurityProperties 属性类 getter and setter 方法
- */
- public SecurityProperties getSecurityProperties() {
- return securityProperties;
- }
- public void setSecurityProperties(SecurityProperties securityProperties) {
- this.securityProperties = securityProperties;
- }
- }
按照 1.1 中 ValidateCodeFilter 类 调用三个方法的顺序一一介绍
validateCodeFilter.setFailureHandler(myFailHandler);
调用 登录失败处理器 getter and setter 方法
那么问题来了, 我们在第一节中有注入这个 登录失败处理器类 , 为什么在这里再进行 设置呢 ?
答案很简单, 因为是一个新的过滤器来调用它而且, 这个 ValidateCode 过滤器 会再 用户验证之前 进行
validateCodeFilter.setSecurityProperties(securityProperties);
这个其实就是 security applicaiton 配置属性 getter and setter 方法
这段代码就不说啥了, 道理和上面的一样因为在核心配置类 BrowserSecurityConfig 里, 我们有注入该对象, 然后把对象传递不传应该也可以因为该类中也有注入该对象, 但是传了肯定不报错
afterPropertiesSet()
方法
这个方法是 InitializingBean 接口下的一个方法, 在初始化配置完成后 运行此方法, 该方法的目的是 将我们在业务逻辑中 需要进行验证码验证的 url 做一个集合, 然后进行拦截(doFilterInternal() 方法里就是拦截 url, 然后进一步处理)
然后我把 application 配置贴出来
- # 图形验证码配置
- fantJ.security.code.image.length = 6
- fantJ.security.code.image.width = 100
- fantJ.security.code.image.url=/user,/user/*
根据
securityProperties.getCode().getImage().getUrl()
我们可以看出, 我在 SecurityProperties 下添加了 Code 对象, Code 下添加了 Image 对象, Image 下添加了 Url 对象 (后面我会把代码完全贴出来) 来获取配置中的 url, 并进行逗号切割, 然后放到 Set 集合中
ValidateCodeFilter 类中的 doFilterInternal()思路介绍
经过调用 afterPropertiesSet()方法, 我们已经拿到了需要拦截的 urls 集合, 然后我判断 request 请求中的 uri 是否是集合中的, 如果是, 拦截请求, 调用 validate(); 方法, validate()方法 将会从 session 中取出 验证码, 从 request 请求中 取出 验证码进行对比校验, 详情请看注释信息 ImageCode 是验证码实体类
3. controller 类生成验证码
上面提到了从 request 请求中 取出 验证码, 在此之前我们需要在 controller 里生成验证码并返回给用户, 用户填写提交我们才能从 request 请求中获取
- package com.fantJ.core.validate;
- /**
- * Created by Fant.J.
- */
- @RestController
- public class ValidateCodeController {
- public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
- /**
- * 引入 session
- */
- private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
- @Autowired
- private ValidateCodeGenerator imageCodeGenerator;
- @GetMapping("/code/image")
- public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
- ImageCode imageCode = imageCodeGenerator.createCode(new ServletWebRequest(request));
- // 将随机数 放到 Session 中
- sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
- // 写给 response 响应
- ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
- }
- }
我们随机生成验证码, 放到 session 中, 并返回给 response 客户端
4. 生成验证码类
- ImageCode .java
- package com.fantJ.core.validate;
- /**
- * 验证码信息类
- * Created by Fant.J.
- */
- public class ImageCode {
- /**
- * 图片
- */
- private BufferedImage image;
- /**
- * 随机数
- */
- private String code;
- /**
- * 过期时间
- */
- private LocalDateTime expireTime;
- public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
- this.image = image;
- this.code = code;
- this.expireTime = expireTime;
- }
- public ImageCode(BufferedImage image, String code, int expireIn) {
- this.image = image;
- this.code = code;
- // 当前时间 加上 设置过期的时间
- this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
- }
- public boolean isExpried(){
- // 如果 过期时间 在 当前日期 之前, 则验证码过期
- return LocalDateTime.now().isAfter(expireTime);
- }
- ...getter and setter
- }
注意这里面有过期时间的处理, 我们上传一个参数 int expireIn, 用当前时间 plus 这个参数, 然后在 isExpried()方法中, 再用当前时间和 plus 后的时间做比较 来判断 验证码是否过期
ValidateCodeGenerator .java 接口类
- /**
- * 验证码生成器
- * Created by Fant.J.
- */
- public interface ValidateCodeGenerator {
- /**
- * 创建验证码
- */
- ImageCode createCode(ServletWebRequest request);
- }
为什么要弄一个接口, 为了封装性, 如果我们以后想写一个更牛逼的验证码生成器, 可以不改原来的代码, 直接继承接口实现方法就 ok
- ImageCodeGenerator.java
- package com.fantJ.core.validate.code;
- /**
- * Created by Fant.J.
- */
- public class ImageCodeGenerator implements ValidateCodeGenerator {
- /**
- * 引入 Security 配置属性类
- */
- private SecurityProperties securityProperties;
- /**
- * 创建验证码
- */
- @Override
- public ImageCode createCode(ServletWebRequest request) {
- // 如果请求中有 width 参数, 则用请求中的, 否则用 配置属性中的
- int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
- // 高度(宽度)
- int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());
- // 图片验证码字符个数
- int length = securityProperties.getCode().getImage().getLength();
- // 过期时间
- int expireIn = securityProperties.getCode().getImage().getExpireIn();
- BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
- Graphics g = image.getGraphics();
- Random random = new Random();
- g.setColor(getRandColor(200, 250));
- g.fillRect(0, 0, width, height);
- g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
- g.setColor(getRandColor(160, 200));
- for (int i = 0; i <155; i++) {
- int x = random.nextInt(width);
- int y = random.nextInt(height);
- int xl = random.nextInt(12);
- int yl = random.nextInt(12);
- g.drawLine(x, y, x + xl, y + yl);
- }
- String sRand = "";
- for (int i = 0; i < length; i++) {
- String rand = String.valueOf(random.nextInt(10));
- sRand += rand;
- g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
- g.drawString(rand, 13 * i + 6, 16);
- }
- g.dispose();
- return new ImageCode(image, sRand, expireIn);
- }
- /**
- * 生成随机背景条纹
- */
- private 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);
- }
- public SecurityProperties getSecurityProperties() {
- return securityProperties;
- }
- public void setSecurityProperties(SecurityProperties securityProperties) {
- this.securityProperties = securityProperties;
- }
- }
最后, 注入该类到 spring 容器 ValidateCodeBeanConfig.java
- package com.fantJ.core.validate.code;
- /**
- * 验证码 实体类设置 类
- * Created by Fant.J.
- */
- @Configuration
- public class ValidateCodeBeanConfig {
- @Autowired
- private SecurityProperties securityProperties;
- @Bean
- @ConditionalOnMissingBean(name = "imageCodeGenerator")
- /**
- *
- *
- *
- * 在触发 ValidateCodeGenerator 之前会检测有没有 imageCodeGenerator 这个 bean
- */
- public ValidateCodeGenerator imageCodeGenerator(){
- ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
- codeGenerator.setSecurityProperties(securityProperties);
- return codeGenerator;
- }
- }
@ConditionalOnMissingBean 该注解其实就相当于我再 ImageCodeGenerator 类上加个 @Component 注解 他俩的不同是, 这个注解是个有条件的注解, 意思是如果 容器中 没有 ImageCodeGenerator 这个类, 我就创建这个类, 如果有, 就不做操作
为什么要这样呢? 如果我们以后重写了一个更牛 b 的生成验证码类, 我们可以直接给它上面添加 @Component 注解来注入, 就不用来管原来的代码, 也不用考虑 bean 名称的冲突
这就是生成验证码用的类效果如下
5. 异常类和 properties 类
- ValidateCodeException .java
- package com.fantJ.core.validate;
- import org.springframework.security.core.AuthenticationException;
- /**
- * 自定义 验证码异常类
- * Created by Fant.J.
- */
- public class ValidateCodeException extends AuthenticationException {
- public ValidateCodeException(String msg) {
- super(msg);
- }
- }
- SecurityProperties.java
- package com.fantJ.core.properties;
- import org.springframework.boot.context.properties.ConfigurationProperties;
- /**
- * Security 属性 类
- * Created by Fant.J.
- */
- @ConfigurationProperties(prefix = "fantJ.security")
- public class SecurityProperties {
- /**
- * 浏览器 属性类
- */
- private BrowserProperties browser = new BrowserProperties();
- /**
- * 验证码 属性类
- */
- private ValidateCodeProperties code = new ValidateCodeProperties();
- getter and setter...
- }
- ValidateCodeProperties.java
- package com.fantJ.core.properties;
- /**
- * 验证码 配置类
- * Created by Fant.J.
- */
- public class ValidateCodeProperties {
- /**
- * 图形验证码 配置属性
- */
- private ImageCodeProperties image = new ImageCodeProperties();
- getter and setter...
- }
- ImageCodeProperties.java
- package com.fantJ.core.properties;
- /**
- * 图形验证码 配置读取类
- * Created by Fant.J.
- */
- public class ImageCodeProperties {
- /**
- * 验证码宽度
- */
- private int width = 67;
- /**
- * 高度
- */
- private int height = 23;
- /**
- * 长度(几个数字)
- */
- private int length = 4;
- /**
- * 过期时间
- */
- private int expireIn = 60;
- /**
- * 需要图形验证码的 url
- */
- private String url;
- getter and setter ...
- }
最后, 再附赠大家一个登录页面 demo
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title > 登录</title>
- </head>
- <body>
- <h2 > 登录</h2>
- <h3 > 表单登录</h3>
- <form action="/authentication/form" method="post">
- <table>
- <tr>
- <td > 用户名:</td>
- <td><input type="text" name="username"></td>
- </tr>
- <tr>
- <td > 密码:</td>
- <td><input type="password" name="password"></td>
- </tr>
- <tr>
- <td > 验证码:</td>
- <td>
- <input type="text" name="imageCode">
- <img src="/code/image?width=100">
- </td>
- </tr>
- <tr>
- <td colspan="2"><button type="submit">登录</button></td>
- </tr>
- </table>
- </form>
- </body>
- </html>
效果展示
登录页
验证码为空
验证码错误
来源: https://juejin.im/entry/5abf4f59f265da238059c53d