SpringSecurity图形验证码认证

图形验证码认证

1.添加过滤器认证

image-20210508102431430

1.1 生成一张图形验证码

Kaptcha 是谷歌提供的一个生成图形验证码的 jar 包, 只要简单配置属性就可以生成。
参考 :https://github.com/penggle/kaptcha

  1. 添加Kaptcha依赖
<!--短信验证码-->
    <dependency>
      <groupId>com.github.penggle</groupId>
      <artifactId>kaptcha</artifactId>
    </dependency>
  1. 生成验证码配置类,在模块中创建KaptchaImageCodeConfig
@Configuration
public class KaptchaImageCodeConfig {
  @Bean
  public DefaultKaptcha getDefaultKaptcha(){
    DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
    Properties properties = new Properties();
    properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
    properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "192,192,192");
    properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "110");
    properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "36");
    properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
    properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "28");
    properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "宋体");
    properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
    // 图片效果
    properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL,
"com.google.code.kaptcha.impl.ShadowGimpy");
    Config config = new Config(properties);
    defaultKaptcha.setConfig(config);
    return defaultKaptcha;
 }
}
  1. 在CustomLoginController 提供请求接口,将验证码图片数据流写出
 @Autowired
  private DefaultKaptcha defaultKaptcha;
  /**
  * 获取图形验证码
  */
  @RequestMapping("/code/image")
  public void imageCode(HttpServletRequest request, HttpServletResponse response) throws
IOException {
    // 1. 获取验证码字符串
    String code = defaultKaptcha.createText();
    logger.info("生成的图形验证码是:" + code);
    // 2. 字符串把它放到session中
    request.getSession().setAttribute(SESSION_KEY , code);
    // 3. 获取验证码图片
    BufferedImage image = defaultKaptcha.createImage(code);
    // 4. 将验证码图片把它写出去
    ServletOutputStream out = response.getOutputStream();
    ImageIO.write(image, "jpg", out);
 }
  1. 在 SpringSecurityConfig.configure(HttpSecurity http) 放行 /code/image 资源权限
.antMatchers(securityProperties.getAuthentication().getLoginPage(),
      "/code/image").permitAll()

1.2 实现验证码校验过滤器

  1. 创建ImageCodeValidateFilter,继承OncePerRequestFilter (在所有请求前都被调用一次)
  2. 如果是登录请求(请求地址:/login/form,请求方式:post),校验验证码输入是否正确校验不合法时,提示信息通过自定义异常 ValidateCodeExcetipn抛出,此异常要继承org.springframework.security.core.AuthenticationException,它是认证的父异常类。
    捕获ImageCodeException异常,交给失败处理器 CustomAuthenticationFailureHandler。
  3. 如果非登录请求,则放行请求 filterChain.doFilter(request, response)
/**
 * @author WGR
 * @create 2021/5/7 -- 16:08
 */
@Component
public class ImageCodeValidateFilter extends OncePerRequestFilter {

    @Autowired
    SecurityProperties securityProperties;
    @Autowired

    CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. 如果是post方式 的登录请求,则校验输入的验证码是否正确
        if (securityProperties.getAuthentication().getLoginProcessingUrl()
                .equals(request.getRequestURI())
                && request.getMethod().equalsIgnoreCase("post")) {
            try {
               // 校验验证码合法性
                validate(request);
            } catch (AuthenticationException e) {
                // 交给失败处理器进行处理异常
                customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                // 一定要记得结束
                return;
            }
        }
        // 放行请求
        filterChain.doFilter(request, response);
    }

    private void validate(HttpServletRequest request) {
        // 先获取seesion中的验证码
        String sessionCode = (String) request.getSession().getAttribute(CustomLoginController.SESSION_KEY);
         // 获取用户输入的验证码
        String inpuCode = request.getParameter("code");
        // 判断是否正确
        if (StringUtils.isBlank(inpuCode)) {
            throw new ValidateCodeException("验证码不能为空");
        }
        if (!inpuCode.equalsIgnoreCase(sessionCode)) {
            throw new ValidateCodeException("验证码输入错误");
        }
    }
}

1.3 创建验证码异常类

创建ValidateCodeExcetipn 异常类,它继承AuthenticationException特别注意是:org.springframework.security.core.AuthenticationException

/**
 * @author WGR
 * @create 2021/5/7 -- 16:15
 */
public class ValidateCodeException extends AuthenticationException {
    public ValidateCodeException(String msg, Throwable t) {
        super(msg, t);
    }

    public ValidateCodeException(String msg) {
        super(msg);
    }
}

1.4 重构SpringSecurityConfig

将校验过滤器 imageCodeValidateFilter 添加到UsernamePasswordAuthenticationFilter 前面

http.addFilterBefore(imageCodeValidateFilter,
          UsernamePasswordAuthenticationFilter.class)
 .formLogin()

image-20210508103746733

2.自定义认证

2.1 请求的简单流程

请求过程如图

image-20210508134858063

说明:

Authentication中包含主体权限列表,主体凭据,主体的详细信息,及是否验证成功等。

