在spring项目中,我们做认证功能的时候 ,基本都是使用spring security 或者 shiro。由于考虑到与spring的集成,我们更多的会去选择spring security。这个比shiro要费事点,当时第一次用的时候,也是好一顿折腾,这里就把spring security 怎么工作的做一个总结。
版本:spring boot 2.4.7
spring 的http 服务分为spring mvc 和spring webflux,这我们是用的spring mvc
spring security 的本质是一组的过滤器,但是并不是基于servlet的过滤器,而是过滤器的表现形式。我们根据官方文档可以知道,使用spring security 需要加@EnableWebSecurity 注解(实际上,不加也可以,引入了spring-boot-starter-security包就可以了)。既然你有配置项,那就好办了,我们就可以找到他的配置入口(所有的应用,必定是有一个主线程的,启动的时候会把相关的配置项加载好,或者是bean初始化好,schedule job除外,但是schedule 这种基本就是我们自己定义的了)。

org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration#springSecurityFilterChain
这里断点可以看到,securityFilterChains就是我们的过滤器的调用链,这里有配置的匹配方式,还有过滤器,从截图可以看出,这么有13个过滤器。
把securityFilterChain给到WebSecurity
最后的 this.webSecurity.build() 会去调用
org.springframework.security.config.annotation.web.builders.WebSecurity#performBuild
也就是说实际的过滤器是这个FilterChainProxy,而上面显示的那13个过滤器,都是由这个过滤器去调用的。
org.springframework.security.web.FilterChainProxy#doFilterInternal
上面我们已经知道了spring security是怎么去调用的,那么具体的认证是怎么个步骤呢?
首先,我们要知道的是spring mvc的secrity是基于threadlocal来实现功能的。拿用户名密码登录来说,请求过来之后,先进行一系列的初始化,比如SecurityContextHolder,然后,进行一系列的逻辑判断。基本流程如下:
现在的项目中,我们更多是使用token去做登录认证,这里我们以jwt为例
自定义一个UserDetails
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class JWTUserDetails {
private Long operateId;
private String token;
private String role;
}
写一个Authentication
public class JWTAuthenticationToken extends AbstractAuthenticationToken {
private Object principal;
private Object credentials;
public JWTAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
public JWTAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
super(authorities);
}
public JWTAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
}
自定义一个过滤器
@Component
@RequiredArgsConstructor
public class JWTAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Claims claims = ...; //解析token
// if(Objects.nonNull(claims)){
// JWTUserDetails userDetails = JWTUserDetails.builder().token("").operateId(1L).build();
// Authentication authentication = new JWTAuthenticationToken(userDetails.getOperateId(),userDetails);
// SecurityContextHolder.getContext().setAuthentication(authentication);
// }
JWTUserDetails userDetails = JWTUserDetails.builder().token("").operateId(1L).build();
Authentication authentication = new JWTAuthenticationToken(userDetails.getOperateId(),userDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request,response);
}
}
配置
@RequiredArgsConstructor
@EnableWebSecurity(debug = true)
@Configuration
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/webjars/**");
}
@Bean
SecurityFilterChain securityWebFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
;
return http.build();
}
}
另外,很多地方讲到用AuthenticationProvider来做认证校验,这个看自己的需求吧,如果要用的话,会发现想要向BasicAuthenticationFilter中一样调用AuthenticationManager来校验的话是拿不到的
Authentication authResult = this.authenticationManager.authenticate(authRequest);
这里的解决办法是需要创建一个Configurer,会由httpSecurity调用configure方法去创建filter,这样的话就可以获得AuthenticationManager了。