Spring Security 安全之路——开发基于表单的认证(三)

  • 认证(你是谁)
  • 授权(你能干什么)
  • 攻击防护(防止伪造身份)

第一章、 Spring Security 原理介绍

  • 基于过滤器链实现认证、授权以及攻击防护。
  • 绿色的过滤器,可通过配置来决定是否生效。

1. 简单示例

package com.seapp.security.browser;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @author seapp
 * @date 2020/8/6 15:35
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * httpBasic():basic登录方式
         * formLogin():指定登录方式为表单认证
         * and():添加以下授权规则
         * authorizeRequests():认证权限
         * anyRequest():任何请求
         * authenticated():认证后才能访问
         */
//        http.httpBasic()
        http.formLogin()
                .and()
                .authorizeRequests()
                .anyRequest()
                .authenticated();
    }
}

对于任何请求都需认证后才能访问:

第二章、 基于Spring Security的默认实现开发“用户名+密码”认证

  • 处理用户信息获取逻辑 : UserDetailsService
  • 处理用户校验逻辑 :UserDetails
  • 处理密码加密解密 : PasswordEncoder
package com.seapp.security.browser;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @author seapp
 * @date 2020/8/6 16:30
 */
@Component
public class MyUserDetailsService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    public PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        logger.info("username:" + username);

        //根据用户名称查找用户信息
        return new User(username,passwordEncoder.encode("123456")
                //写死权限,工具类将字符串封装为需要的权限校验需要的对象
                , AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

1. 处理用户信息获取逻辑

实现UserDetailsService接口,在loadUserByUsername方法中可获取用户信息

2. 处理用户校验逻辑

package org.springframework.security.core.userdetails;

public interface UserDetails extends Serializable {
    //用户权限信息
    Collection<? extends GrantedAuthority> getAuthorities();
    //用户名
    String getUsername();
    //账户是在有效期
    boolean isAccountNonExpired();
    //账户是否锁定
    boolean isAccountNonLocked();
    //密码是否在有效期
    boolean isCredentialsNonExpired();
    //账户是否被删除
    boolean isEnabled();
}

3.处理密码加密解密

默认密码加密方式的配置

 @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

第四章、 个性化用户认证流程

1. 自定义登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<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 colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

2. 处理不同登录的请求

  • 来自PC端,针对html的请求,当认证失败时,返回html登录页面信息。
  • 来自移动端(Android/Ios),当认证失败时,返回json响应数据

当访问到达时,根据请求方式的不同,响应不同的结果

package com.seapp.security.browser;

/**
 * @author seapp
 * @date 2020/8/6 22:39
 */
@RestController
public class BrowserSecurityController {


    @Autowired
    private SecurityProperties securityProperties;

    private Logger logger = LoggerFactory.getLogger(getClass());

    private RequestCache requestCache = new HttpSessionRequestCache();

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    /**
     * 当需要认证的时候,调用该方法
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponse requireAuthentication(HttpServletRequest request,
                                        HttpServletResponse response) throws IOException {

        SavedRequest savedRequest = requestCache.getRequest(request,response);
        if(savedRequest != null){
            String targetUrl = savedRequest.getRedirectUrl();
            logger.info("引发跳转的请求是:" + targetUrl);
            if(StringUtils.endsWithIgnoreCase(targetUrl,".html")){
                redirectStrategy.sendRedirect(request,response,securityProperties.getBrowser().getLoginPage());
            }
        }
        return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页面");
    }

}

对响应json结果的封装

package com.seapp.security.browser.support;

/**定义封装好的返回对象
 * @author seapp
 * @date 2020/8/6 22:53
 */
public class SimpleResponse {


    public SimpleResponse(Object content) {
        this.content = content;
    }

    private Object content;

    public Object getContent() {
        return content;
    }

    public void setContent(Object content) {
        this.content = content;
    }
}

3. 作为基类,当第三方调用该封装框架是,传入自定义的登录页面实现

  • 通过在application.properties中配置seapp.security.browser.loginPage来实现自定义界面
seapp.security.browser.loginPage=/demo-signin.html
  • Security-core中对配置属性的获取

①:core中目录结构



②:对BrowserProperties的定义

package com.seapp.security.core.properties;

/**
 * @author seapp
 * @date 2020/8/6 23:03
 */
public class BrowserProperties {
    
    //当第三方引用未设定自定义登录页时,使用默认的登录页
    private String loginPage = "/seapp-signin.html";

    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }
}

③:通过SecurityProperties对配置属性的获取

package com.seapp.security.core.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author seapp
 * @date 2020/8/6 23:03
 */
@ConfigurationProperties(prefix = "seapp.security")
public class SecurityProperties {

    private BrowserProperties browser = new BrowserProperties();

    public BrowserProperties getBrowser() {
        return browser;
    }

    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }
}

