您现在的位置是:首页 >学无止境 >Spring Security 04 自定义认证网站首页学无止境

Spring Security 04 自定义认证

夜光下丶 2023-06-03 16:00:02
简介Spring Security 04 自定义认证

登录⽤户数据获取

SecurityContextHolder

        Spring Security 会将登录⽤户数据保存在 Session 中。但是,为了使⽤⽅便, Spring Security 在此基础上还做了⼀些改进,其中最主要的⼀个变化就是线程绑定。当⽤户登录成功后,Spring Security 会将登录成功的⽤户信息保存到SecurityContextHolder 中。

        SecurityContextHolder 中的数据保存默认是通过 ThreadLocal 来实现的,使⽤ ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是⽤户数据和请求线程绑定在⼀起。当登录请求处理完毕后, Spring Security 会将SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时, Spring Security 就会先从 Session 中取出⽤户登录数据,保存到SecurityContextHolder 中,⽅便在该请求的后续处理过程中使⽤,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder 中的数据清空。

        实际上 SecurityContextHolder 中存储是 SecurityContext,在SecurityContext 中存储是 Authentication。

这种设计是典型的策略设计模式:

public class SecurityContextHolder {

	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";

	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";

	public static final String MODE_GLOBAL = "MODE_GLOBAL";

	private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";

	public static final String SYSTEM_PROPERTY = "spring.security.strategy";

	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

	private static SecurityContextHolderStrategy strategy;

	private static int initializeCount = 0;

	static {
		initialize();
	}

	private static void initialize() {
		initializeStrategy();
		initializeCount++;
	}

	private static void initializeStrategy() {
		if (MODE_PRE_INITIALIZED.equals(strategyName)) {
			Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
					+ ", setContextHolderStrategy must be called with the fully constructed strategy");
			return;
		}
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}
		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
			return;
		}
		if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
			return;
		}
		if (strategyName.equals(MODE_GLOBAL)) {
			strategy = new GlobalSecurityContextHolderStrategy();
			return;
		}
		// Try to load a custom strategy
		try {
			Class<?> clazz = Class.forName(strategyName);
			Constructor<?> customStrategy = clazz.getConstructor();
			strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
		}
		catch (Exception ex) {
			ReflectionUtils.handleReflectionException(ex);
		}
	}

	/**
	 * Explicitly clears the context value from the current thread.
	 */
	public static void clearContext() {
		strategy.clearContext();
	}

	/**
	 * Obtain the current <code>SecurityContext</code>.
	 * @return the security context (never <code>null</code>)
	 */
	public static SecurityContext getContext() {
		return strategy.getContext();
	}

	/**
	 * Primarily for troubleshooting purposes, this method shows how many times the class
	 * has re-initialized its <code>SecurityContextHolderStrategy</code>.
	 * @return the count (should be one unless you've called
	 * {@link #setStrategyName(String)} or
	 * {@link #setContextHolderStrategy(SecurityContextHolderStrategy)} to switch to an
	 * alternate strategy).
	 */
	public static int getInitializeCount() {
		return initializeCount;
	}

	/**
	 * Associates a new <code>SecurityContext</code> with the current thread of execution.
	 * @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
	 */
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}

	/**
	 * Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
	 * a given JVM, as it will re-initialize the strategy and adversely affect any
	 * existing threads using the old strategy.
	 * @param strategyName the fully qualified class name of the strategy that should be
	 * used.
	 */
	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}

	/**
	 * Use this {@link SecurityContextHolderStrategy}.
	 *
	 * Call either {@link #setStrategyName(String)} or this method, but not both.
	 *
	 * This method is not thread safe. Changing the strategy while requests are in-flight
	 * may cause race conditions.
	 *
	 * {@link SecurityContextHolder} maintains a static reference to the provided
	 * {@link SecurityContextHolderStrategy}. This means that the strategy and its members
	 * will not be garbage collected until you remove your strategy.
	 *
	 * To ensure garbage collection, remember the original strategy like so:
	 *
	 * <pre>
	 *     SecurityContextHolderStrategy original = SecurityContextHolder.getContextHolderStrategy();
	 *     SecurityContextHolder.setContextHolderStrategy(myStrategy);
	 * </pre>
	 *
	 * And then when you are ready for {@code myStrategy} to be garbage collected you can
	 * do:
	 *
	 * <pre>
	 *     SecurityContextHolder.setContextHolderStrategy(original);
	 * </pre>
	 * @param strategy the {@link SecurityContextHolderStrategy} to use
	 * @since 5.6
	 */
	public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
		Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
		SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
		SecurityContextHolder.strategy = strategy;
		initialize();
	}

	/**
	 * Allows retrieval of the context strategy. See SEC-1188.
	 * @return the configured strategy for storing the security context.
	 */
	public static SecurityContextHolderStrategy getContextHolderStrategy() {
		return strategy;
	}

	/**
	 * Delegates the creation of a new, empty context to the configured strategy.
	 */
	public static SecurityContext createEmptyContext() {
		return strategy.createEmptyContext();
	}

}

 

  1. MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,⼤家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实⾮常适合 web 应⽤,因为在默认情况下,⼀个请求⽆论经过多少 Filter 到达 Servlet,都是由⼀个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了⼦线程,在⼦线程中去获取登录⽤户数据,就会获取不到。
  2. MODE INHERITABLETHREADLOCAL:这种存储模式适⽤于多线程环境,如果希望在⼦线程中也能够获取到登录⽤户数据,那么可以使⽤这种存储模式。
  3. MODE GLOBAL:这种存储模式实际上是将数据保存在⼀个静态变量中,在 JavaWeb 开发中,这种模式很少使⽤到。

