05. Spring Security 图形验证码

05. Spring Security 图形验证码

参考:

https://blog.csdn.net/yuanlaijike/article/details/80253922

https://mrbird.cc/Spring-Security-ValidateCode.html

#依赖

        <dependency>
            <groupId>org.springframework.social</groupId>
            <artifactId>spring-social-config</artifactId>
            <version>1.1.6.RELEASE</version>
        </dependency>

#验证码对象Image

/**
 * 验证码
 */
@Data
public class ImageCode {

    //允许出现的序列值
    private char[] codeSequence = {'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', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    //有效时间
    private LocalDateTime ttl;
    //验证码内容
    private String code;
    //验证码长度
    private Integer codeCount = 4;
    private BufferedImage bufferedImage;

    public ImageCode(Integer ttl, BufferedImage bufferedImage) {
        this.ttl = LocalDateTime.now().plusSeconds(ttl);
        this.bufferedImage = bufferedImage;
    }

    public ImageCode(Integer ttl, String code, BufferedImage bufferedImage) {
        this.ttl = LocalDateTime.now().plusSeconds(ttl);
        this.code = code;
        this.bufferedImage = bufferedImage;
    }

    /**
     * 判断验证码是否过期
     */
    public boolean isExpire() {
        return LocalDateTime.now().isAfter(ttl);
    }
}

#生成验证码的controller

@Slf4j
@RestController
public class ValidateController {
    public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";
    //无法显示导包
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 生成验证码请求
     */
    @GetMapping("/code/image")
    public void createImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode(100, 36, 60);
        //存储一个session key 为SESSION_KEY_IMAGE_CODE, 值为imageCode的到session域中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
        log.warn("验证码"+imageCode.getCode());
        //将图片写入到前端
        ImageIO.write(imageCode.getBufferedImage(), "png", response.getOutputStream());
    }

    /**
     * @param width  验证码图片宽度
     * @param height 验证码图片长度
     * @param ttl    验证码有效时间 60s
     * @return
     */
    private ImageCode createImageCode(int width, int height, int ttl) {
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();
        //设置颜色
        g.setColor(getRandomColor(200, 259));
        //设置形状
        g.fillRect(0, 0, width, height);
        //设置字体颜色
        g.setColor(getRandomColor(160, 200));
        //设置字体
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        //干扰线
        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);
        }
        StringBuilder condeStr = new StringBuilder();
        ImageCode imageCode = new ImageCode(ttl, image);
        char[] codeSequence = imageCode.getCodeSequence();
        //验证码内容
        for (int i = 0; i < imageCode.getCodeCount(); i++) {
            //随机获取校验内容
            String rand = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]);
            //为每个字符设置不同的颜色
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 17 * i + 6, 25);
            condeStr.append(rand);
        }
        //将验证码的内容封装到对象中以便做对比
        imageCode.setCode(condeStr.toString());
        g.dispose();
        return imageCode;
    }

    /**
     * 获取随机颜色
     */
    private Color getRandomColor(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);
    }
}

#html

<form class="login-page" th:action="@{/login}" method="post">
    <div class="form">
        <h3>账户登录</h3>
        <input type="text" placeholder="用户名" name="username" required="required"/>
        <input type="password" placeholder="密码" name="password" required="required"/>
        <!--        自动登入设置的name必须是remember-me-->
        <span><input type="checkbox" name="remember-me"/>自动登录<span/>
        <!--        图形验证码-->
        <span style="display: inline">
            <input type="text" name="imageCode" placeholder="验证码" style="width: 50%;"/>
            <img src="/code/image"/>
        </span>
        <button type="submit">登录</button>
    </div>
</form>

img 的 src属性对应@GetMapping("/code/image")

#修改配置类

	/**
     * 校验图形验证码
     */
    @Bean
    public OncePerRequestFilter validatedCodeFilter() {
        return new ValidateCodeFilter();
    }

	@Override
    protected void configure(HttpSecurity http) throws Exception {
       		 http
                .authorizeRequests()
                //放行图形验证码
                .antMatchers("/login", "/code/image").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .successForwardUrl("/")
                .failureForwardUrl("/fail2")
                .and()
                .logout().permitAll()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .clearAuthentication(true)
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .and()
                .rememberMe()
                .tokenValiditySeconds(60)
                .tokenRepository(persistentTokenRepository())
                .userDetailsService(userDetailsService)
                .and()
                .csrf()
                .disable();
    }