④:配置读取器

package com.seapp.security.core;

import com.seapp.security.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @author seapp
 * @date 2020/8/6 23:07
 */
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)//让设置的配置读取器生效
public class SecurityConfig {
}

结合以下 两个类配置属性的引用,实现对自定义登录页面的加载


4. 自定义登录成功处理

AuthenticationSuccessHandler

package com.seapp.security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.seapp.security.core.properties.LoginResponseType;
import com.seapp.security.core.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author seapp
 * @date 2020/8/8 10:21
 */
@Component("seappAuthenticationSuccessHandler")
public class SeappAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response
            , Authentication authentication) throws ServletException, IOException {

        logger.info("登录成功");

        if(String.valueOf(LoginResponseType.JSON).equals(securityProperties.getBrowser().getLoginType())){
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }else {
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}

5. 自定义登录失败处理

AuthenticationFailureHandler

package com.seapp.security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.seapp.security.browser.support.SimpleResponse;
import com.seapp.security.core.properties.LoginResponseType;
import com.seapp.security.core.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author seapp
 * @date 2020/8/7 23:42
 */
@Component("seappAuthenticationFailureHandler")
public class SeappAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {


    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        logger.info("登录失败");
        if(LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())){
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(exception.getMessage())));
        }else {
            super.onAuthenticationFailure(request, response, exception);
        }
    }
}

6. 自定义成功及识别处理的配置:

第五章、认证流程源码级详解

  • 认证处理流程说明
  • 认证结果如何在多个请求之间共享
  • 获取认证用户信息

第六章 实现图形验证码功能

1. 开发生成图形验证码接口

  • 根据随机数生成图片
  • 将随机数存到Session中
  • 将生成的图片写到接口的响应中
  • 在认证流程中加入图形验证码校验
package com.seapp.security.core.validate;

import com.seapp.security.core.validate.code.ImageCode;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/**
 * 调用Controller中/code/image接口,生成随机图片,并将图片信息存到session中。
 * @author seapp
 * @date 2020/8/7 11:32
 */
@RestController
public class ValidateCodeController {

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {


        ImageCode imageCode = creaetImageCode(request);
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());

    }

    /**
     * 生成随机验证图片
     * @param request
     * @return
     */
    private ImageCode creaetImageCode(HttpServletRequest request) {

        int width = 120;// 定义图片的width
        int height = 45;// 定义图片的height
        int codeCount = 4;// 定义图片上显示验证码的个数
        int xx = 22;
        int fontHeight = 35;
        int codeY = 35;
        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', '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',
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

        // 定义图像buffer
        BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics gd = buffImg.getGraphics();
        // 创建一个随机数生成器类
        Random random = new Random();
        // 将图像填充为白色
        gd.setColor(Color.WHITE);
        gd.fillRect(0, 0, width, height);

        // 创建字体,字体的大小应该根据图片的高度来定。
        Font font = new Font("Fixedsys", Font.BOLD, fontHeight);
        // 设置字体。
        gd.setFont(font);

        // 画边框。
        gd.setColor(Color.BLACK);
        gd.drawRect(0, 0, width - 1, height - 1);

        // 随机产生50条干扰线,使图象中的认证码不易被其它程序探测到。
        gd.setColor(Color.BLACK);
        for (int i = 0; i < 50; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            gd.drawLine(x, y, x + xl, y + yl);
        }

        // randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
        StringBuffer randomCode = new StringBuffer();
        int red = 0, green = 0, blue = 0;

        // 随机产生codeCount数字的验证码。
        for (int i = 0; i < codeCount; i++) {
            // 得到随机产生的验证码数字。
            String code = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]);
            // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
            red = random.nextInt(255);
            green = random.nextInt(255);
            blue = random.nextInt(255);

            // 用随机产生的颜色将验证码绘制到图像中。
            gd.setColor(new Color(red, green, blue));
            gd.drawString(code, (i + 1) * xx, codeY);

            // 将产生的四个随机数组合在一起。
            randomCode.append(code);
        }

        ImageCode imageCode = new ImageCode(buffImg,randomCode.toString(),60);

        return imageCode;
    }

}
  • ImageCode类实现对图片信息的封装