SecurityContextHolderStrategy

通过 SecurityContextHolder 可以得知, SecurityContextHolderStrategy 接⼝⽤来定义存储策略⽅法

public interface SecurityContextHolderStrategy {

	/**
	 * Clears the current context.
	 */
	void clearContext();

	/**
	 * Obtains the current context.
	 */
	SecurityContext getContext();

	/**
	 * Sets the current context.
	 */
	void setContext(SecurityContext context);

	/**
	 * Creates a new, empty context implementation, for use by
	 */
	SecurityContext createEmptyContext();

}

从上⾯可以看出每⼀个实现类对应⼀种策略的实现。

获取用户数据 

   @GetMapping("/hello")
    public String hello() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        return user.toString();
    }

多线程下获取用户数据

    @GetMapping("/hello")
    public String hello() {
        new Thread(() -> {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            User user = (User) authentication.getPrincipal();
            System.out.println(user.toString());
        }).start();
        return "hello page success";
    }

可以看到默认策略,是⽆法在⼦线程中获取⽤户信息,如果需要在⼦线程中获取必须使⽤第⼆种策略,默认策略是通过 System.getProperty 加载的,因此我们可以通过增加 VM Options 参数进⾏修改。 

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

⾃定义认证数据源 

Servlet Authentication Architecture :: Spring Security

 

  • 发起认证请求,请求中携带⽤户名、密码,该请求会被 UsernamePasswordAuthenticationFilter 拦截
  • 在 UsernamePasswordAuthenticationFilter 的 attemptAuthentication ⽅法中将请求中⽤户名和密码,封装为Authentication 对象,并交给 AuthenticationManager 进⾏认证
  • 认证成功,将认证信息存储到 SecurityContextHodler 以及调⽤记住我等,并回调 AuthenticationSuccessHandler 处理
  • 认证失败,清除 SecurityContextHodler 以及 记住我中信息,回调 AuthenticationFailureHandler 处理

三者关系

从上⾯分析中得知, AuthenticationManager 是认证的核⼼类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider 。他们三者关系是样的呢?

  • AuthenticationManager 是⼀个认证管理器,它定义了 Spring Security 过滤器要执⾏认证操作。
  • ProviderManager AuthenticationManager接⼝的实现类。 Spring Security认证时默认使⽤就是 ProviderManager
  • AuthenticationProvider 就是针对不同的身份类型执⾏的具体的身份认证。

AuthenticationManager 与 ProviderManager

        ProviderManager 是 AuthenticationManager 的唯⼀实现,也是 Spring Security 默认使⽤实现。从这⾥不难看出默认情况下AuthenticationManager 就是⼀个ProviderManager。

