什么是SpringSecurity
Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。
Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。而我们最常用的两个功能就是认证和鉴权,因此作为入门文章本文也只介绍这两个功能的使用。
❝
Spring Security可以用于Servlet应用和Reactive应用,本文主要介绍基于Servlet应用的场景
❞
如需更详细的使用方式请参考官方文档:https://spring.io/projects/spring-security
架构
上图是Spring Security官方提供的架构图。我们先看图的左边部分,就是一个典型Servlet
Filter
(过滤器)处理流程,我们依次讲解流程涉及的组件。
FilterChain
FilterChain
:过滤器链,是Servlet
容器在接收到客户端发送的请求时创建的,一个FilterChain
可以包含多个Filter
和一个Servlet
,Servlet
容器根据请求URI的路径来处理HttpServletRequest
。
在Spring MVC中,Servlet
就是DispatcherServlet
实例。一个Servlet
最多只能处理一个HttpServletRequest
和HttpServletResponse
。然而,可以使用多个Filter
来完成如下工作。
-
防止下游的 Filter
或Servlet
被调用。在这种阻断请求的情况下,Filter
通常会使用HttpServletResponse
对客户端写入响应内容。 -
修改下游的 Filter
和Servlet
所使用的HttpServletRequest
或HttpServletResponse
。
DelegatingFilterProxy
DelegatingFilterProxy
:Spring Security 对 Servlet
的支持是基于Servlet
Filter
的,而DelegatingFilterProxy
就是Spring Security的Filter
实现。
DelegatingFilterProxy
允许在 Servlet
容器的生命周期和 Spring 的ApplicationContext
之间建立桥梁。Servlet
容器允许通过使用自己的标准来注册Filter
实例,但Servlet
容器不知道 Spring 定义的 Bean。因此大多数情况下我们通过标准的Servlet
容器机制来注册DelegatingFilterProxy
,但将所有工作委托给实现Filter
的Spring Bean。
❝
Spring Security会自动向
Servlet
容器机制注册DelegatingFilterProxy
,无需我们手动去注册❞
FilterChainProxy
FilterChainProxy
:是 Spring Security 提供的一个特殊的Filter
,允许通过SecurityFilterChain
委托给许多Filter
实例。由于FilterChainProxy
是一个Spring Bean,因此它被包含在DelegatingFilterProxy
中。
SecurityFilterChain
SecurityFilterChain
:是FilterChainProxy
用来确定当前请求应该调用哪些Spring SecurityFilter
实例的过滤器链。
SecurityFilterChain
中的Security
Filter
一般都是Spring Bean,但这些Security
Filter
是用FilterChainProxy
进行注册,而不是通过DelegatingFilterProxy
注册。与直接向Servlet容器或DelegatingFilterProxy
注册相比,FilterChainProxy
有很多优势。
-
首先,由于 FilterChainProxy
是 Spring Security 使用的核心,它可以处理一些必须要做的事情。 例如:-
清除 SecurityContext
以避免内存泄漏。 -
应用Spring Security的 HttpFirewall
来保护应用程序免受某些类型的攻击。
-
-
其次,它在确定何时应该调用 SecurityFilterChain
方面提供了更大的灵活性。在Servlet容器中,Filter
实例仅基于URL被调用。 然而,FilterChainProxy
可以通过使用RequestMatcher
接口,根据HttpServletRequest
中的任何内容确定调用。
图的右边部分是存在多个SecurityFilterChain
,FilterChainProxy
的匹配策略则是匹配第一个满足的SecurityFilterChain
。
比如,请求的URL是/api/messages/
,它首先与/api/**
的SecurityFilterChain 0
模式匹配,所以只有SecurityFilterChain0
被调用;虽然它也与SecurityFilterChain n
匹配。
如果请求的URL是/messages/
,它与/api/**
的SecurityFilterChain 0
模式不匹配,所以FilterChainProxy
继续尝试每个SecurityFilterChain
。如果没有其他SecurityFilterChain
实例相匹配,则调用SecurityFilterChain n
。
SecurityFilter
SecurityFilter
:是指通过SecurityFilterChain
插入FilterChainProxy
中的 Filter
。
这些 Filter
可以用于许多不同的目的,如认证、授权、漏洞保护等。Filter
是按照特定的顺序执行的,以保证它们在正确的时间被调用。
例如,执行认证的Filter
应该在执行授权的Filter
之前被调用。如果想要知道 Spring Security 的Filter
的顺序,可以查看FilterOrderRegistration
源码。
❝
如果想查看你应用中注册了哪些
SecurityFilter
的话可以将org.springframework.security的日志级别调到info,这样在你应用启动的时候就会在控制台打印出当前应用注册的所有SecurityFilter
。效果如下:❞
2023-06-14T08:55:22.321-03:00INFO76975---[main]o.s.s.web.DefaultSecurityFilterChain:Willsecureanyrequestwith[
org.springframework.security.web.session.Di服务器托管sableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
至此,Spring Security官方架构图中涉及的组件就基本介绍完了,大家先对整体架构和执行流程有一个了解,只有先了解了整体架构,才方便接下来我们去理解Spring Security是如何去实现认证和授权的。
常用Spring Security开启的SecurityFilter
-
CsrfFilter:防止Csrf攻击的 SecurityFilter
-
AuthorizationFilter:授权 SecurityFilter
-
ExceptionTranslationFilter:处理认证和授权异常的 SecurityFilter
异常处理
Spring Security中有一个ExceptionTranslationFilter
,ExceptionTranslationFilter
作为Security Filter之一被插入到FilterChainProxy中。
ExceptionTranslationFilter
可以处理AuthenticationException或AccessDeniedException,其逻辑大概是这样:
try{
filterChain.doFilter(request,response);
}catch(AccessDeniedException|AuthenticationExceptionex){
if(!authenticated||exinstanceofAuthenticationException){
startAuthentication();
}else{
accessDenied();
}
}
❝
这段代码的逻辑大致就是,拦截AccessDeniedException 或 AuthenticationException,如果不是这两个异常则不处理。
❞
ExceptionTranslationFilter
流程如下:
因此如果我们想自己处理AuthenticationException或者AccessDeniedException,分别实现AuthenticationEntryPoint
或者AccessDeniedHandler
即可
@Component
publicclassAccessDeniedHandlerImplimplementsAccessDeniedHandler{
@Override
publicvoidhandle(HttpServletRequestrequest,HttpServletResponseresponse,AccessDeniedExceptionaccessDeniedException)throwsIOException,ServletException{
log.error("AccessDeniedException请求URI:{}",request.getRequestURI(),accessDeniedException);
response.setCharacterEncoding("UTF-8");
HashMapresult=newHashMap();
result.put("code","401");
result.put("message","权限不足");
//处理没有权限的错误错误
response.getWriter().write(JsonUtil.toString(result));
}
}
@Component
publicclassAuthenticationEntryPointImplimplementsAuthenticationEntryPoint{
@Override
publicvoidcommence(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionauthException)throwsIOException,ServletException{
log.error("AccessDeniedException请求URI:{}",request.getRequestURI(),authException);
response.setCharacterEncoding("UTF-8");
//处理认证失败的错误
HashMapresult=newHashMap();
result.put("code","500");
result.put("message","用户名或密码错误");
//处理没有权限的错误错误
response.getWriter().write(JsonUtil.toString(result));
}
}
@Configuration
publicclassSecurityConfig{
@Bean
publicSecurityFilterChainapiFilterChain(HttpSecurityhttpSecurity,
AuthenticationEntryPointauthenticationEntryPoint,
AccessDeniedHandleraccessDeniedHandler)throwsException{
//配置异常处理
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
returnhttpSecurity.build();
}
}
上面是Spring Security对于认证和鉴权异常的处理机制,但是如果我们自定义了一个Filter。如果这个Filter抛出异常,Spring的全局异常处理机制是无法处理的(原因自行搜索)。所以我们还需要自己做一个Filter异常的处理流程。
首先,我们自定义一个Filter,要在FilterChain中的位置比较靠前,没有其他逻辑就是拦截后面filter抛出的异常,然后转发到指定Controller去处理,然后再用全局异常去处理Filter抛出的异常。
@Component
publicclassExceptionFilterextendsOncePerRequestFilter{
@Override
protectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{
try{
filterChain.doFilter(request,response);
}catch(Exceptione){
//将异常信息写入请求
request.setAttribute("filterException",e);
//重定向到处理异常的controller
request.getRequestDispatcher("/exception").forward(request,response);
}
}
}
@RestController
publicclassExceptionController{
@RequestMapping("/exception")
publicvoidhandleException(HttpServletRequestrequest)throwsException{
Objectattribute=request.getAttribute("filterException");
if(ObjectUtil.isNotEmpty(attribute)&&attributeinstanceofExceptione){
throwe;
}
}
}
@Configuration
publicclassSecurityConfig{
@Bean
publicSecurityFilterChainapiFilterChain(HttpSecurityhttpSecurity,
ExceptionFilterexceptionFilter)throwsException{
//配置异常处理过滤器
//这里我们配置在ExceptionTranslationFilter之前让ExceptionTranslationFilter先处理AuthenticationException或者AccessDeniedException
//剩下的其他Exception再交由我们自定义的ExceptionFilter处理
httpSecurity.addFilterBefore(exceptionFilter,ExceptionTranslationFilter.class);
returnhttpSecurity.build();
}
}
认证
上面我们已经把Spring Security整体流程讲完了,接下来我们就看一下具体认证的流程是怎样的。Spring Security有提供一套基于标准页面的流程,但是不适用于基于前后端分离的开发模式。地址给大家贴这,有需要的可自行去看看:认证 :: Spring Security Reference (springdoc.cn)
接下来介绍基于前后端分离的流程:
-
先配置AuthenticationManager(认证管理器),其中最常用的AuthenticationManager实现是ProviderManager
@Configuration
publicclassSecurityConfig{
/**
*配置AuthenticationManager(认证管理器)
*@return
*/
@Bean
publicAuthenticationManagerauthenticationManager(AuthenticationProviderauthenticationProvider){
//ProviderManager是AuthenticationManager最常用的实现
returnnewProviderManager(authenticationProvider);
}
}
-
配置AuthenticationProvider,这里我们的认证方案是使用数据库存储的密码和登录请求的密码进行匹配验证,因此我们选择 DaoAuthenticationProvider
。DaoAuthenticationProvider
是一个AuthenticationProvider
的实现,它使用UserDetailsService
和PasswordEncoder
来验证一个用户名和密码。
@Configuration
publicclassSecurityConfig{
/**
*配置PasswordEncoder(密码编码器)
*@return
*/
@Bean
publicPasswordEncoderpasswordEncoder(){
returnnewBCryptPasswordEncoder();
}
/**
*配置AuthenticationProvider
*@return
*/
@Bean
publicAuthenticationProviderauthenticationProvider(UserDetailsServiceuserDetailsService,
PasswordEncoderpasswordEncoder){
DaoAuthenticationProviderdaoAuthenticationProvider=newDaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
returndaoAuthenticationProvider;
}
}
-
配置 UserDetails
和UserDetailsService
importlombok.EqualsAndHashCode;
importlombok.Getter;
importlombok.Setter;
importlombok.ToString;
importorg.springframework.security.core.GrantedAuthority;
importorg.springframework.security.core.userdetails.UserDetails;
importjava.util.Collection;
importjava.util.List;
@Setter
@EqualsAndHashCode
@ToString
publicclassSecurityUserDetailimplementsUserDetails{
@Getter
privateLonguserId;
privateStringuserName;
privateStringpassword;
privateListauthorities;
@Override
publicCollectionextendsGrantedAuthority>getAuthorities(){
returnthis.authorities;
}
@Override
publicStringgetPassword(){
returnthis.password;
}
@Override
publicStringgetUsername(){
returnthis.userName;
}
@Override
publicbooleanisAccountNonExpired(){
returntrue;
}
@Override
publicbooleanisAccountNonLocked(){
returntrue;
}
@Override
publicbooleanisCredentialsNonExpired(){
returntrue;
}
@Override
publicbooleanisEnabled(){
returntrue;
}
}
importlombok.RequiredArgsConstructor;
importorg.springframework.security.core.userdetails.UserDetails;
importorg.springframework.security.core.userdetails.UserDetailsService;
importorg.springframework.security.core.userdetails.UsernameNotFoundException;
importorg.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
publicclassAuthUserDetailsServiceimplementsUserDetailsService{
privatefinalUserServiceuserService;
@Override
publicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{
Useruser=userService.getValidUser(username);
Assert.notEmpty(user,"用户名或密码错误");
SecurityUserDetailuserDetail=newSecurityUserDetail();
userDetail.setUserId(user.getId());
userDetail.setUserName(user.getUserName());
userDetail.setPassword(user.getPassword());
returnuserDetail;
}
}
-
LoginController提供登录接口,伪代码如下:
@Slf4j
@RestController
@RequiredArgsConstructor
publicclassLoginController{
privatefinalAuthenticationManagerauthenticationManager;
publicResponselogin(LoginAOloginAo){
UsernamePasswordAuthenticationTokenauthenticationToken=
newUsernamePasswordAuthenticationToken(loginAo.getKey(),loginAo.getPassword());
Authenticationauthenticate=authenticationMana服务器托管ger.authenticate(authenticationToken);
Assert.notEmpty(authenticate,"用户名或密码错误");
if(authenticate.getPrincipal()instanceofSecurityUserDetailuserDetail){
Useruser=User.builder().id(userDetail.getUserId()).userName(userDetail.getUsername()).build();
Stringtoken=JwtUtil.generateToken(user);
//设置上下文
UsernamePasswordAuthenticationTokenauthentication=newUsernamePasswordAuthenticationToken(user,null,null);
//设置子线程支持从父线程获取用户上下文注意使用ForkJoinPool无法生效
//SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
SecurityContextHolder.getContext().setAuthentication(authentication);
returnResponse.success(LoginVO.builder().token(token).build());
}else{
log.error("登录异常,从上下文获取用户信息失败,authenticate:{}",JsonUtil.toString(authenticate));
returnResponse.fail(null);
}
}
}
授权
本文这里分享两种主流的Spring Security授权方式,一种是基于注解的方式,一种是基于配置的方式。
基于注解的授权校验
基于注解的方式校验授权,是通过Spring aop实现的,其流程如下:
首先,我们要开启注解鉴权的功能
@Configuration
/**
*开启基于注解的方式控制权限
*/
@EnableMethodSecurity
publicclassSecurityConfig{
}
然后在需要鉴权的方法上添加权限注解
@RestController
publicclassUserController{
privatefinalUserServiceuserService;
@PreAuthorize("hasAuthority('sys:user:page')")
publicResponse>page(QueryUserPageParamparam){
returnResponse.success(userService.page(param));
}
}
Spring Security的常用权限注解有:
-
@PreAuthorize:前置校验权限,在方法执行之前校验权限,支持Spel表达式 -
@PostAuthorize:后置权限校验,在方法执行结束以后进行校验,可以对返回结果进行校验,支持Spel表达式 -
@PreFilter:对方法参数进行过滤 -
@PostFilter:对方法结果进行过滤
具体每个权限注解的使用方式可以自行去官网学习,这里就不具体介绍了。
下面就以最常用的@PreAuthorize注解为例,介绍一下Spring Security基于注解鉴权的流程与原理:
-
AuthorizationManagerBeforeMethodInterceptor
(授权管理前置方法拦截器),会将权限注解与AuthorizationManager
(授权管理器)进行关联及初始化 -
AuthorizationManagerBeforeMethodInterceptor
拦截器拦截到请求后,会根据权限注解@PostAuthorize
调用匹配的PreAuthorizeAuthorizationManager#check
方法,并从SecurityContextHolder上下文中获取Authentication
对象,将Supplier
和MethodInvocation
作为参数传递给PreAuthorizeAuthorizationManager#check
方法。
-
AuthorizationManager
授权管理器使用MethodSecurityExpressionHandler
解析@PostAuthorize
注解的SpEL 表达式,并从包含Supplier
和MethodInvocation
的MethodSecurityExpressionRoot
构建相应的EvaluationContext
。 -
然后从
Supplier
读取Authentication
,并检查其权限集合中是否有sys:user:page
。 -
如果校验通过,将继续调用业务方法。如果校验不通过,会发布一个
AuthorizationDeniedEvent
,并抛出一个AccessDeniedException
,ExceptionTranslationFilter
会捕获并处理。
基于配置的授权校验
基于配置的授权校验是通过AuthorizationFilter
实现的,首先我们需要配置授权校验规则:
@Configuration
publicclassSecurityConfig{
/**
*不需要校验权限的资源
*/
publicstaticfinalString[]PERMIT_URL=newString[]{
//knife4j资源
"/doc.html",
"/favicon.ico",
"/swagger-resources",
"/v3/**",
"/webjars/**",
//监控接口
"/actuator/**",
//登录接口
"/login",
//注册接口
"/register",
};
@Bean
publicSecurityFilterChainapiFilterChain(HttpSecurityhttpSecurity,
TokenFiltertokenFilter,
AuthenticationEntryPointauthenticationEntryPoint,
ExceptionFilterexceptionFilter,
AccessDeniedHandleraccessDeniedHandler)throwsException{
//配置token校验过滤器
httpSecurity.addFilterBefore(tokenFilter,AuthorizationFilter.class);
//配置异常处理过滤器
httpSecurity.addFilterBefore(exceptionFilter,ExceptionTranslationFilter.class);
//配置异常处理
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
//配置授权拦截规则
httpSecurity.authorizeHttpRequests()
//配置放行的规则这样配置只能在AuthorizationManager#check方法时返回true如果不经过AuthorizationManager则不生效(比如自定义Filter)
//可以通过自定义WebSecurityCustomizer来达到SecurityFilterChain中的Filter忽略处理参考下方自定义WebSecurityCustomizer
.antMatchers(PERMIT_URL).permitAll()
.antMatchers("/sys/user/page").hasAuthority("sys:user:page")
.anyRequest().authenticated();
//配置组件基于JWT认证因此禁用csrf
httpSecurity.csrf().disable();
//基于JWT认证因此禁用session
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//禁用缓存
httpSecurity.headers().cacheControl().disable();
//允许跨域
httpSecurity.cors();
returnhttpSecurity.build();
}
/**
自定义WebSecurityCustomizer忽略指定路径但是SpringSecurity不建议这么做建议通过antMatchers(PERMIT_URL).permitAll()实现
如果想使用此方式只需要将其注册为SpringBean即可
*/
//@Bean
publicWebSecurityCustomizerwebSecurityCustomizer(){
//配置放行规则
returncustomizer->customizer.ignoring().antMatchers(PERMIT_URL);
}
}
这里咱们还是以前置校验是否拥有某个权限为例:
//配置授权拦截规则校验
httpSecurity.authorizeHttpRequests()
.antMatchers("/sys/user/page").hasAuthority("sys:user:page")
.anyRequest().authenticated();
这里初始化设置了AuthorityAuthorizationManager
作为AuthorizationManager
的实现,不同的权限校验功能可能对应的AuthorizationManager
实现会不一样,比如anyRequest().authenticated()
对应的AuthorizationManager
实现则是AuthenticatedAuthorizationManager
当AuthorizationFilter
执行的时候,会根据配置的授权规则找到对应的AuthorizationManager
实现,然后执行check
方法,并从SecurityContextHolder上下文中获取Authentication
对象,将Authentication
和request
作为参数传递给AuthorizationManager#check
方法。 这里根据上面的规则hasAuthority()
对应的AuthorizationManager
实现就是AuthorityAuthorizationManager
。
然后AuthorityAuthorizationManager
会根据SecurityContextHolder的Authentication
中获取所有权限和配置需要的权限进行对比,如果用户上下文SecurityContextHolder中存储的权限集合包含配置需要的权限则返回true通过,反之则返回false。
注意事项
-
需要特别说明的是,Spring Security存储角色和权限都是使用的 GrantedAuthority
对象,因此Spring Security规定角色需要加上统一前缀方便与权限区分开
❝
这个统一前缀默认为
ROLE_
,无论是基于配置还是基于注解的授权校验都是同样的规则。❞
当然你也可以自定义这个前缀,只需要将自定义的GrantedAuthorityDefaults
对象注册进Spring容器即可。
@Configuration
publicclassSecurityConfig{
/**
*配置Role前缀
*@return
*/
@Bean
staticGrantedAuthorityDefaultsgrantedAuthorityDefaults(){
returnnewGrantedAuthorityDefaults("ROLE_");
}
}
-
细心的朋友可能已经发现了,无论是基于注解还是基于配置的授权校验,都是从用户上下文SecurityContextHolder中获取当前用户拥有的角色和权限,然后再和需要的权限去比较是否拥有权限。所以我们需要在授权校验之前需要往用户上下文SecurityContextHolder中设置当前用户所拥有的权限。这里就需要用到自定义Filter了。
自定义Filter
如果Spring Security中的SecurityFilter
不能满足你的业务需求,需要自定义SecurityFilter
。比如我们需要自定义一个Filter用于解析请求的Token,然后从Token中获取用户信息和权限。自定义SecurityFilter
有两种方式:
-
自定义 SecurityFilter
实现jakarta.servlet.Filter,在doFilter方法中实现自己的业务逻辑,参考案例:
publicclassTokenFilterimplementsFilter{
@Override
publicvoiddoFilter(ServletRequestservletRequest,ServletResponseservletResponse,FilterChainfilterChain)throwsIOException,ServletException{
HttpServletRequestrequest=(HttpServletRequest)servletRequest;
HttpServletResponseresponse=(HttpServletResponse)servletResponse;
Stringtoken=request.getHeader("Authorization");
booleanhasAccess=checkToken(token);
if(hasAccess){
filterChain.doFilter(request,response);
return;
}
//注意AuthenticationException或AccessDeniedException会被ExceptionTranslationFilter处理如果是其他异常需要自己处理Springboot全局异常无法处理
thrownewAuthenticationException("权限不足");
}
}
然后将该SecurityFilter
注册进SecurityFilterChain
@Configuration
publicclassSecurityConfig{
@Bean
SecurityFilterChainfilterChain(HttpSecurityhttp)throwsException{
http.addFilterBefore(newTokenFilter(),AuthorizationFilter.class);
returnhttp.build();
}
}
❝
注意,如果想把jakarta.servlet.Filter的实现注册为Spring Bean,这可能会导致 filter 被调用两次,一次由容器调用,一次由 Spring Security 调用,而且顺序不同。可以通过声明
FilterRegistrationBean
Bean 并将其enabled
属性设置为false
来告诉 Spring Boot不要向容器注册它。配置如下:❞
@Bean
publicFilterRegistrationBeantenantFilterRegistration(TokenFilterfilter){
FilterRegistrationBeanregistration=newFilterRegistrationBean(filter);
registration.setEnabled(false);
returnregistration;
}
-
自定义 SecurityFilter
继承OncePerRequestFilter,这样能保证每个请求只会调用一次的filter(「推荐方式」),然后将该SecurityFilter
注册进SecurityFilterChain
@Component
publicclassTokenFilterextendsOncePerRequestFilter{
@Override
protectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{
StringrequestURI=request.getRequestURI();
//如果时不需要授权的URI直接放行
if(!AntPathUtil.match(requestURI,SecurityConfig.PERMIT_URL)){
Stringtoken=request.getHeader("Authorization");
if(StrUtil.isBlank(token)){
//注意AuthenticationException或AccessDeniedException会被ExceptionTranslationFilter处理如果是其他异常需要自己处理Springboot全局异常无法处理
//AuthenticationCredentialsNotFoundException是AuthenticationException的子类
thrownewAuthenticationCredentialsNotFoundException("请先登录");
}
//这里是伪代码逻辑就是通过token解析出用户信息然后查询出用户所有角色和权限
Useruser=parse(token);
Listauthorities=listUserAllPermissions(user.getId());
PreAuthenticatedAuthenticationTokenauthenticationToken=newPreAuthenticatedAuthenticationToken(user,null,authorities);
//设置子线程支持从父线程获取用户上下文注意使用ForkJoinPool无法生效如果是线程池可能导致数据错误谨慎使用
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
//设置上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
//放行
filterChain.doFilter(request,response);
}
}
publicclassAntPathUtil{
privateAntPathUtil(){}
publicstaticfinalAntPathMatcherMATCHER=createMatcher();
publicstaticbooleanmatch(Stringpath,String...pattern){
if(ArrayUtil.isEmpty(pattern)){
returntrue;
}
if(StrUtil.isBlank(path)){
returnfalse;
}
returnArrays.stream(pattern).filter(p->MATCHER.match(p,path)).findAny().isPresent();
}
privatestaticAntPathMatchercreateMatcher(){
AntPathMatcherantPathMatcher=newAntPathMatcher();
antPathMatcher.setCaseSensitive(false);
returnantPathMatcher;
}
}
然后将该SecurityFilter
注册进SecurityFilterChain
@Configuration
publicclassSecurityConfig{
@Bean
SecurityFilterChainfilterChain(HttpSecurityhttp,TokenFiltertokenFilter)throwsException{
http.addFilterBefore(tokenFilter,AuthorizationFilter.class);
returnhttp.build();
}
}
总结
最后总结Spring Security的认证和授权的流程如下:
梳理一下上面的流程:
-
首先,用户携带用户名密码通过LoginController进行登录(认证流程),如果登录成功则返回token(推荐使用JWT作为token) -
后续其他请求,携带通过登录获取得到的token,然后先被TokenFilter解析token获取用户信息,并将用户信息写入SecurityContextHolder -
然后进行授权流程 -
AuthorizationManager
通过从SecurityContextHolder获取到当前用户的authentication(权限集合),然后与需要的权限进行对比,从而校验当前用户是否有权限使用当前业务功能
本文使用 markdown.com.cn 排版
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net