package com.seapp.security.core.validate.code;

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

/**
 * @author seapp
 * @date 2020/8/7 11:19
 */
public class ImageCode {

    private BufferedImage image;

    private String code;

    private LocalDateTime expireTime;

    /**
     * 当传递过期时间参数时,获取具体的过期时间
     * @param image
     * @param code
     * @param expireIn
     */
    public ImageCode(BufferedImage image, String code, int expireIn) {
        this.image = image;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }


    public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
        this.image = image;
        this.code = code;
        this.expireTime = expireTime;
    }

    public boolean isExpired(){
        return LocalDateTime.now().isAfter(expireTime);
    }

    public BufferedImage getImage() {
        return image;
    }

    public void setImage(BufferedImage image) {
        this.image = image;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public LocalDateTime getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime;
    }
}

  • 请求中发送获取到随机图片的code值,在用户名密码校验之前添加图片校验的过滤器,当图片校验成功后再执行用户名与密码的校验过滤
package com.seapp.security.core.validate.code;

import com.seapp.security.core.validate.ValidateCodeController;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author seapp
 * @date 2020/8/7 23:08
 *
 * 继承 OncePerRequestFilter过滤器,保障该过滤器只执行一次。
 * 实现对图片码的校验,在该类中可引入自定义的失败处理器。
 *
 */
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        if(StringUtils.equals("/authentication/form",request.getRequestURI())
                    && StringUtils.equals("POST",request.getMethod())){
            //登录请求,执行图片校验
            try{
                validate(new ServletWebRequest(request));
            }catch (ValidateCodeException exception){
                authenticationFailureHandler.onAuthenticationFailure(request,response,exception);
                return;
            }


        }

        //若不满足上述条件,则不是登录请求,则执行后续的请求
        filterChain.doFilter(request,response);

    }

    private void validate(ServletWebRequest request) throws ServletRequestBindingException {

        ImageCode codeInSession = (ImageCode) sessionStrategy
                .getAttribute(request, ValidateCodeController.SESSION_KEY);
        String codeInRequest = ServletRequestUtils
                .getStringParameter(request.getRequest(),"imageCode");

        if(StringUtils.isBlank(codeInRequest)){
            throw new ValidateCodeException("验证码的值不能为空");
        }

        if(codeInSession == null){
            throw new ValidateCodeException("验证码不存在");
        }

        if(codeInSession.isExpired()){
            //将过期的验证码在session中移除
            sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }

        if(StringUtils.equals(codeInSession.getCode(),codeInRequest)){
            throw  new ValidateCodeException("验证码不匹配");
        }

        //校验全部通过后,移除在session中校验码
        sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);

    }

    public AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    public SessionStrategy getSessionStrategy() {
        return sessionStrategy;
    }

    public void setSessionStrategy(SessionStrategy sessionStrategy) {
        this.sessionStrategy = sessionStrategy;
    }
}

  • 图片认证码校验过滤器的配置:

2. 图片校验代码重构,支持第三方应用以及配置

  • 图片验证码的基本参数可配置(图片大小、验证码长度、有效期时间等)
  • 验证码拦截的接口可配置(可定义那些接口拦截、那些接口放行)
  • 验证码的生成逻辑可配置

定义ImageCodeProperties配置接收类,参数包含图片宽度、高度、校验码长度、以及拦截接口

package com.seapp.security.core.properties;

/**
 * @author seapp
 * @date 2020/8/8 16:05
 */
public class ImageCodeProperties {

    private int width = 120;// 定义图片的width
    private int height = 45;// 定义图片的height
    private int codeCount = 4;// 定义图片上显示验证码的个数
    private int expireIn = 60;//过期时间