ProviderManager 与 AuthenticationProvider

 

 

        在 Spring Seourity 中,允许系统同时⽀持多种不同的认证⽅式,例如同时⽀持⽤户名/密码认证、 ReremberMe 认证、⼿机号码动态认证等,⽽不同的认证⽅式对应了不同的 AuthenticationProvider,所以⼀个完整的认证流程可能由多个AuthenticationProvider 来提供。

        多个 AuthenticationProvider 将组成⼀个列表,这个列表将由 ProviderManager 代理。换句话说,在ProviderManager 中存在⼀个 AuthenticationProvider 列表,在Provider Manager 中遍历列表中的每⼀个 AuthenticationProvider 去执⾏身份认证,最终得到认证结果。

        ProviderManager 本身也可以再配置⼀个 AuthenticationManager 作为 parent,这样当ProviderManager 认证失败之后,就可以进⼊到 parent 中再次进⾏认证。理论上来说, ProviderManager 的 parent 可以是任意类型的AuthenticationManager,但是通常都是由 ProviderManager 来扮演 parent 的⻆⾊,也就是 ProviderManager 是ProviderManager 的 parent。​ ProviderManager 本身也可以有多个,多个ProviderManager 共⽤同⼀个 parent。有时,⼀个应⽤程序有受保护资源的逻辑组(例如,所有符合路径模式的⽹络资源,如/api!!*),每个组可以有⾃⼰的专⽤ AuthenticationManager。通常,每个组都是⼀个ProviderManager,它们共享⼀个⽗级。然后,⽗级是⼀种 全局资源,作为所有提供者的后备资源。

Getting Started | Spring Security Architecture

        弄清楚认证原理之后我们来看下具体认证时数据源的获取。 默认情况下 AuthenticationProvider 是由 DaoAuthenticationProvider 类来实现认证的,在DaoAuthenticationProvider 认证时⼜通过 UserDetailsService 完成数据源的校验。 他们之间调⽤关系如下:

总结: AuthenticationManager 是认证管理器,在 Spring Security 中有全局 AuthenticationManager,也可以有局部AuthenticationManager。全局的 AuthenticationManager ⽤来对全局认证进⾏处理,局部的 AuthenticationManager ⽤来对某些特殊资源认证处理。当然⽆论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进⾏实现。 每⼀个ProviderManger 中都代理⼀个 AuthenticationProvider 的列表,列表中每⼀个实现代表⼀种身份认证⽅式。认证时底层数据源需要调⽤ UserDetailService 来实现

配置全局 AuthenticationManager

Getting Started | Spring Security Architecture

 

默认的全局 AuthenticationManager

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  @Autowired
  public void initialize(AuthenticationManagerBuilder builder) {
    //builder..
  }
}

springboot 对 security 进行自动配置时自动在工厂中创建一个全局AuthenticationManager 

总结

  • 默认自动配置创建全局AuthenticationManager 默认找当前项目中是否存在自定义 UserDetailService 实例 自动将当前项目 UserDetailService 实例设置为数据源
  • 默认自动配置创建全局AuthenticationManager 在工厂中使用时直接在代码中注入即可

自定义全局 AuthenticationManager

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  @Override
  public void configure(AuthenticationManagerBuilder builder) {
    //builder ....
  }
}

总结

  • 一旦通过 configure 方法自定义 AuthenticationManager实现 就回将工厂中自动配置AuthenticationManager 进行覆盖
  • 一旦通过 configure 方法自定义 AuthenticationManager实现 需要在实现中指定认证数据源对象 UserDetaiService 实例
  • 一旦通过 configure 方法自定义 AuthenticationManager实现 这种方式创建AuthenticationManager对象工厂内部本地一个 AuthenticationManager 对象 不允许在其他自定义组件中进行注入
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  
    //1.自定义AuthenticationManager  推荐  并没有在工厂中暴露出来
    @Override
    public void configure(AuthenticationManagerBuilder builder) throws Exception {
        System.out.println("自定义AuthenticationManager: " + builder);
        builder.userDetailsService(userDetailsService());
    }
​
    //作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
​

 

自定义内存数据源

@Configuration
public class WebSecurityConfig {
​
    @Bean
    public UserDetailsService userDetailsService(){
        UserDetails user = User.withUsername("admin").password("{noop}123").roles("ADMIN").build();
        return new InMemoryUserDetailsManager(user);
    }
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/index")
                .permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .successHandler(new LoginSuccessHandler())
                .failureHandler(new LoginFailureHandler())
                .and().logout().logoutSuccessHandler(new LogoutHandler())
                .and().userDetailsService(userDetailsService());
        return http.csrf().disable().build();
    }
}

自定义数据库数据源

