Spring Security 04 自定义认证

news2024/11/24 5:24:07

登录⽤户数据获取

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 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/448352.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Node内置模块 【crypto加密模块】

文章目录 &#x1f31f;前言&#x1f31f;crypto加密模块&#x1f31f;Crypto模块介绍&#x1f31f;Hash算法&#x1f31f;Hash算法介绍&#x1f31f;Hash算法之MD5&#x1f31f;算法简介&#x1f31f;MD5加密使用 &#x1f31f;Hash算法之SHA1&#x1f31f;算法简介&#x1f3…

二叉树经典题题解

目录 &#x1f345;1.单值二叉树&#x1f345; &#x1f349; 2.相同的树&#x1f349; &#x1f34a;3.对称二叉树&#x1f34a; &#x1f34e;4.另一颗树的子树&#x1f34e; &#x1f34f;5.翻转二叉树&#x1f34f; &#x1f351;6.平衡二叉树&#x1f351; &#x1f3…

【LeetCode: 1027. 最长等差数列 | 暴力递归=>记忆化搜索=>动态规划】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

SHELL的脚本编辑与运行

目录 1.编写脚本for1.sh,使用for循环创建20账户&#xff0c;账户名前缀由用户从键盘输入&#xff0c;账户初始密码由用户输入&#xff0c;例如&#xff1a;test1、test2、test3、、test10 a.编辑脚本 b.运行脚本 c.进行检验 2.编写脚本for2.sh,使用for循环&#xff0c;通过…

如何把aac转化为mp3,4个处理方法教给你

一般情况下&#xff0c;将 AAC 文件转换为 MP3 文件有以下几种情况&#xff1a;设备不兼容&#xff1a;AAC 格式通常用于苹果设备上。如果您想在其他设备上播放音频文件&#xff0c;如 Android 手机、MP3 播放器等&#xff0c;就需要将其转换为 MP3 格式。需要更好的兼容性&…

机器学习(七):基于多项式贝叶斯对蘑菇毒性分类预测分析

系列文章目录 作者&#xff1a;i阿极 作者简介&#xff1a;Python领域新星作者、多项比赛获奖者&#xff1a;博主个人首页 &#x1f60a;&#x1f60a;&#x1f60a;如果觉得文章不错或能帮助到你学习&#xff0c;可以点赞&#x1f44d;收藏&#x1f4c1;评论&#x1f4d2;关注…

第六章 原型模式

文章目录 前言一、克隆羊问题sheep类clint 调用方 二、引入原型模式动态克隆对象sheep类clint 类 三、原型模式在Spring框架中源码分析四、深拷贝与浅拷贝完整代码DeepCloneableTargetDeepProtoTypeClient2 五、原型模式的注意事项和细节 前言 一、克隆羊问题 sheep类 package…

mysql查询字段未加引号问题及隐式转换

1. 问题重现 最近线上出了个问题&#xff0c;用户明明没有投票&#xff0c;却提示已投票&#xff0c;我查询数据&#xff0c;刚开始没有查出数据&#xff0c;后来却查出数据了&#xff0c;以为没有问题&#xff0c;后来以为是插入的时候通过int类型插入&#xff0c;导致varcha…

美国最大公共养老基金之一Strs Ohio不断增持IonQ股票

​ &#xff08;图片来源&#xff1a;网络&#xff09; 截至2022年12月31日第四季度末&#xff0c;美国最大的公共养老基金之一Strs Ohio发布报告称&#xff0c;其一直在增持IonQ&#xff0c;Inc.&#xff08;纽约证券交易所代码&#xff1a;IONQ&#xff09;的股份。资产管理公…

如何在云服务器/云主机上部署最新版本的Hadoop3.3.5(Ubuntu20.0.4)

在云服务器上部署Hadoop 步骤1&#xff1a;更新系统 sudo apt-get update sudo apt-get upgrade步骤2&#xff1a;安装Java Hadoop需要Java运行环境。首先&#xff0c;安装OpenJDK 8&#xff1a; sudo apt-get install openjdk-8-jdk检查Java版本&#xff1a; java -versi…