启动后, 效果如下

#添加验证码校验

在校验验证码的过程中,可能会抛出各种验证码类型的异常,比如“验证码错误”、“验证码已过期”等,所以我们定义一个验证码类型的异常类:

/**
 * 自定义验证码错误
 */
public class ValidateCodeException extends AuthenticationException {
    private static final long serialVersionUID = 1L;
    public ValidateCodeException(String msg) {
        super(msg);
    }
}

注意,这里继承的是AuthenticationException而不是Exception

我们都知道,Spring Security实际上是由许多过滤器组成的过滤器链,处理用户登录逻辑的过滤器为UsernamePasswordAuthenticationFilter,而验证码校验过程应该是在这个过滤器之前的,即只有验证码校验通过后才去校验用户名和密码。由于Spring Security并没有直接提供验证码校验相关的过滤器接口,所以我们需要自己定义一个验证码校验的过滤器ValidateCodeFilter

  • 方法一: 重定向
/**
 * 自定义过滤器,如果没有权限验证,每发送一次请求调用一次
 */
public class ValidateCodeFilter extends OncePerRequestFilter {
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //只对登入请求校验
        if (isProtectedUrl(request)) {
            //将httpServlet包装为ServletWebRequest
            try {
                validateCode(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                //如果校验失败,请求转发
                request.getRequestDispatcher("/fail").forward(request,response);
            }
        }
        //放行请求
        filterChain.doFilter(request, response);
    }

    /**
     *  校验验证码
     * @param servletWebRequest 通过ServletWebRequest可以拿到webRequest(所有的web request请求)是一个适配器
     * @throws ServletRequestBindingException
     */
    private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
        //通过session key获取到对应的imageCode
        ImageCode codeInSession = (ImageCode) sessionStrategy
                .getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
        //从request获取name为imageCode的值
        String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
        if (StringUtils.isBlank(codeInRequest)){
            throw new ValidateCodeException("验证码不能为空");
        }else if (ObjectUtils.isEmpty(codeInSession)){
            throw new ValidateCodeException("验证码不存在");
        }else if (codeInSession.isExpire()){
            throw new ValidateCodeException("验证码过期");
        }else if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(),codeInRequest)){
            throw new ValidateCodeException("验证码不正确");
        }
        //如果校验通过, 删除session中的imageCode
        sessionStrategy.removeAttribute(servletWebRequest,ValidateController.SESSION_KEY_IMAGE_CODE);
    }

    /**
     * 过滤请求
     */
    private boolean isProtectedUrl(HttpServletRequest request) {
        return StringUtils.equalsIgnoreCase("/login", request.getRequestURL())
                && StringUtils.equalsIgnoreCase("post", request.getMethod());
    }
}

  • 方法二: failureHandler

Handler

public class FailureHandler implements AuthenticationFailureHandler {
    @Autowired
    private ObjectMapper mapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
    }
}

修改ValidateCodeFilter

    @Autowired
    AuthenticationFailureHandler failureHandler;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //只对登入请求校验
        if (isProtectedUrl(request)) {
            //将httpServlet包装为ServletWebRequest
            try {
                validateCode(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                failureHandler.onAuthenticationFailure(request,response,e);
                //这里必须要return,否则还是会执行doFilter,验证码不正确也会校验成功
                return;
            }
        }
        //放行请求
        filterChain.doFilter(request, response);
    }

配置类

    @Bean
    public AuthenticationFailureHandler failureHandler(){
        return new FailureHandler();
    }

	/**
     * 校验图形验证码
     */
    @Bean
    public OncePerRequestFilter validatedCodeFilter() {
        return new ValidateCodeFilter();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //添加过滤器
        http.addFilterBefore(validatedCodeFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                //放行图形验证码
                .antMatchers("/login", "/code/image").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .successForwardUrl("/")
                .failureHandler(failureHandler())
                .and()
                .logout().permitAll()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .clearAuthentication(true)
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .and()
                .rememberMe()
                .tokenValiditySeconds(60)
                .tokenRepository(persistentTokenRepository())
                .userDetailsService(userDetailsService)
                .and()
                .csrf()
                .disable();
    }
posted @ 2020-05-15 16:46  CyberPelican  阅读(285)  评论(0编辑  收藏  举报