在讲解源码之前,我们先通过SpringSecurity+JWT来实现一段简单的登录逻辑

1 引入依赖

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

2 配置SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    
    private final CustomUserDetailsService userDetailsService;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(authz -> authz
                    .requestMatchers("/auth/login").permitAll()
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/actuator/**").permitAll()
                .anyRequest().authenticated()
            )
          			// 请求首先会被jwt过滤器处理 如果JWT验证成功,会设置认证上下文,后续过滤器会跳过
                // 如果JWT验证失败,请求会继续传递到后续过滤器,但是由于放行了/auth/** auth的请求也不会拦截
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
} 

3 创建SysUser实体实现UserDetails

@Data
@TableName("sys_users")
public class SysUser implements UserDetails {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String password;
} 

4 自定义UserDetailsService实现类

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username);
        SysUser sysUser = sysUserMapper.selectOne(queryWrapper);

        if (sysUser == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        if (sysUser.getStatus() == 0) {
            throw new RuntimeException("用户已禁用");
        }

        return sysUser;
    }
} 

4 JWT拦截器

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        
        try {
            String jwt = getJwtFromRequest(request);
            
            if (StringUtils.hasText(jwt) && jwtUtil.validateToken(jwt)) {
                String username = jwtUtil.getUsernameFromToken(jwt);
                
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
} 

5 实现登录逻辑

controller调用service的实现方法

public LoginResponse login(LoginRequest loginRequest) {
        // 验证用户名和密码
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
        );
        // 设置安全上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        // 生成JWT令牌
        String token = jwtUtil.generateToken(loginRequest.getUsername(), AuthType.PASSWORD);
        
        // 获取用户信息
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getUsername, loginRequest.getUsername());
        SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
        
        if (sysUser == null) {
            throw new RuntimeException("用户不存在");
        }
        
        return new LoginResponse(token, sysUser);
    }

源码解读:

通过上面的逻辑大概梳理了以下执行流程:

  1. 程序启动首先会加载SecurityConfig中的各种bean(比如注册AuthenticationManager)
  2. jwt过滤器会拦截用户请求(登录肯定是没有认证信息的,这块为啥要拦截呢?后面具体说)
  3. 创建UsernamePasswordAuthenticationToken存放认证信息,一开始只有账密,状态为未认证
  4. AuthenticationManager认证管理器执行未认证的token
  5. 执行AuthenticationManager认证管理器的ProviderManager逻辑
  6. 执行ProviderManager提供者管理器的authenticate方法
  7. 通过集合中提供者provider.supports()方法匹配对应的provider提供者
  8. 在提供者的实现中执行自定义的UserDetailsService的loadUserByUsername方法和自定逻辑
  9. AbstractUserDetailsAuthenticationProvider创建认证成功信息返回
  10. 设置安全上下文,登录成功

接下来,通过以上梳理的步骤来看具体源码:

1.程序启动首先会加载SecurityConfig中的各种bean(比如注册AuthenticationManager)

看下面截图中,程序启动会依次注册authenticationManager、passwordEncoder、filterChain;

其中passwordEncoder是密码加密验证用到的,authenticationManager是认证管理器主要用于执行认证逻辑;

然后filterChain是过滤器链,由于我们采用jwt的token存储方式,所以session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)这一步用来设置session为无状态,然后.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);这一步注册我们的过滤器,由于我们采用jwt的方式所以将jwt过滤器注册在UsernamePassword的前面;

请求首先会被jwt过滤器处理 如果JWT验证成功,会设置认证上下文,后续过滤器会跳过,如果JWT验证失败,请求会继续传递到后续过滤器,但是由于放行了/auth/** auth的请求也不会拦截,会拦截其他未放行的请求。
在这里插入图片描述

2.jwt过滤器会拦截用户请求

在jwt过滤器中如果token验证成功后会把认证信息放到Security的上下文中,然后执行下面的过滤器链,因为前面我们也注册了UsernamePasswordAuthenticationFilter过滤器,所以执行完jwt过滤器后请求会到达UsernamePasswordAuthenticationFilter,如果jwt效验成功就会设置认证信息到上下文,这种情况UsernamePasswordAuthenticationFilter就会直接放行了;如果没有设置认证信息到上下文中并且接口路径也没有放行那UsernamePasswordAuthenticationFilter就会拦截。
在这里插入图片描述
那具体UsernamePasswordAuthenticationFilter是怎么拦截的呢,来看下

attemptAuthentication方法会从请求中获取username和password,显然除了登录接口是没有密码的,所以后续的认证逻辑中自然就不会成功,具体的认证逻辑我们后面会讲到。
在这里插入图片描述

3.创建UsernamePasswordAuthenticationToken存放认证信息,一开始只有账密,状态为未认证

在我们的登录逻辑中会创建UsernamePasswordAuthenticationToken类,其实这里的作用就是把账密存到这个实例里面,后面认证的时候从这个UsernamePasswordAuthenticationToken中获取。
在这里插入图片描述

注意这里的构造方法是一个没有认证信息的方法,认证成功后才会创建有认证信息的UsernamePasswordAuthenticationToken
在这里插入图片描述

4.AuthenticationManager认证管理器执行未认证的token

authenticationManager就是一开始我们在配置文件中注入的bean
在这里插入图片描述

5.执行AuthenticationManager认证管理器的ProviderManager逻辑

跟着authenticationManager.authenticate()这个方法一直往下走,首先会执行ProviderManager的实现
在这里插入图片描述

6.执行ProviderManager提供者管理器的authenticate方法

authentication参数就是登录逻辑中我们创建的UsernamePasswordAuthenticationToken类
在这里插入图片描述

7.通过集合中提供者provider.supports()方法匹配对应的provider提供者

这里this.getProviders()集合中拿到的provider类型是DaoAuthenticationProvider,解释一下,DaoAuthenticationProvider是注入的默认提供者(如果还创建了其他自定义provider,那这里集合就有多个对象),他的父类是AbstractUserDetailsAuthenticationProvider,由于DaoAuthenticationProvider中没有supports和authenticate方法,所以会执行父类AbstractUserDetailsAuthenticationProvider的这两个方法
在这里插入图片描述在这里插入图片描述

可以看到父类中的supports方法中定义了UsernamePasswordAuthenticationToken类型,所以就会跟我们一开始登录逻辑中authenticationManager.authenticate()方法中传进来的UsernamePasswordAuthenticationToken相匹配
在这里插入图片描述

8.在提供者的实现中执行自定义的UserDetailsService的loadUserByUsername方法和自定逻辑

在supports方法命中后就会执行对应的provider的实际认证方法
在这里插入图片描述

那程序走到这里其实就会执行AbstractUserDetailsAuthenticationProvider这个提供者
在这里插入图片描述

接着往后看,然后会执行用户名缓存等查询就不说了,重点是执行this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);方法
在这里插入图片描述

再接着走,会执行子类DaoAuthenticationProvider中的retrieveUser方法,主要看this.getUserDetailsService().loadUserByUsername(username);这一段
在这里插入图片描述

this.getUserDetailsService()方法会返回我们自定义的实现类,这里匹配到它也是因为在创建security配置的时候把这个类注入进去了
在这里插入图片描述

点进这个方法去,其实就会执行到我们最一开始自定义的CustomUserDetailsService实现类
在这里插入图片描述

然后我们会在这个自定义实现类查询用户并返回用户信息
在这里插入图片描述

9.AbstractUserDetailsAuthenticationProvider创建认证成功信息返回
在这里插入图片描述

可以看到createSuccessAuthentication方法中创建了一个携带认证信息的UsernamePasswordAuthenticationToken(我们一开始执行登录的时候创建的是未携带认证的UsernamePasswordAuthenticationToken)
在这里插入图片描述

10.设置安全上下文,登录成功
在这里插入图片描述

到这里整个流程就走完了,重点其实就是7、8这两步,如果觉得有用的话欢迎点赞分享。

One more thing
原来时间真的不是一条横跨在你面前的河,有着此岸和彼岸,而是一条挂在悬崖上的瀑布,奔流直下,一去无回。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