Spring Security 04 自定义认证

今天药忘吃喽~ 2024-03-23 16:10 230阅读 0赞

登录⽤户数据获取

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。

c968ba653baf43088badd71d3563d730.png

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

  1. public class SecurityContextHolder {
  2. public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
  3. public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
  4. public static final String MODE_GLOBAL = "MODE_GLOBAL";
  5. private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
  6. public static final String SYSTEM_PROPERTY = "spring.security.strategy";
  7. private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
  8. private static SecurityContextHolderStrategy strategy;
  9. private static int initializeCount = 0;
  10. static {
  11. initialize();
  12. }
  13. private static void initialize() {
  14. initializeStrategy();
  15. initializeCount++;
  16. }
  17. private static void initializeStrategy() {
  18. if (MODE_PRE_INITIALIZED.equals(strategyName)) {
  19. Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
  20. + ", setContextHolderStrategy must be called with the fully constructed strategy");
  21. return;
  22. }
  23. if (!StringUtils.hasText(strategyName)) {
  24. // Set default
  25. strategyName = MODE_THREADLOCAL;
  26. }
  27. if (strategyName.equals(MODE_THREADLOCAL)) {
  28. strategy = new ThreadLocalSecurityContextHolderStrategy();
  29. return;
  30. }
  31. if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
  32. strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
  33. return;
  34. }
  35. if (strategyName.equals(MODE_GLOBAL)) {
  36. strategy = new GlobalSecurityContextHolderStrategy();
  37. return;
  38. }
  39. // Try to load a custom strategy
  40. try {
  41. Class<?> clazz = Class.forName(strategyName);
  42. Constructor<?> customStrategy = clazz.getConstructor();
  43. strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
  44. }
  45. catch (Exception ex) {
  46. ReflectionUtils.handleReflectionException(ex);
  47. }
  48. }
  49. /**
  50. * Explicitly clears the context value from the current thread.
  51. */
  52. public static void clearContext() {
  53. strategy.clearContext();
  54. }
  55. /**
  56. * Obtain the current <code>SecurityContext</code>.
  57. * @return the security context (never <code>null</code>)
  58. */
  59. public static SecurityContext getContext() {
  60. return strategy.getContext();
  61. }
  62. /**
  63. * Primarily for troubleshooting purposes, this method shows how many times the class
  64. * has re-initialized its <code>SecurityContextHolderStrategy</code>.
  65. * @return the count (should be one unless you've called
  66. * {@link #setStrategyName(String)} or
  67. * {@link #setContextHolderStrategy(SecurityContextHolderStrategy)} to switch to an
  68. * alternate strategy).
  69. */
  70. public static int getInitializeCount() {
  71. return initializeCount;
  72. }
  73. /**
  74. * Associates a new <code>SecurityContext</code> with the current thread of execution.
  75. * @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
  76. */
  77. public static void setContext(SecurityContext context) {
  78. strategy.setContext(context);
  79. }
  80. /**
  81. * Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
  82. * a given JVM, as it will re-initialize the strategy and adversely affect any
  83. * existing threads using the old strategy.
  84. * @param strategyName the fully qualified class name of the strategy that should be
  85. * used.
  86. */
  87. public static void setStrategyName(String strategyName) {
  88. SecurityContextHolder.strategyName = strategyName;
  89. initialize();
  90. }
  91. /**
  92. * Use this {@link SecurityContextHolderStrategy}.
  93. *
  94. * Call either {@link #setStrategyName(String)} or this method, but not both.
  95. *
  96. * This method is not thread safe. Changing the strategy while requests are in-flight
  97. * may cause race conditions.
  98. *
  99. * {@link SecurityContextHolder} maintains a static reference to the provided
  100. * {@link SecurityContextHolderStrategy}. This means that the strategy and its members
  101. * will not be garbage collected until you remove your strategy.
  102. *
  103. * To ensure garbage collection, remember the original strategy like so:
  104. *
  105. * <pre>
  106. * SecurityContextHolderStrategy original = SecurityContextHolder.getContextHolderStrategy();
  107. * SecurityContextHolder.setContextHolderStrategy(myStrategy);
  108. * </pre>
  109. *
  110. * And then when you are ready for {@code myStrategy} to be garbage collected you can
  111. * do:
  112. *
  113. * <pre>
  114. * SecurityContextHolder.setContextHolderStrategy(original);
  115. * </pre>
  116. * @param strategy the {@link SecurityContextHolderStrategy} to use
  117. * @since 5.6
  118. */
  119. public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
  120. Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
  121. SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
  122. SecurityContextHolder.strategy = strategy;
  123. initialize();
  124. }
  125. /**
  126. * Allows retrieval of the context strategy. See SEC-1188.
  127. * @return the configured strategy for storing the security context.
  128. */
  129. public static SecurityContextHolderStrategy getContextHolderStrategy() {
  130. return strategy;
  131. }
  132. /**
  133. * Delegates the creation of a new, empty context to the configured strategy.
  134. */
  135. public static SecurityContext createEmptyContext() {
  136. return strategy.createEmptyContext();
  137. }
  138. }
  1. MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,⼤家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实⾮常适合 web 应⽤,因为在默认情况下,⼀个请求⽆论经过多少 Filter 到达 Servlet,都是由⼀个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了⼦线程,在⼦线程中去获取登录⽤户数据,就会获取不到。
  2. MODE INHERITABLETHREADLOCAL:这种存储模式适⽤于多线程环境,如果希望在⼦线程中也能够获取到登录⽤户数据,那么可以使⽤这种存储模式。
  3. MODE GLOBAL:这种存储模式实际上是将数据保存在⼀个静态变量中,在 JavaWeb 开发中,这种模式很少使⽤到。

