源码解读工作中最常用到的Spring Security安全框架
本文介绍了使用Spring Security和JWT实现登录验证的基本流程。主要包括:1)引入Spring Security依赖;2)配置SecurityConfig类定义安全规则;3)创建用户实体实现UserDetails接口;4)自定义UserDetailsService加载用户信息;5)编写JWT过滤器进行令牌验证。关键点包括配置无状态会话管理、CORS设置、密码加密以及将JWT过滤器添加到
在讲解源码之前,我们先通过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);
}
源码解读:
通过上面的逻辑大概梳理了以下执行流程:
- 程序启动首先会加载SecurityConfig中的各种bean(比如注册AuthenticationManager)
- jwt过滤器会拦截用户请求(登录肯定是没有认证信息的,这块为啥要拦截呢?后面具体说)
- 创建UsernamePasswordAuthenticationToken存放认证信息,一开始只有账密,状态为未认证
- AuthenticationManager认证管理器执行未认证的token
- 执行AuthenticationManager认证管理器的ProviderManager逻辑
- 执行ProviderManager提供者管理器的authenticate方法
- 通过集合中提供者provider.supports()方法匹配对应的provider提供者
- 在提供者的实现中执行自定义的UserDetailsService的loadUserByUsername方法和自定逻辑
- AbstractUserDetailsAuthenticationProvider创建认证成功信息返回
- 设置安全上下文,登录成功
接下来,通过以上梳理的步骤来看具体源码:
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
原来时间真的不是一条横跨在你面前的河,有着此岸和彼岸,而是一条挂在悬崖上的瀑布,奔流直下,一去无回。
更多推荐


所有评论(0)