    private String url;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getCodeCount() {
        return codeCount;
    }

    public void setCodeCount(int codeCount) {
        this.codeCount = codeCount;
    }

    public int getExpireIn() {
        return expireIn;
    }

    public void setExpireIn(int expireIn) {
        this.expireIn = expireIn;
    }
}

*对其进行封装,最后至于SecurityProperties配置类中
@ConfigurationProperties(prefix = "seapp.security")//该配置定义application.properties配置类中的前缀

package com.seapp.security.core.properties;

/**
 * @author seapp
 * @date 2020/8/6 23:03
 */
public class ValidateCodeProperties {

    ImageCodeProperties image = new ImageCodeProperties();

    public ImageCodeProperties getImage() {
        return image;
    }

    public void setImage(ImageCodeProperties image) {
        this.image = image;
    }
}


package com.seapp.security.core.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author seapp
 * @date 2020/8/6 23:03
 */
@ConfigurationProperties(prefix = "seapp.security")
public class SecurityProperties {

    private BrowserProperties browser = new BrowserProperties();

    private ValidateCodeProperties code = new ValidateCodeProperties();

    public ValidateCodeProperties getCode() {
        return code;
    }

    public void setCode(ValidateCodeProperties code) {
        this.code = code;
    }

    public BrowserProperties getBrowser() {
        return browser;
    }

    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }
}

配置示例如图所示

参数的获取,以及使用。参数获取中包括对接口请求中传递的宽、高参数的获取。

package com.seapp.security.core.validate.code;

import com.seapp.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestUtils;

import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

/**
 * @author seapp
 * @date 2020/8/8 16:48
 */
public class ImageCodeGenerator implements ValidateCodeGenerator {

    private SecurityProperties securityProperties;

    @Override
    public ImageCode creaetImageCode(HttpServletRequest request) {
        //从请求参数中获取width与height的值,若没有使用配置项中的。配置项也那不到的话,使用默认配置。
        int width = ServletRequestUtils.getIntParameter(request,"width"
                ,securityProperties.getCode().getImage().getWidth());// 定义图片的width
        int height = ServletRequestUtils.getIntParameter(request,"height"
                ,securityProperties.getCode().getImage().getHeight());// 定义图片的height
        int codeCount = securityProperties.getCode().getImage().getCodeCount();// 定义图片上显示验证码的个数
        int xx = 22;
        int fontHeight = 35;
        int codeY = 35;
        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', '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',
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

        // 定义图像buffer
        BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics gd = buffImg.getGraphics();
        // 创建一个随机数生成器类
        Random random = new Random();
        // 将图像填充为白色
        gd.setColor(Color.WHITE);
        gd.fillRect(0, 0, width, height);

        // 创建字体,字体的大小应该根据图片的高度来定。
        Font font = new Font("Fixedsys", Font.BOLD, fontHeight);
        // 设置字体。
        gd.setFont(font);

        // 画边框。
        gd.setColor(Color.BLACK);
        gd.drawRect(0, 0, width - 1, height - 1);

        // 随机产生50条干扰线,使图象中的认证码不易被其它程序探测到。
        gd.setColor(Color.BLACK);
        for (int i = 0; i < 50; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            gd.drawLine(x, y, x + xl, y + yl);
        }

        // randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
        StringBuffer randomCode = new StringBuffer();
        int red = 0, green = 0, blue = 0;

        // 随机产生codeCount数字的验证码。
        for (int i = 0; i < codeCount; i++) {
            // 得到随机产生的验证码数字。
            String code = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]);
            // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
            red = random.nextInt(255);
            green = random.nextInt(255);
            blue = random.nextInt(255);

            // 用随机产生的颜色将验证码绘制到图像中。
            gd.setColor(new Color(red, green, blue));
            gd.drawString(code, (i + 1) * xx, codeY);

            // 将产生的四个随机数组合在一起。
            randomCode.append(code);
        }
        //通过securityProperties对象获取配置的过期时间
        ImageCode imageCode = new ImageCode(buffImg,randomCode.toString()
                ,securityProperties.getCode().getImage().getExpireIn());

        return imageCode;
    }

    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
}

** 可拦截接口的配置,这个需在ValidateFilter过滤器中实现**