SecurityContextHolderStrategy

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

  1. public interface SecurityContextHolderStrategy {
  2. /**
  3. * Clears the current context.
  4. */
  5. void clearContext();
  6. /**
  7. * Obtains the current context.
  8. */
  9. SecurityContext getContext();
  10. /**
  11. * Sets the current context.
  12. */
  13. void setContext(SecurityContext context);
  14. /**
  15. * Creates a new, empty context implementation, for use by
  16. */
  17. SecurityContext createEmptyContext();
  18. }

6627b535b4be4a5084e18e5e523b62ed.png

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

获取用户数据

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

73a83caf959f42b491e55a9503bc25cf.png

多线程下获取用户数据

  1. @GetMapping("/hello")
  2. public String hello() {
  3. new Thread(() -> {
  4. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  5. User user = (User) authentication.getPrincipal();
  6. System.out.println(user.toString());
  7. }).start();
  8. return "hello page success";
  9. }

2f718d88d4354f43873e20eb20eeb248.png

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

  1. -Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

7e05fb67aa154aa9ada16d3954f39bc9.png

⾃定义认证数据源

Servlet Authentication Architecture :: Spring Security

3e70fc85ca21453a9ee58d55bc6a7215.png

  • 发起认证请求,请求中携带⽤户名、密码,该请求会被 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

011d0088b68a48b19ae6f179ed426a7c.png

在 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 完成数据源的校验。 他们之间调⽤关系如下:

5793c7be00e949ff8460dcebddf6897b.png

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

配置全局 AuthenticationManager

Getting Started | Spring Security Architecture

默认的全局 AuthenticationManager

  1. @Configuration
  2. public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  3. @Autowired
  4. public void initialize(AuthenticationManagerBuilder builder) {
  5. //builder..
  6. }
  7. }

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

总结

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

自定义全局 AuthenticationManager

  1. @Configuration
  2. public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  3. @Override
  4. public void configure(AuthenticationManagerBuilder builder) {
  5. //builder ....
  6. }
  7. }

总结

  • 一旦通过 configure 方法自定义 AuthenticationManager实现 就回将工厂中自动配置AuthenticationManager 进行覆盖
  • 一旦通过 configure 方法自定义 AuthenticationManager实现 需要在实现中指定认证数据源对象 UserDetaiService 实例
  • 一旦通过 configure 方法自定义 AuthenticationManager实现 这种方式创建AuthenticationManager对象工厂内部本地一个 AuthenticationManager 对象 不允许在其他自定义组件中进行注入

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    1. //1.自定义AuthenticationManager 推荐 并没有在工厂中暴露出来
    2. @Override
    3. public void configure(AuthenticationManagerBuilder builder) throws Exception {
    4. System.out.println("自定义AuthenticationManager: " + builder);
    5. builder.userDetailsService(userDetailsService());
    6. }

    1. //作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
    2. @Override
    3. @Bean
    4. public AuthenticationManager authenticationManagerBean() throws Exception {
    5. return super.authenticationManagerBean();
    6. }

    }