上海车展:比亚迪宋L概念车全球首发,这是要硬扛特斯拉?

纵观2023年的新能源汽车市场&#xff0c;特斯拉可以说当仁不让地成为了全球最为“吸睛”的车企之一。凭借一系列令无数人瞠目结舌的降价举措&#xff0c;特斯拉给全球汽车市场带来了强烈冲击。虽然特斯拉上海工厂已经接近满负荷运转&#xff0c;但是面对雪片般飞来的订单依然供…

SpringBoot设置动态定时任务

SpringBoot设置动态定时任务 1.准备工作 搭建SpringBoot工程 引入相关依赖 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><depende…

ROS学习第八节——话题通信自定义msg

1.介绍 在 ROS 通信协议中&#xff0c;数据载体是一个较为重要组成部分&#xff0c;ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty.... 但是&#xff0c;这些数据一般只包含一个 data 字段&#xff0c;结构的单一意味着功能上的…

Java基础(九)多线程

我们之前学习的程序在没有跳转语句的情况下&#xff0c;都是由上至下沿着一条路径依次执行。现在想要设计一个程序&#xff0c;可以同时有多条执行路径同时执行。比如&#xff0c;一边游戏&#xff0c;一边qq聊天&#xff0c;一边听歌&#xff0c;怎么设计&#xff1f; 要解决…

每日一个小技巧:今天告诉你拍照识别文字的软件有哪些

在现代社会里&#xff0c;手机已经成为了人们生活中必不可少的工具。它的功能众多&#xff0c;比如通讯、上网、拍照以及导航等&#xff0c;为我们的生活带来了许多便利。除此之外&#xff0c;手机还能帮助我们解决一些实际的问题&#xff0c;例如&#xff0c;当你需要识别图片…

Spring Aop详解汇总

文章目录 近期想法什么是AOPSpringAOP与AspectjSpringAOP体系概述概念详解连接点- Jointpoint切入点- Pointcut通知- Advice切面- Aspect织入- Weaving 实现原理—动态代理JDK动态代理描述原理代码示例注意执行结果 优点缺点 CGLib动态代理描述原理代码示例注意执行结果 优点缺…

ChatGPT会凉吗?巴菲特、马斯克呼吁暂停

ChatGPT 迅速“风靡”全球&#xff0c;无疑成为了人工智能领域备受瞩目的“明星产品”&#xff0c;然而随着 ChatGPT 应用越来越广泛&#xff0c;陆续爆出被用于学术造假、制作黑客武器以及泄露用户敏感聊天信息等一系列负面新闻。至此&#xff0c;社会开始重新审视类似 ChatGP…

图像傅里叶变换以及频谱中心化

图像的空域滤波&#xff1a; I ∗ G I*G I∗G 矩阵 I I I与一个小矩阵 G G G进行卷积 图像的频域滤波&#xff1a; F − 1 [ F [ I ] H ] F^{-1}[F[I]\times H] F−1[F[I]H] 矩阵 I I I的傅里叶变换与同样大小的矩阵 H H H进行对应元素相乘&#xff0c;然后将结果通过逆傅里…

并查集解决图的连通性问题

并查集 1. 定义2.并查集3.模板代码4. 力扣例题4.1 剑指 Offer II 118. 多余的边4.2 力扣695. 岛屿的最大面积 1. 定义 在计算机科学中&#xff0c;并查集&#xff08;英文&#xff1a;Disjoint-set data structure&#xff0c;直译为不交集数据结构&#xff09;是一种数据结构&…

卷积神经网络轻量化教程之通道剪枝【附代码】

这两天自己手写了一个可以简单实现通道剪枝的代码&#xff0c;在这篇文章中也会对代码进行讲解&#xff0c;方便大家在自己代码中的使用。 如果还想学习YOLO系列的剪枝代码&#xff0c;可以参考我其他文章&#xff0c;下面的这些文章都是我根据通道剪枝的论文在YOLO上进行的实…