-- 用户表
CREATE TABLE `user`
(
    `id`                    int(11) NOT NULL AUTO_INCREMENT,
    `username`              varchar(32)  DEFAULT NULL,
    `password`              varchar(255) DEFAULT NULL,
    `enabled`               tinyint(1) DEFAULT NULL,
    `accountNonExpired`     tinyint(1) DEFAULT NULL,
    `accountNonLocked`      tinyint(1) DEFAULT NULL,
    `credentialsNonExpired` tinyint(1) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- 角色表
CREATE TABLE `role`
(
    `id`      int(11) NOT NULL AUTO_INCREMENT,
    `name`    varchar(32) DEFAULT NULL,
    `name_zh` varchar(32) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- 用户角色关系表
CREATE TABLE `user_role`
(
    `id`  int(11) NOT NULL AUTO_INCREMENT,
    `uid` int(11) DEFAULT NULL,
    `rid` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY   `uid` (`uid`),
    KEY   `rid` (`rid`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- 插入用户数据
BEGIN;
  INSERT INTO `user`
  VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);
  INSERT INTO `user`
  VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);
  INSERT INTO `user`
  VALUES (3, 'cheny', '{noop}123', 1, 1, 1, 1);
COMMIT;

-- 插入角色数据
BEGIN;
  INSERT INTO `role`
  VALUES (1, 'ROLE_product', '商品管理员');
  INSERT INTO `role`
  VALUES (2, 'ROLE_admin', '系统管理员');
  INSERT INTO `role`
  VALUES (3, 'ROLE_user', '用户管理员');
COMMIT;

-- 插入用户角色数据
BEGIN;
  INSERT INTO `user_role`
  VALUES (1, 1, 1);
  INSERT INTO `user_role`
  VALUES (2, 1, 2);
  INSERT INTO `user_role`
  VALUES (3, 2, 2);
  INSERT INTO `user_role`
  VALUES (4, 3, 3);
COMMIT;

项目中引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.7</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

配置 springboot 配置文件

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2B8&allowMultiQueries=true
    username: root
    password: root
​
mybatis:
  mapper-locations: mapper/*Mapper.xml
  type-aliases-package: com.yang.entity

创建 entity

@Data
public class User implements UserDetails {
​
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    private List<Role> roles = new ArrayList<>();
​
​
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
        return grantedAuthorities;
    }
​
    @Override
    public String getPassword() {
        return password;
    }
​
    @Override
    public String getUsername() {
        return username;
    }
​
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }
​
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }
​
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }
​
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

@Data
public class Role {
​
    private Integer id;
    private String name;
    private String nameZh;
}

创建 UserMapper 接口,编写sql语句

@Mapper
public interface UserMapper {
​
    //根据用户名查询用户
    User loadUserByUsername(String username);
​
    //根据用户id查询角色
    List<Role> getRolesByUid(Integer uid);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yang.mapper.UserMapper">
    <!--查询单个-->
    <select id="loadUserByUsername" resultType="com.yang.entity.User">
        select id,
               username,
               password,
               enabled,
               accountNonExpired,
               accountNonLocked,
               credentialsNonExpired
        from user
        where username = #{username}
    </select>
​
    <!--查询指定行数据-->
    <select id="getRolesByUid" resultType="com.yang.entity.Role">
        select r.id,
               r.name,
               r.name_zh nameZh
        from role r,
             user_role ur
        where r.id = ur.rid
          and ur.uid = #{uid}
    </select>
</mapper>

创建 service

public interface UserService {
​
    UserDetails loadUserByUsername(String username);
}
​
​
@Service
public class UserServiceImpl implements UserService {
​
    private final UserMapper userMapper;
​
    @Autowired
    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
​
    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userMapper.loadUserByUsername(username);
        if(ObjectUtils.isEmpty(user)){
            throw new RuntimeException("用户不存在");
        }
        user.setRoles(userMapper.getRolesByUid(user.getId()));
        return user;
    }
}

创建 UserDetailsService

@Component
public class UserDetailService implements UserDetailsService {
​
    private final UserService userService;
​
    public UserDetailService(UserService userService) {
        this.userService = userService;
    }
​
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userService.loadUserByUsername(username);
    }
}

配置 authenticationManager 使用自定义UserDetailService

@Configuration
public class SecurityWebConfig {
​
    private final UserDetailService userDetailService;
​
    public SecurityWebConfig(UserDetailService userDetailService) {
        this.userDetailService = userDetailService;
    }
​
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
​
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/index")
                .permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .successHandler(new LoginSuccessHandler())
                .failureHandler(new LoginFailureHandler())
                .and().logout().logoutSuccessHandler(new LogoutHandler()) // 注销登入处理器
                .and().userDetailsService(userDetailService); // 自定义数据源
        return http.csrf().disable().build();
    }
}

添加验证码

 <dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

生成验证码

@Configuration
public class KaptchaConfig {
​
    @Bean
    public Producer kaptcha() {
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "150");
        properties.setProperty("kaptcha.image.height", "50");
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}
@RestController
public class KaptchaController {
    private final Producer producer;
​
    public KaptchaController(Producer producer) {
        this.producer = producer;
    }
​
    @GetMapping("/vc.png")
    public String getVerifyCode(HttpSession session) throws IOException {
        //1.生成验证码
        String code = producer.createText();
        session.setAttribute("kaptcha", code);//可以更换成 redis 实现
        BufferedImage bi = producer.createImage(code);
        //2.写入内存
        FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
        ImageIO.write(bi, "png", fos);
        //3.生成 base64
        return Base64.encodeBase64String(fos.toByteArray());
    }
}

定义验证码异常类

public class KaptchaNotMatchException extends AuthenticationException {
​
    public KaptchaNotMatchException(String msg) {
        super(msg);
    }
​
    public KaptchaNotMatchException(String msg, Throwable cause) {
        super(msg, cause);
    }
}

在自定义LoginKaptchaFilter中加入验证码验证

/**
 * @Author: chenyang
 * @DateTime: 2023/2/27 10:14
 * @Description: 自定义过滤器
 */
public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
​
    public static final String FORM_CAPTCHA_KEY = "captcha";
​
    private String kaptchaParameter = FORM_CAPTCHA_KEY;
​
    public String getKaptchaParameter() {
        return kaptchaParameter;
    }
​
    public void setKaptchaParameter(String kaptchaParameter) {
        this.kaptchaParameter = kaptchaParameter;
    }
​
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        try {
            //1.获取请求数据
            Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            String kaptcha = userInfo.get(getKaptchaParameter());//用来获取数据中验证码
            String username = userInfo.get(getUsernameParameter());//用来接收用户名
            String password = userInfo.get(getPasswordParameter());//用来接收密码
            //2.获取 session 中验证码
            String sessionVerifyCode = (String) request.getSession().getAttribute(FORM_CAPTCHA_KEY);
            if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionVerifyCode) &&
                    kaptcha.equalsIgnoreCase(sessionVerifyCode)) {
                //3.获取用户名 和密码认证
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new KaptchaNotMatchException("验证码不匹配!");
    }
}