/**
 * @author seapp
 * @date 2020/8/7 23:08
 *
 * 继承 OncePerRequestFilter过滤器,保障该过滤器只执行一次。
 *  实现接口InitializingBean,是可以在最后去组装urls参数
 *
 */
@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    //该工具类实现对正则字符串的匹配
    private AntPathMatcher pathMatcher = new AntPathMatcher();

    /**
     * 拦截器中接收传递过来的需要过滤的url参数
     */
    private Set<String> urls = new HashSet<>();

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(
                securityProperties.getCode().getImage().getUrl(),",");
        for (String configUrl:configUrls) {
            urls.add(configUrl);
        }
        //登录接口是必须校验的,所以在读取完配置后,默认将该接口参数添加上。
        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符合匹配参数时,将action值置位true,进行图片校验
            if(pathMatcher.match(url,request.getRequestURI())){
                action = true;
            }
        }
        if(action){
            //登录请求,执行图片校验
            try{
                validate(new ServletWebRequest(request));
            }catch (ValidateCodeException exception){
                authenticationFailureHandler.onAuthenticationFailure(request,response,exception);
                return;
            }


        }

        //若不满足上述条件,则不是登录请求,则执行后续的请求
        filterChain.doFilter(request,response);

    }

    private void validate(ServletWebRequest request) throws ServletRequestBindingException {

        ImageCode codeInSession = (ImageCode) sessionStrategy
                .getAttribute(request, ValidateCodeController.SESSION_KEY);
        String codeInRequest = ServletRequestUtils
                .getStringParameter(request.getRequest(),"imageCode");

        if(StringUtils.isBlank(codeInRequest)){
            throw new ValidateCodeException("验证码的值不能为空");
        }

        if(codeInSession == null){
            throw new ValidateCodeException("验证码不存在");
        }

        if(codeInSession.isExpired()){
            //将过期的验证码在session中移除
            sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }

        if(StringUtils.equals(codeInSession.getCode(),codeInRequest)){
            throw  new ValidateCodeException("验证码不匹配");
        }

        //校验全部通过后,移除在session中校验码
        sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);

    }

    public AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    public SessionStrategy getSessionStrategy() {
        return sessionStrategy;
    }

    public void setSessionStrategy(SessionStrategy sessionStrategy) {
        this.sessionStrategy = sessionStrategy;
    }


    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
}

注意:验证码逻辑的可配置

  • 实现接口
package com.seapp.security.core.validate.code;

import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/** 供外部实现校验码图片的生成逻辑
 * @author seapp
 * @date 2020/8/8 16:47
 */
public interface ValidateCodeGenerator {

    ImageCode creaetImageCode(HttpServletRequest request);
}

  • 提供默认实现
    ImageCodeGenerator实现类(文档上方有)
  • 通过JavaBean类配置实现可替换逻辑
package com.seapp.security.core.validate.code;

import com.seapp.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author seapp
 * @date 2020/8/8 17:01
 */
@Configuration
public class ValidateCodeConfig {

    @Autowired
    private SecurityProperties securityProperties;

    @Bean
    @ConditionalOnMissingBean(name = "imageCodeGenerator")
    //在加载该注册bean时,若容器中已经有了该名称命名的bean类,则不再加加载。
    public ValidateCodeGenerator imageCodeGenerator(){
        ImageCodeGenerator imageCodeGenerator = new ImageCodeGenerator();
        imageCodeGenerator.setSecurityProperties(securityProperties);
        return imageCodeGenerator;
    }

}
  • 测试类实现,需要实现ValidateCodeGenerator接口,并在Ioc中注册名称指定为"imageCodeGenerator"(在配置类中有标注),即可使用图片检验码自定义
package com.seapp.validator;

import com.seapp.security.core.validate.code.ImageCode;
import com.seapp.security.core.validate.code.ValidateCodeGenerator;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * @author seapp
 * @date 2020/8/8 17:13
 */
@Component("imageCodeGenerator")
public class DemoValidateCodeGenerator implements ValidateCodeGenerator {
    @Override
    public ImageCode creaetImageCode(HttpServletRequest request) {

        System.out.println("更高级的图形校验码生成工具类");

        return null;
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348