自定义内存数据源

  1. @Configuration
  2. public class WebSecurityConfig {
  3. @Bean
  4. public UserDetailsService userDetailsService(){
  5. UserDetails user = User.withUsername("admin").password("{noop}123").roles("ADMIN").build();
  6. return new InMemoryUserDetailsManager(user);
  7. }
  8. @Bean
  9. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  10. http.authorizeHttpRequests()
  11. .mvcMatchers("/index")
  12. .permitAll()
  13. .anyRequest().authenticated()
  14. .and().formLogin()
  15. .successHandler(new LoginSuccessHandler())
  16. .failureHandler(new LoginFailureHandler())
  17. .and().logout().logoutSuccessHandler(new LogoutHandler())
  18. .and().userDetailsService(userDetailsService());
  19. return http.csrf().disable().build();
  20. }
  21. }

自定义数据库数据源

  1. -- 用户表
  2. CREATE TABLE `user`
  3. (
  4. `id` int(11) NOT NULL AUTO_INCREMENT,
  5. `username` varchar(32) DEFAULT NULL,
  6. `password` varchar(255) DEFAULT NULL,
  7. `enabled` tinyint(1) DEFAULT NULL,
  8. `accountNonExpired` tinyint(1) DEFAULT NULL,
  9. `accountNonLocked` tinyint(1) DEFAULT NULL,
  10. `credentialsNonExpired` tinyint(1) DEFAULT NULL,
  11. PRIMARY KEY (`id`)
  12. ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
  13. -- 角色表
  14. CREATE TABLE `role`
  15. (
  16. `id` int(11) NOT NULL AUTO_INCREMENT,
  17. `name` varchar(32) DEFAULT NULL,
  18. `name_zh` varchar(32) DEFAULT NULL,
  19. PRIMARY KEY (`id`)
  20. ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
  21. -- 用户角色关系表
  22. CREATE TABLE `user_role`
  23. (
  24. `id` int(11) NOT NULL AUTO_INCREMENT,
  25. `uid` int(11) DEFAULT NULL,
  26. `rid` int(11) DEFAULT NULL,
  27. PRIMARY KEY (`id`),
  28. KEY `uid` (`uid`),
  29. KEY `rid` (`rid`)
  30. ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
  31. -- 插入用户数据
  32. BEGIN;
  33. INSERT INTO `user`
  34. VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);
  35. INSERT INTO `user`
  36. VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);
  37. INSERT INTO `user`
  38. VALUES (3, 'cheny', '{noop}123', 1, 1, 1, 1);
  39. COMMIT;
  40. -- 插入角色数据
  41. BEGIN;
  42. INSERT INTO `role`
  43. VALUES (1, 'ROLE_product', '商品管理员');
  44. INSERT INTO `role`
  45. VALUES (2, 'ROLE_admin', '系统管理员');
  46. INSERT INTO `role`
  47. VALUES (3, 'ROLE_user', '用户管理员');
  48. COMMIT;
  49. -- 插入用户角色数据
  50. BEGIN;
  51. INSERT INTO `user_role`
  52. VALUES (1, 1, 1);
  53. INSERT INTO `user_role`
  54. VALUES (2, 1, 2);
  55. INSERT INTO `user_role`
  56. VALUES (3, 2, 2);
  57. INSERT INTO `user_role`
  58. VALUES (4, 3, 3);
  59. COMMIT;

项目中引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-security</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.mybatis.spring.boot</groupId>
  11. <artifactId>mybatis-spring-boot-starter</artifactId>
  12. <version>2.2.0</version>
  13. </dependency>
  14. <dependency>
  15. <groupId>mysql</groupId>
  16. <artifactId>mysql-connector-java</artifactId>
  17. <version>8.0.29</version>
  18. </dependency>
  19. <dependency>
  20. <groupId>com.alibaba</groupId>
  21. <artifactId>druid</artifactId>
  22. <version>1.2.7</version>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.projectlombok</groupId>
  26. <artifactId>lombok</artifactId>
  27. </dependency>

配置 springboot 配置文件

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

创建 entity

  1. @Data
  2. public class User implements UserDetails {
  3. private Integer id;
  4. private String username;
  5. private String password;
  6. private Boolean enabled;
  7. private Boolean accountNonExpired;
  8. private Boolean accountNonLocked;
  9. private Boolean credentialsNonExpired;
  10. private List<Role> roles = new ArrayList<>();
  11. @Override
  12. public Collection<? extends GrantedAuthority> getAuthorities() {
  13. List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
  14. roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
  15. return grantedAuthorities;
  16. }
  17. @Override
  18. public String getPassword() {
  19. return password;
  20. }
  21. @Override
  22. public String getUsername() {
  23. return username;
  24. }
  25. @Override
  26. public boolean isAccountNonExpired() {
  27. return accountNonExpired;
  28. }
  29. @Override
  30. public boolean isAccountNonLocked() {
  31. return accountNonLocked;
  32. }
  33. @Override
  34. public boolean isCredentialsNonExpired() {
  35. return credentialsNonExpired;
  36. }
  37. @Override
  38. public boolean isEnabled() {
  39. return enabled;
  40. }
  41. }
  42. @Data
  43. public class Role {
  44. private Integer id;
  45. private String name;
  46. private String nameZh;
  47. }

创建 UserMapper 接口,编写sql语句

  1. @Mapper
  2. public interface UserMapper {
  3. //根据用户名查询用户
  4. User loadUserByUsername(String username);
  5. //根据用户id查询角色
  6. List<Role> getRolesByUid(Integer uid);
  7. }
  8. <?xml version="1.0" encoding="UTF-8"?>
  9. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  10. <mapper namespace="com.yang.mapper.UserMapper">
  11. <!--查询单个-->
  12. <select id="loadUserByUsername" resultType="com.yang.entity.User">
  13. select id,
  14. username,
  15. password,
  16. enabled,
  17. accountNonExpired,
  18. accountNonLocked,
  19. credentialsNonExpired
  20. from user
  21. where username = #{username}
  22. </select>
  23. <!--查询指定行数据-->
  24. <select id="getRolesByUid" resultType="com.yang.entity.Role">
  25. select r.id,
  26. r.name,
  27. r.name_zh nameZh
  28. from role r,
  29. user_role ur
  30. where r.id = ur.rid
  31. and ur.uid = #{uid}
  32. </select>
  33. </mapper>

创建 service

  1. public interface UserService {
  2. UserDetails loadUserByUsername(String username);
  3. }
  4. @Service
  5. public class UserServiceImpl implements UserService {
  6. private final UserMapper userMapper;
  7. @Autowired
  8. public UserServiceImpl(UserMapper userMapper) {
  9. this.userMapper = userMapper;
  10. }
  11. @Override
  12. public UserDetails loadUserByUsername(String username) {
  13. User user = userMapper.loadUserByUsername(username);
  14. if(ObjectUtils.isEmpty(user)){
  15. throw new RuntimeException("用户不存在");
  16. }
  17. user.setRoles(userMapper.getRolesByUid(user.getId()));
  18. return user;
  19. }
  20. }

创建 UserDetailsService

  1. @Component
  2. public class UserDetailService implements UserDetailsService {
  3. private final UserService userService;
  4. public UserDetailService(UserService userService) {
  5. this.userService = userService;
  6. }
  7. @Override
  8. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  9. return userService.loadUserByUsername(username);
  10. }
  11. }