配置

@Configuration
public class WebSecurityConfig {
​
    private final UserDetailService userDetailService;
​
    public WebSecurityConfig(UserDetailService userDetailService) {
        this.userDetailService = userDetailService;
    }
​
​
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
​
    @Bean
    public LoginKaptchaFilter loginKaptchaFilter(AuthenticationManager authenticationManager) {
        LoginKaptchaFilter filter = new LoginKaptchaFilter();
        //1.认证 url
        filter.setFilterProcessesUrl("/doLogin");
​
        //2.认证 接收参数
        filter.setUsernameParameter("username");
        filter.setPasswordParameter("pwd");
        filter.setKaptchaParameter("kaptcha");
​
        //3.指定认证管理器
        filter.setAuthenticationManager(authenticationManager);
​
        // 4.指定成功/失败时处理
        filter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
        filter.setAuthenticationFailureHandler(new LoginFailureHandler());
​
        return filter;
    }
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/index", "/vc.png")
                .permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().logout().logoutSuccessHandler(new LogoutHandler()) // 注销登入处理器
                .and().exceptionHandling().authenticationEntryPoint(new UnAuthenticationHandler()) // 未认证处理器
                .and().userDetailsService(userDetailService) // 自定义数据源
                .addFilterBefore(loginKaptchaFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class); // 自定义过滤器
        return http.csrf().disable().build();
    }
}

自定义认证异常处理类

/**
 * @Author: chenyang
 * @DateTime: 2023/2/27 11:27
 * @Description: 未认证时请求处理器
 */
public class UnAuthenticationHandler implements AuthenticationEntryPoint {
​
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().println("必须认证之后才能访问!");
    }
}

spring cloud security 中的 AuthenticationEntryPoint 设置与 AccessDeniedException 捕获过程 - 掘金

SpringSecurity系列 之 AuthenticationEntryPoint接口及其实现类的用法_oauth2authenticationentrypoint_姠惢荇者的博客-CSDN博客

测试验证 

调用接口获取图片的Base64 编码,再将编码转换成图片

登入

调用获取验证码接口时会自动保存session 

 

 

 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。