AuthenticationProvider被SpringSecurity定义为一个验证过程。

ProviderManager管理多个AuthenticationProvider。

当我们自定义的时候,Authentication还可以携带额外的数据

public interface Authentication extends Principal, Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();

	Object getCredentials();

    //允许携带任意对象
	Object getDetails();

	Object getPrincipal();

	boolean isAuthenticated();

	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

其中ProviderManager是由UsernamePasswordAuthenticationFilter调用的,也就是说所有AuthenticationProvider包含的Authentication都来源于UsernamePasswordAuthenticationFilter。

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}

		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

	protected void setDetails(HttpServletRequest request,
			UsernamePasswordAuthenticationToken authRequest) {
		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
	}

从上面的源码中看出,用户的详细信息是由authenticationDetailsSource构建的

public class WebAuthenticationDetailsSource implements
		AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

	// ~ Methods
	// ========================================================================================================

	/**
	 * @param context the {@code HttpServletRequest} object.
	 * @return the {@code WebAuthenticationDetails} containing information about the
	 * current request
	 */
	public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
		return new WebAuthenticationDetails(context);
	}
}
	public WebAuthenticationDetails(HttpServletRequest request) {
		this.remoteAddress = request.getRemoteAddr();

		HttpSession session = request.getSession(false);
		this.sessionId = (session != null) ? session.getId() : null;
	}

携带了session和ip地址。

2.2 代码

/**
 * @author WGR
 * @create 2021/5/8 -- 10:48
 */
public class MyWebAuthenticationDetails extends WebAuthenticationDetails {

    private boolean imageCodeIsRight = false;

    public boolean isImageCodeIsRight() {
        return imageCodeIsRight;
    }


    public MyWebAuthenticationDetails(HttpServletRequest request) {

        super(request);
        // 先获取seesion中的验证码
        String sessionCode = (String) request.getSession().getAttribute(CustomLoginController.SESSION_KEY);
        // 获取用户输入的验证码
        String inpuCode = request.getParameter("code");
        System.out.println(inpuCode);
        System.out.println(sessionCode);
        if(inpuCode.equals(sessionCode)){
            imageCodeIsRight = true;
        }
        System.out.println(imageCodeIsRight);

    }
}

/**
 * @author WGR
 * @create 2021/5/8 -- 10:55
 */
@Component
public class MyWebAuthenticationDetailsSource implements
        AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new MyWebAuthenticationDetails(context);
    }
}


/**
 * @author WGR
 * @create 2021/5/8 -- 10:41
 */
@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);

    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        MyWebAuthenticationDetails  details = (MyWebAuthenticationDetails)authentication.getDetails();
        if(!details.isImageCodeIsRight()){
            throw new ValidateCodeException("验证码输入错误");
        }
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

修改配置类

@Configuration
@EnableWebSecurity // 开启springsecurity过滤链 filter
public class SpringSecurityConfig2 extends WebSecurityConfigurerAdapter {
    Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private SecurityProperties securityProperties;


    @Autowired
    private CustomAuthenticationSuccessHandler2 customAuthenticationSuccessHandler;

    @Autowired
    private CustomAuthenticationFailureHandler2 customAuthenticationFailureHandler;


    @Autowired
    AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> myAuthenticationDetailsSource;

    @Autowired
    AuthenticationProvider authenticationProvider;

    @Autowired
    private DataSource dataSource;

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 明文+随机盐值》加密存储
        return new BCryptPasswordEncoder();
    }

    @Bean
    public JdbcTokenRepositoryImpl jdbcTokenRepository(){
       JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
       jdbcTokenRepository.setDataSource(dataSource);
       return jdbcTokenRepository;
    }

    /**
     * 认证管理器:
     * 1. 认证信息(用户名,密码)
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.httpBasic() // 采用 httpBasic认证方式
        http.formLogin().authenticationDetailsSource(myAuthenticationDetailsSource) // 表单登录方式
                .loginPage(securityProperties.getAuthentication().getLoginPage())
                .loginProcessingUrl(securityProperties.getAuthentication().getLoginProcessingUrl()) // 登录表单提交处理url, 默认是/login
                .usernameParameter(securityProperties.getAuthentication().getUsernameParameter()) //默认的是 username
                .passwordParameter(securityProperties.getAuthentication().getPasswordParameter())  // 默认的是 password
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .and()
                .authorizeRequests() // 认证请求
                .antMatchers(securityProperties.getAuthentication().getLoginPage(),"/code/image").permitAll() // 放行/login/page不需要认证可访问
                 //这里图片不放行的话就会看不见
                .anyRequest().authenticated() //所有访问该应用的http请求都要通过身份认证才可以访问
                .and()
        .rememberMe().tokenRepository(jdbcTokenRepository()).tokenValiditySeconds(60*60*24*7)
        ; // 注意不要少了分号
    }

    /**
     * * 释放静态资源
     * * @param web
     * 
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/dist/**", "/modules/**", "/plugins/**");
    }


}

原文地址:https://www.cnblogs.com/dalianpai/p/14744704.html