配置 authenticationManager 使用自定义UserDetailService

  1. @Configuration
  2. public class SecurityWebConfig {
  3. private final UserDetailService userDetailService;
  4. public SecurityWebConfig(UserDetailService userDetailService) {
  5. this.userDetailService = userDetailService;
  6. }
  7. @Bean
  8. public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
  9. return authenticationConfiguration.getAuthenticationManager();
  10. }
  11. @Bean
  12. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  13. http.authorizeHttpRequests()
  14. .mvcMatchers("/index")
  15. .permitAll()
  16. .anyRequest().authenticated()
  17. .and().formLogin()
  18. .successHandler(new LoginSuccessHandler())
  19. .failureHandler(new LoginFailureHandler())
  20. .and().logout().logoutSuccessHandler(new LogoutHandler()) // 注销登入处理器
  21. .and().userDetailsService(userDetailService); // 自定义数据源
  22. return http.csrf().disable().build();
  23. }
  24. }

添加验证码

  1. <dependency>
  2. <groupId>com.github.penggle</groupId>
  3. <artifactId>kaptcha</artifactId>
  4. <version>2.3.2</version>
  5. </dependency>

生成验证码

  1. @Configuration
  2. public class KaptchaConfig {
  3. @Bean
  4. public Producer kaptcha() {
  5. Properties properties = new Properties();
  6. properties.setProperty("kaptcha.image.width", "150");
  7. properties.setProperty("kaptcha.image.height", "50");
  8. properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
  9. properties.setProperty("kaptcha.textproducer.char.length", "4");
  10. Config config = new Config(properties);
  11. DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
  12. defaultKaptcha.setConfig(config);
  13. return defaultKaptcha;
  14. }
  15. }
  16. @RestController
  17. public class KaptchaController {
  18. private final Producer producer;
  19. public KaptchaController(Producer producer) {
  20. this.producer = producer;
  21. }
  22. @GetMapping("/vc.png")
  23. public String getVerifyCode(HttpSession session) throws IOException {
  24. //1.生成验证码
  25. String code = producer.createText();
  26. session.setAttribute("kaptcha", code);//可以更换成 redis 实现
  27. BufferedImage bi = producer.createImage(code);
  28. //2.写入内存
  29. FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
  30. ImageIO.write(bi, "png", fos);
  31. //3.生成 base64
  32. return Base64.encodeBase64String(fos.toByteArray());
  33. }
  34. }

定义验证码异常类

  1. public class KaptchaNotMatchException extends AuthenticationException {
  2. public KaptchaNotMatchException(String msg) {
  3. super(msg);
  4. }
  5. public KaptchaNotMatchException(String msg, Throwable cause) {
  6. super(msg, cause);
  7. }
  8. }

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

  1. /**
  2. * @Author: chenyang
  3. * @DateTime: 2023/2/27 10:14
  4. * @Description: 自定义过滤器
  5. */
  6. public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
  7. public static final String FORM_CAPTCHA_KEY = "captcha";
  8. private String kaptchaParameter = FORM_CAPTCHA_KEY;
  9. public String getKaptchaParameter() {
  10. return kaptchaParameter;
  11. }
  12. public void setKaptchaParameter(String kaptchaParameter) {
  13. this.kaptchaParameter = kaptchaParameter;
  14. }
  15. @Override
  16. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  17. if (!request.getMethod().equals("POST")) {
  18. throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
  19. }
  20. try {
  21. //1.获取请求数据
  22. Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
  23. String kaptcha = userInfo.get(getKaptchaParameter());//用来获取数据中验证码
  24. String username = userInfo.get(getUsernameParameter());//用来接收用户名
  25. String password = userInfo.get(getPasswordParameter());//用来接收密码
  26. //2.获取 session 中验证码
  27. String sessionVerifyCode = (String) request.getSession().getAttribute(FORM_CAPTCHA_KEY);
  28. if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionVerifyCode) &&
  29. kaptcha.equalsIgnoreCase(sessionVerifyCode)) {
  30. //3.获取用户名 和密码认证
  31. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
  32. setDetails(request, authRequest);
  33. return this.getAuthenticationManager().authenticate(authRequest);
  34. }
  35. } catch (IOException e) {
  36. e.printStackTrace();
  37. }
  38. throw new KaptchaNotMatchException("验证码不匹配!");
  39. }
  40. }

配置

  1. @Configuration
  2. public class WebSecurityConfig {
  3. private final UserDetailService userDetailService;
  4. public WebSecurityConfig(UserDetailService userDetailService) {
  5. this.userDetailService = userDetailService;
  6. }
  7. @Bean
  8. public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
  9. return authenticationConfiguration.getAuthenticationManager();
  10. }
  11. @Bean
  12. public LoginKaptchaFilter loginKaptchaFilter(AuthenticationManager authenticationManager) {
  13. LoginKaptchaFilter filter = new LoginKaptchaFilter();
  14. //1.认证 url
  15. filter.setFilterProcessesUrl("/doLogin");
  16. //2.认证 接收参数
  17. filter.setUsernameParameter("username");
  18. filter.setPasswordParameter("pwd");
  19. filter.setKaptchaParameter("kaptcha");
  20. //3.指定认证管理器
  21. filter.setAuthenticationManager(authenticationManager);
  22. // 4.指定成功/失败时处理
  23. filter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
  24. filter.setAuthenticationFailureHandler(new LoginFailureHandler());
  25. return filter;
  26. }
  27. @Bean
  28. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  29. http.authorizeHttpRequests()
  30. .mvcMatchers("/index", "/vc.png")
  31. .permitAll()
  32. .anyRequest().authenticated()
  33. .and().formLogin()
  34. .and().logout().logoutSuccessHandler(new LogoutHandler()) // 注销登入处理器
  35. .and().exceptionHandling().authenticationEntryPoint(new UnAuthenticationHandler()) // 未认证处理器
  36. .and().userDetailsService(userDetailService) // 自定义数据源
  37. .addFilterBefore(loginKaptchaFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class); // 自定义过滤器
  38. return http.csrf().disable().build();
  39. }
  40. }

自定义认证异常处理类

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

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

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

测试验证

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

d4d3942d0e0241199e0a9e23ae38479b.png

登入

a58cdf9d71b2428a871a9920dc555ed7.png

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

ada46b0ab31a461dbacb05868bbfc7f4.png

b190252a645c404dbd6fa328c96a9ff8.png

发表评论

表情:
评论列表 (有 0 条评论,230人围观)

还没有评论,来说两句吧...

相关阅读