1154 lines
58 KiB
Markdown
1154 lines
58 KiB
Markdown
- [Spring Security](#spring-security)
|
||
- [Spring Security简介](#spring-security简介)
|
||
- [Spring Security自动配置](#spring-security自动配置)
|
||
- [Spring Security结构](#spring-security结构)
|
||
- [DelegatingFilterProxy](#delegatingfilterproxy)
|
||
- [FilterChainProxy](#filterchainproxy)
|
||
- [SecurityFilterChain](#securityfilterchain)
|
||
- [SecurityFilters](#securityfilters)
|
||
- [添加自定义filter到SecurityFilterChain](#添加自定义filter到securityfilterchain)
|
||
- [处理Security异常](#处理security异常)
|
||
- [在多次认证之间保存request](#在多次认证之间保存request)
|
||
- [RequestCache](#requestcache)
|
||
- [Spring Security身份认证架构](#spring-security身份认证架构)
|
||
- [SecurityContextHolder](#securitycontextholder)
|
||
- [SecurityContext](#securitycontext)
|
||
- [Authentication](#authentication)
|
||
- [GrantedAuthority](#grantedauthority)
|
||
- [AuthenticationManager](#authenticationmanager)
|
||
- [ProviderManager](#providermanager)
|
||
- [AuthenticationProvider](#authenticationprovider)
|
||
- [AuthenticationEntryPoint](#authenticationentrypoint)
|
||
- [AbstractAuthenticationProcessingFilter](#abstractauthenticationprocessingfilter)
|
||
- [Username/Password Authentication](#usernamepassword-authentication)
|
||
- [发布AuthenticationManager bean](#发布authenticationmanager-bean)
|
||
- [自定义AuthenticationManager](#自定义authenticationmanager)
|
||
- [从请求中读取Username/Password](#从请求中读取usernamepassword)
|
||
- [Form](#form)
|
||
- [非/login请求时,UsernamePassword并不执行认证校验逻辑](#非login请求时usernamepassword并不执行认证校验逻辑)
|
||
- [HttpBasic](#httpbasic)
|
||
- [Digest](#digest)
|
||
- [password storage](#password-storage)
|
||
- [in-memory authentication](#in-memory-authentication)
|
||
- [jdbc authentication](#jdbc-authentication)
|
||
- [UserDetails](#userdetails)
|
||
- [UserDetailsService](#userdetailsservice)
|
||
- [RememberMe](#rememberme)
|
||
- [hash based token](#hash-based-token)
|
||
- [将token持久化到数据库中](#将token持久化到数据库中)
|
||
- [RememberMe的接口及其实现](#rememberme的接口及其实现)
|
||
- [TokenBasedRememberMeService](#tokenbasedremembermeservice)
|
||
- [匿名身份认证](#匿名身份认证)
|
||
- [匿名认证配置](#匿名认证配置)
|
||
- [处理logout](#处理logout)
|
||
- [自定义登出url](#自定义登出url)
|
||
- [Authorization](#authorization)
|
||
- [结构](#结构)
|
||
- [Invocation Handling](#invocation-handling)
|
||
- [基于委托的AuthorizationManager实现](#基于委托的authorizationmanager实现)
|
||
- [AuthorityAuthorizationManager](#authorityauthorizationmanager)
|
||
- [AuthenticatedAuthorizationManager](#authenticatedauthorizationmanager)
|
||
- [AuthorizationManagers](#authorizationmanagers)
|
||
- [自定义AuthorizationManager](#自定义authorizationmanager)
|
||
- [ROLE继承](#role继承)
|
||
- [基于HttpServletRequest的权限认证](#基于httpservletrequest的权限认证)
|
||
- [基于Request的Authorization Component实现](#基于request的authorization-component实现)
|
||
- [AuthorizationFilter默认情况下是SecurityFilterChain中的最后一个filter](#authorizationfilter默认情况下是securityfilterchain中的最后一个filter)
|
||
- [Authentication查询是延迟的](#authentication查询是延迟的)
|
||
- [authorizing endpoint](#authorizing-endpoint)
|
||
- [请求匹配的pattern声明](#请求匹配的pattern声明)
|
||
- [Ant](#ant)
|
||
- [regex](#regex)
|
||
- [通过Http Method来进行匹配](#通过http-method来进行匹配)
|
||
- [自定义Matcher来进行匹配](#自定义matcher来进行匹配)
|
||
- [Authorizing Request](#authorizing-request)
|
||
- [通过Spel表达式来执行权限认证](#通过spel表达式来执行权限认证)
|
||
- [使用Authorization表达式](#使用authorization表达式)
|
||
- [使用path变量](#使用path变量)
|
||
- [使用数据库来进行权限认证](#使用数据库来进行权限认证)
|
||
- [SecurityMatcher](#securitymatcher)
|
||
|
||
|
||
# Spring Security
|
||
## Spring Security简介
|
||
Spring Security作为一个安全框架,向使用者提供了用户认证、授权、常规攻击保护等功能。
|
||
## Spring Security自动配置
|
||
默认情况下,在引入Spring Security的启动器依赖之后,Spring Boot自动配置会做如下工作:
|
||
- 启用Spring Security的默认配置,创建一个的bean对象,bean对象名称为“springSecurityFilterChain”,bean对象类型为SecurityFilterChain,实现了Filter。该bean对象为负责应用中所有与安全相关的操作(例如验证提交的username和password,保护应用的url等)
|
||
- 创建一个UserDetailsService的bean对象,并且产生一个为”user“的username和一个随机产生的password,随机产生的password会输出在console日志中
|
||
- 将名为springSecurityFilterChain的bean对象注册到servlet容器中,用来对每个servlet请求进行处理
|
||
> Spring Security会通过Spring Security会通过BCtypt(Hash解密算法)来对密码的存储进行保护
|
||
|
||
## Spring Security结构
|
||
### DelegatingFilterProxy
|
||
Spring提供了Filter的一个实现类DelegatingFilterProxy,其将Servlet容器的生命周期和Spring的ApplicationContext连接在一起。
|
||
***DelegatingFilterProxy会通过标准的servlet容器机制被注册,但是将所有工作都委托给Spring容器中实现了Filter的bean对象***。
|
||
> DelegatingFilterProxy会在ApplicationContext中查找实现了Filter的bean对象,并且调用该bean对象的doFilter方法
|
||
> ```java
|
||
> public void doFilter(ServletRequest request,
|
||
> ServletResponse response, FilterChain chain) {
|
||
> // Lazily get Filter that was registered as a Spring Bean
|
||
> // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
|
||
> Filter delegate = getFilterBean(someBeanName);
|
||
> // delegate work to the Spring Bean
|
||
> delegate.doFilter(request, response);
|
||
> }
|
||
> ```
|
||
|
||
### FilterChainProxy
|
||
Spring Security支持FilterChainProxy,FilterChainProxy是一个由Spring Security提供的特殊Filter,FilterChainProxy通过SecurityFilterChain允许向多个Filters委托工作。
|
||
***FilterChainProxy是一个bean对象,通过被包含在DelegatingFilterProxy中。***
|
||
|
||
### SecurityFilterChain
|
||
SecurityFilterChain通常被FilterChainProxy使用,用来决定在该次请求中调用哪个Spring Security Filters。
|
||
|
||
### SecurityFilters
|
||
Security Filters通过SecurityFilterChain API被插入到FilterChainProxy中。
|
||
|
||
### 添加自定义filter到SecurityFilterChain
|
||
大多数情况下,默认的security是足够的,但是,也允许向security filter chain中添加自定义过滤器。
|
||
|
||
如下是一个自定义过滤器的示例,示例中自定义filter会在认证后校验用户对于请求的租户是否拥有权限:
|
||
```java
|
||
import java.io.IOException;
|
||
|
||
import jakarta.servlet.Filter;
|
||
import jakarta.servlet.FilterChain;
|
||
import jakarta.servlet.ServletException;
|
||
import jakarta.servlet.ServletRequest;
|
||
import jakarta.servlet.ServletResponse;
|
||
import jakarta.servlet.http.HttpServletRequest;
|
||
import jakarta.servlet.http.HttpServletResponse;
|
||
|
||
import org.springframework.security.access.AccessDeniedException;
|
||
|
||
public class TenantFilter implements Filter {
|
||
|
||
@Override
|
||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||
|
||
String tenantId = request.getHeader("X-Tenant-Id"); //(1)
|
||
boolean hasAccess = isUserAllowed(tenantId); //(2)
|
||
if (hasAccess) {
|
||
filterChain.doFilter(request, response); //(3)
|
||
return;
|
||
}
|
||
throw new AccessDeniedException("Access denied"); //(4)
|
||
}
|
||
|
||
}
|
||
```
|
||
如下是自定义过滤器链的流程:
|
||
1. 步骤从请求header中获取了tenantId
|
||
2. 步骤校验了用户对于该tenant id代表的租户资源是否拥有访问权限
|
||
3. 只有当用户对租户资源拥有访问权限时,才会继续调用filterChain中剩余的部分
|
||
4. 如果用户没有访问权限,将会抛出AccessDeniedException
|
||
|
||
在定义完自定义过滤器后,还需要将自定义filter添加到Security Filter Chain中,添加代码如下所示例:
|
||
```java
|
||
@Bean
|
||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||
http
|
||
// ...
|
||
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
|
||
return http.build();
|
||
}
|
||
```
|
||
> 由于权限校验操作在身份认证之后,故而将TenantFilter加到AuthorizationFilter之前可以保证在执行TenantFilter时,所有的身份认证操作都已经执行完成。
|
||
|
||
> 在向SecurityFilterChain注册filter时,不应该将自定义filter注册为bean,因为spring会将注册为bean的filter对象都添加到内置容器中,这会造成注册为bean的filter被调用两次:一次被Spring Seurity调用,一次被container调用。
|
||
|
||
|
||
|
||
### 处理Security异常
|
||
ExceptionTranslationFilter将`AccessDeniedException`和`AuthenticationException`翻译为http response。
|
||
|
||
> ExceptionTranslationFilter被插入到FilterChainProxy中,作为SecurityFilterChain中的一个filter。
|
||
> 如果应用程序没有抛出AccessDeniedException或AuthenticationException,那么ExceptionTranslationFilter并不会做任何事情。
|
||
|
||
ExceptionTranslationFilter的处理流程:
|
||
1. ExceptionTranslationFilter首先会调用`filterChain.doFilter`来执行后续的逻辑
|
||
2. 在执行后续请求时,如果抛出了AuthenticationException,那么则会开启认证流程
|
||
1. 清空SecurityContextHolder
|
||
2. 保存HttpServletRequest,当认证通过时,还能重新发送原始请求
|
||
3. `AuthenticationEntryPoint`用于重新请求用户的凭证,其可能会重定向到登录页面或是发送 `www-authenticate`header
|
||
3. 在执行后续请求时,如果抛出了AccessDeniedException,那么会调用`AccessDeniedHandler`用于处理访问拒绝。
|
||
4. 如果执行后续逻辑时,抛出了非`AuthenticationException`和`AccessDeniedException`的异常,那么异常将会被重新抛出,ExceptionTranslationFilter并不会针对其进行处理
|
||
|
||
```java
|
||
// ExceptionTranslationFilter的伪代码
|
||
try {
|
||
filterChain.doFilter(request, response);
|
||
} catch (AccessDeniedException | AuthenticationException ex) {
|
||
if (!authenticated || ex instanceof AuthenticationException) {
|
||
startAuthentication();
|
||
} else {
|
||
accessDenied();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 在多次认证之间保存request
|
||
如果用户在发送请求访问需要认证的资源时,尚未经过身份认证,那么需要将本次请求保存起来,并且对用户进行认证,待用户认证通过之后,再重新执行保存的原始请求。在Spring Security中,是通过RequestCache来实现保存请求的。
|
||
|
||
#### RequestCache
|
||
HttpServletRequest被保存在RequestCache中,当用户经过重新认证后,RequestCache将会用于重新发送原始请求。
|
||
|
||
在Spring Security中,在sendStartAuthentication时会像requestCache中存储request,并且重新执行认证流程,认证通过后,将会重新发送原始请求。
|
||
|
||
RequestCacheAwareFilter负责请求的重新发送。
|
||
|
||
默认情况下,会使用`HttpSessionRequestCache`,如下代码展示了HttpSessionRequestCache的自定义使用:
|
||
```java
|
||
@Bean
|
||
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
|
||
// 匹配本次request时,需要确保请求url参数中具有continue参数
|
||
requestCache.setMatchingRequestParameterName("continue");
|
||
http
|
||
// ...
|
||
.requestCache((cache) -> cache
|
||
.requestCache(requestCache)
|
||
);
|
||
return http.build();
|
||
}
|
||
```
|
||
|
||
由于在认证失败时存储原始请求会占用内存,如果想要禁止在抛出AuthenticationException后,保存原始请求到RequestCache的行为,可以为SecurityFilterChain指定`NullRequestCache`:
|
||
```java
|
||
@Bean
|
||
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||
RequestCache nullRequestCache = new NullRequestCache();
|
||
http
|
||
// ...
|
||
.requestCache((cache) -> cache
|
||
.requestCache(nullRequestCache)
|
||
);
|
||
return http.build();
|
||
}
|
||
```
|
||
|
||
## Spring Security身份认证架构
|
||
### SecurityContextHolder
|
||
SecurityContextHolder用于存储用户的认证信息,是Spring Security身份认证认证模型的核心,SecurityContextHolder中包含有SecurityContextHolder,SecurityContextHolder负责将当前线程和SecurityContext相关联。
|
||
|
||
如果想要将用户表示为已经通过身份认证,最简单的方式是向SecurityContextHolder中设置值,示例如下:
|
||
|
||
```java
|
||
SecurityContext context = SecurityContextHolder.createEmptyContext(); // 1
|
||
Authentication authentication =
|
||
new TestingAuthenticationToken("username", "password", "ROLE_USER"); //2
|
||
context.setAuthentication(authentication);
|
||
|
||
SecurityContextHolder.setContext(context); //3
|
||
```
|
||
|
||
上述流程如下所示:
|
||
1. 创建了一个空的SecurityContext。在最开始,应该通过createEmptyContext方法创建一个空的SecurityContext,而不是使用`getContext().setAuthentication(authentication)`,从而避免在多线程下的竞争场景
|
||
2. 第二步中,创建了一个Authentication对象,Spring Security不关注向SecurityContext中塞入了哪种类型的Authentication实现
|
||
3. 在第三步中,将SecurityContext设置到了SecurityContextHolder中,spring使用设置的SecurityContext信息进行身份认证
|
||
|
||
如果后续操作想要访问当前已认证用户的信息,可以访问SecurityContextHolder。
|
||
|
||
```java
|
||
SecurityContext context = SecurityContextHolder.getContext();
|
||
Authentication authentication = context.getAuthentication();
|
||
String username = authentication.getName();
|
||
Object principal = authentication.getPrincipal();
|
||
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
||
```
|
||
|
||
在默认情况下,`SecurityContextHolder`使用ThreadLocal来存SecurityContext,故而SecurityContext在同一线程内可以共享。在SecurityContextHolder中使用ThreadLocal是安全的,不会发生当前请求完成而SecurityContext未被清理的情况,Spring的FilterChainProxy保证SecurityContext会被清理。
|
||
|
||
在一些场景下,可能不希望使用ThreadLocal来存储ContextHolder,例如在swing程序下,可能希望整个java虚拟机中的线程都共享同一个security context。可以在启动时为SecurityContextHolder指定一个strategy,该strategy决定context应该如何被存储。
|
||
|
||
对于应用程序内所有线程共享的security context,可以使用`SecurityContextHolder.MODE_GLOBAL`策略。
|
||
|
||
### SecurityContext
|
||
Security Context用于存储用户的身份认证信息,从SecurityContextHolder中获取。
|
||
|
||
### Authentication
|
||
Authentication在Spring Security中具有两种使用场景:
|
||
- 在用户尚未被认证的场景,向`AuthenticationManager`提供用于身份认证的凭证
|
||
- 在用户已经被认证的场景,Authentication用于代表当前已经通过认证的用户,此时可以通过SecurityContext来获取用户认证信息
|
||
|
||
Authentication包含有如下信息:
|
||
- 主体(principal):用于标识用户,当通过username/password进行认证时,其通常是UserDetails类的实例
|
||
- 凭据(credentials):通常是password,在许多场景下为了保证凭证不会被泄露,在用户通过认证后,凭证将会被清空
|
||
- 权限(authorities):代表用户被授予的权限,为`GrantedAuthority`的实例集合
|
||
|
||
### GrantedAuthority
|
||
GrantedAuthority是用户被授予的权限。
|
||
|
||
可以通过`Authentication.getAuthorities()`方法来获取用户被授予的权限集合,该方法会返回`GrantedAuthority`的集合。
|
||
|
||
### AuthenticationManager
|
||
AuthenticationManager定义了Security Filters如何执行认证。
|
||
|
||
AuthenticationManager会对传递给其的Authentication执行认证操作,如果认证成功,会返回一个被完全填充的Authetication对象。
|
||
|
||
在AuthenticationManager的authenticate方法返回Authentication对象之后,调用AuthenticationManager的controller会将返回的Authentication对象设置到SecurityContextHolder中。
|
||
|
||
AuthenticationManager的实现可以是任何类,最通用的实现类是`ProviderManager`.
|
||
|
||
### ProviderManager
|
||
ProviderManager是最常用的AuthenticationManager实现类,ProviderMananger将认证工作委托给了一个`AuthenticationProvider`的实例集合。在集合中的每一个AuthenticationProvider,都可以决定当前认证工作是认证成功、认证失败还是将认证工作交由集合中的后续AuthenticationProvider决定。
|
||
|
||
如果AuthenticationProvider集合中没有任何AuthenticationProvider可以决定当前认证工作是成功或是失败,那么当前认证工作会失败,并且抛出`ProviderNotFoundException`异常。该异常代表当前的认证方式没有被支持。
|
||
|
||
> 在实践中,每个AuthenticationProvider都明确其应该负责的认证方式,例如一个AuthenticationProvider负责用户名/密码形式的登录,另一个AuthenticationProvider负责saml形式的登录。故而,可以在支持多种认证方式的情况下,只通过一个AuthenticationManager bean对象来进行整合多种认证方式。
|
||
|
||
对于ProviderManager,可以配置一个parent ProviderManager,当本级没有AuthenticationProvider能够完成认证工作时,将会将认证工作委托给父级manager。
|
||
|
||
默认情况下,ProviderManager将会在认证成功后,清空任任何成功登录后返回Authenticaiton对象的凭证信息。
|
||
|
||
### AuthenticationProvider
|
||
AuthenticationProvider负责执行特定类型的认证工作,可以将多个AuthenticationProvider对象注入到同一个ProviderMananger中。
|
||
|
||
例如,DaoAuthenticationProvider支持基于用户名/密码的认证,JwtAuthenticationManager则是支持基于jwt形式的认证。
|
||
|
||
### AuthenticationEntryPoint
|
||
AuthenticationEntryPoint用于服务端向用户请求凭证(例如,重定向到登录页面,向客户端发送`WWW-Authenticate`响应等。)
|
||
|
||
一些场景下,用户在请求资源时会主动发送在请求中包含凭据。在这些场景下,spring security不用向客户返回相应来要求客户提供凭证。
|
||
|
||
在其他场景下,客户端可能在未认证的情况下发送请求来访问资源,这时,可以使用`AuthenticationEntryPoint`的实现来向客户请求凭证。`AuthenticationEntryPoint`的实现可以重定向到登录页,或是向客户端发送`WWW-Authenticate`响应,或是采用其他方式来向客户端请求凭据。
|
||
|
||
### AbstractAuthenticationProcessingFilter
|
||
`AbstractAuthenticationProcessingFilter`用于认证用户的凭据。在凭据可以被认证之前,spring security通过会通过`AuthentciationEntryPoint`来向客户请求凭据。
|
||
|
||
AbstractAuthenticationProcessingFilter通过如下流程来执行认证:
|
||
1. 当用户提交凭据时,AbstractAuthenticationProcessingFilter会根据将要被认证的request来创建一个Authentication对象,创建Authentication的类型基于AbstractAuthenticationProcessingFilter的继承类。例如,UsernamePasswordAuthenticationFilter将会基于request中提交的username和password创建一个UsernamePasswordAuthenticationToken
|
||
2. 接下来,Authentication将会被传递给AuthenticationMananger用于认证
|
||
3. 如果认证失败,那么
|
||
1. SecurityContextHolder将会被清空
|
||
2. RememberMeServices.loginFail方法将会被调用,如果remeberMe没有被调用,那么将不会执行任何操作
|
||
3. AuthenticationFailureHandler将会被调用
|
||
4. 如果认证成功:
|
||
1. 将会通知SessionAuthenticaitonStrategy将会被提示有一个新的登录
|
||
2. Authentication将会被设置到SecurityContextHolder中。如果想要在多个请求之前保存context,需要手动调用`SecurityContextRepository#saveContext`方法,调用后,SecurityContext可以在后续的请求中自动被设置
|
||
3. 调用RememberMeServices.loginSuccess方法,如果没有设置rememberMe,那么不会执行任何操作
|
||
4. ApplicationEventPublisher将会发不InteractiveAuthenticationSuccessEvent事件
|
||
5. AuthenticationSuccessHandler将会被调用
|
||
|
||
## Username/Password Authentication
|
||
认证用户时最广泛使用的认证方式为用户名/密码认证。Spring Security对基于用户名和密码的认证提供了全面的支持。
|
||
|
||
可以通过如下方式来配置用户名和密码认证:
|
||
|
||
```java
|
||
@Configuration
|
||
@EnableWebSecurity
|
||
public class SecurityConfig {
|
||
|
||
@Bean
|
||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.anyRequest().authenticated()
|
||
)
|
||
.httpBasic(Customizer.withDefaults())
|
||
.formLogin(Customizer.withDefaults());
|
||
|
||
return http.build();
|
||
}
|
||
|
||
@Bean
|
||
public UserDetailsService userDetailsService() {
|
||
UserDetails userDetails = User.withDefaultPasswordEncoder()
|
||
.username("user")
|
||
.password("password")
|
||
.roles("USER")
|
||
.build();
|
||
|
||
return new InMemoryUserDetailsManager(userDetails);
|
||
}
|
||
|
||
}
|
||
```
|
||
上述示例向SecurityFilterChain中自动注册了InMemoryUserDetailsService,并且向默认的AuthenticationManager中注册了DaoUserAuthenticationProvider,并且启用了FormLogin和HttpBasic的认证。
|
||
|
||
### 发布AuthenticationManager bean
|
||
当想要自定义认证过程时,可以将AuthenticationManager发布为bean对象,示例如下:
|
||
|
||
```java
|
||
@Configuration
|
||
@EnableWebSecurity
|
||
public class SecurityConfig {
|
||
|
||
@Bean
|
||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.requestMatchers("/login").permitAll()
|
||
.anyRequest().authenticated()
|
||
);
|
||
|
||
return http.build();
|
||
}
|
||
|
||
@Bean
|
||
public AuthenticationManager authenticationManager(
|
||
UserDetailsService userDetailsService,
|
||
PasswordEncoder passwordEncoder) {
|
||
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
|
||
authenticationProvider.setUserDetailsService(userDetailsService);
|
||
authenticationProvider.setPasswordEncoder(passwordEncoder);
|
||
|
||
return new ProviderManager(authenticationProvider);
|
||
}
|
||
|
||
@Bean
|
||
public UserDetailsService userDetailsService() {
|
||
UserDetails userDetails = User.withDefaultPasswordEncoder()
|
||
.username("user")
|
||
.password("password")
|
||
.roles("USER")
|
||
.build();
|
||
|
||
return new InMemoryUserDetailsManager(userDetails);
|
||
}
|
||
|
||
@Bean
|
||
public PasswordEncoder passwordEncoder() {
|
||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||
}
|
||
|
||
}
|
||
```
|
||
> 在上述示例中,没有指定formLogin或是httpBasic来执行身份认证,并且针对login请求,其权限校验为permitAll,故而login请求的实际身份认证工作由自定义的loginController来执行
|
||
|
||
|
||
|
||
当添加了上述Configuration类后,可以指定自定义的Controller类:
|
||
```java
|
||
@RestController
|
||
public class LoginController {
|
||
|
||
private final AuthenticationManager authenticationManager;
|
||
|
||
public LoginController(AuthenticationManager authenticationManager) {
|
||
this.authenticationManager = authenticationManager;
|
||
}
|
||
|
||
@PostMapping("/login")
|
||
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest) {
|
||
Authentication authenticationRequest =
|
||
UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.username(), loginRequest.password());
|
||
Authentication authenticationResponse =
|
||
this.authenticationManager.authenticate(authenticationRequest);
|
||
// ...
|
||
}
|
||
|
||
public record LoginRequest(String username, String password) {
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||
### 自定义AuthenticationManager
|
||
默认情况下,SpringSecurity会在内部构建一个AuthenticationManager,该AuthenticationManager由DaoAuthenticationProvider组成,用于对username/password进行认证。
|
||
|
||
为了实现AuthenticationMananger的自定义,可以发布一个AuthenticationManager bean对象,spring security会使用发布的bean对象。
|
||
|
||
```java
|
||
@Configuration
|
||
@EnableWebSecurity
|
||
public class SecurityConfig {
|
||
|
||
@Bean
|
||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.requestMatchers("/login").permitAll()
|
||
.anyRequest().authenticated()
|
||
)
|
||
.httpBasic(Customizer.withDefaults())
|
||
.formLogin(Customizer.withDefaults());
|
||
|
||
return http.build();
|
||
}
|
||
|
||
@Bean
|
||
public AuthenticationManager authenticationManager(
|
||
UserDetailsService userDetailsService,
|
||
PasswordEncoder passwordEncoder) {
|
||
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
|
||
authenticationProvider.setUserDetailsService(userDetailsService);
|
||
authenticationProvider.setPasswordEncoder(passwordEncoder);
|
||
|
||
ProviderManager providerManager = new ProviderManager(authenticationProvider);
|
||
providerManager.setEraseCredentialsAfterAuthentication(false);
|
||
// provider为最常用的AuthenticationManager的实现类
|
||
return providerManager;
|
||
}
|
||
|
||
@Bean
|
||
public UserDetailsService userDetailsService() {
|
||
UserDetails userDetails = User.withDefaultPasswordEncoder()
|
||
.username("user")
|
||
.password("password")
|
||
.roles("USER")
|
||
.build();
|
||
|
||
return new InMemoryUserDetailsManager(userDetails);
|
||
}
|
||
|
||
@Bean
|
||
public PasswordEncoder passwordEncoder() {
|
||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||
}
|
||
|
||
}
|
||
```
|
||
除了发布自定义的AuthenticaitonMananger之外,还可以通过修改AuthenticationManagerBuilder bean对象来实现。
|
||
|
||
AuthenticationManagerBuilder被发布为bean对象,并且用于构建Spring Security的全局AuthenticationManager,可以按如下方式来自定义:
|
||
```java
|
||
@Configuration
|
||
@EnableWebSecurity
|
||
public class SecurityConfig {
|
||
|
||
@Bean
|
||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||
// ...
|
||
return http.build();
|
||
}
|
||
|
||
@Bean
|
||
public UserDetailsService userDetailsService() {
|
||
// Return a UserDetailsService that caches users
|
||
// ...
|
||
}
|
||
|
||
@Autowired
|
||
public void configure(AuthenticationManagerBuilder builder) {
|
||
builder.eraseCredentials(false);
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||
如果想要自定义登录页,可以通过如下方式:
|
||
```java
|
||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||
http
|
||
.formLogin(form -> form
|
||
.loginPage("/login")
|
||
.permitAll()
|
||
);
|
||
// ...
|
||
}
|
||
```
|
||
|
||
并且在使用spring mvc时,需要将/login和页面模板映射:
|
||
```java
|
||
@Controller
|
||
class LoginController {
|
||
@GetMapping("/login")
|
||
String login() {
|
||
return "login";
|
||
}
|
||
}
|
||
```
|
||
|
||
> 上述permitAll方法确保了所有用户对loginUrl,loginProcessingUrl,failureUrl都拥有访问权限。
|
||
|
||
### 从请求中读取Username/Password
|
||
Spring Security提供如下方式来从请求中读取Username和password:
|
||
- Form
|
||
- Basic
|
||
- Digest
|
||
|
||
#### Form
|
||
在http用户名和密码被作为http form的形式(x-www-form-urlencoded)被传递时,Spring Security支持获取form形式的username和password。
|
||
|
||
默认情况下,Spring Security form login被开启,但是如果在应用手动配置了SecurityFilterChain,那么必须显式调用`formLogin`来指定。
|
||
|
||
```java
|
||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||
http
|
||
.formLogin(withDefaults());
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### 非/login请求时,UsernamePassword并不执行认证校验逻辑
|
||
> 当未认证用户访问受保护资源时,AuthorizationFilter会抛出AccessDenied异常,导致重定向到登录页面。
|
||
>
|
||
> UsernamePasswordAuthenticationFilter当请求url并不为/login时,并不执行认证逻辑,而是交由后续AuthorizationFilter来校验当前用户是否由访问资源的权限。
|
||
>
|
||
> UsernamePasswordAuthenticationFilter只是在用户发送/login请求执行登录时,才通过authenticationManager来执行认证逻辑。
|
||
|
||
#### HttpBasic
|
||
在使用httpBasic时,如果未经认证的用户请求受保护资源,会返回`WWW-Authenticate`的header,此时客户端应该在下次提交时在请求中添加用户名和密码。
|
||
|
||
在使用httpBasic时,应该在请求的`Authorization`header中添加username和password的内容,形式如下:
|
||
```http
|
||
Authorization: Basic <credentials>
|
||
```
|
||
其中`<credentials>`应该通过base64进行加密
|
||
|
||
#### Digest
|
||
目前应用中不应使用Digest。
|
||
|
||
### password storage
|
||
|
||
#### in-memory authentication
|
||
`InMemoryUserDetailsManager`实现了`UserDetailsService`,其将用户名和密码存储在内存中,并且提供了基于username和password的认证。
|
||
|
||
使用实例如下所示:
|
||
```java
|
||
@Bean
|
||
public UserDetailsService users() {
|
||
UserDetails user = User.builder()
|
||
.username("user")
|
||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||
.roles("USER")
|
||
.build();
|
||
UserDetails admin = User.builder()
|
||
.username("admin")
|
||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||
.roles("USER", "ADMIN")
|
||
.build();
|
||
return new InMemoryUserDetailsManager(user, admin);
|
||
}
|
||
```
|
||
在上述示例中,需要手动对密码进行散列和编码处理,可以通过`User.withDefaultPasswordEncoder`来自动对传入密码进行编码处理:
|
||
```java
|
||
@Bean
|
||
public UserDetailsService users() {
|
||
// The builder will ensure the passwords are encoded before saving in memory
|
||
UserBuilder users = User.withDefaultPasswordEncoder();
|
||
UserDetails user = users
|
||
.username("user")
|
||
.password("password")
|
||
.roles("USER")
|
||
.build();
|
||
UserDetails admin = users
|
||
.username("admin")
|
||
.password("password")
|
||
.roles("USER", "ADMIN")
|
||
.build();
|
||
return new InMemoryUserDetailsManager(user, admin);
|
||
}
|
||
```
|
||
#### jdbc authentication
|
||
`JdbcDaoImpl`实现了`UserDetailsService`,支持基于用户名和密码的认证,并且将密码存储在数据库中。
|
||
|
||
#### UserDetails
|
||
`UserDetails`由`UserDetailsService`返回,`DaoAuthenticationProvider`针对UserDetails及逆行校验,并且返回`Authentication`。Authentication是基于UserDetails进行构建的。
|
||
|
||
#### UserDetailsService
|
||
DaoAuthenticationProvider通过UserDetailsService来获取UserDetails信息,UserDetails信息中包含用户名、密码和用户其他信息。
|
||
|
||
可以通过自定义UserDetailsService的bean对象来自定义UserDetails的获取逻辑:
|
||
```java
|
||
@Bean
|
||
CustomUserDetailsService customUserDetailsService() {
|
||
return new CustomUserDetailsService();
|
||
}
|
||
```
|
||
|
||
### RememberMe
|
||
rememberMe要求在多个session之间,应用能够记住用户的身份。其通常是由cookie实现的,如果指定cookie在后续请求中被检测到,那么会进行自动登录。
|
||
|
||
Spring Security提供了两种Remember Me实现,一种是基于hash来保存token,另一种则是通过数据库来保存token。
|
||
|
||
#### hash based token
|
||
该方案通过hash来实现有效的rememberMe策略,在认证成功后,如下cookie会被发送给客户端:
|
||
```http
|
||
base64(username + ":" + expirationTime + ":" + algorithmName + ":"
|
||
+algorithmHex(username + ":" + expirationTime + ":" password + ":" + key))
|
||
|
||
username: As identifiable to the UserDetailsService
|
||
password: That matches the one in the retrieved UserDetails
|
||
expirationTime: The date and time when the remember-me token expires, expressed in milliseconds
|
||
key: A private key to prevent modification of the remember-me token
|
||
algorithmName: The algorithm used to generate and to verify the remember-me token signature
|
||
```
|
||
其中,第四部分通过(username,expirationTime,password,key),以algorithmName算法生成。
|
||
|
||
> 当该cookie重新被发送到服务端时,服务端会取出cookie的前三部分,并通过UserDetailService查询出password(该password为获取的UserDetails中的password,为编码后的密码),按照cookie中指定的算法,结合存储在服务端的private key重新计算出第四部分,并且将计算结果和第四部分进行比较。
|
||
>
|
||
> 该算法可以保证cookie并没有被篡改,因为password和private key都存储在服务端,如果手动修改username或是过期时间,那么都会导致服务端计算的第四部分结果不匹配。
|
||
|
||
但是,使用该hash-based方法时,存在安全隐患,因为所有捕获到该token的agent都可以使用该token访问用户资源,直到token过期。
|
||
|
||
在用户意识到token已经被泄露时,可以通过修改密码来使该token无效,因为修改密码后第四部分的计算结果会发生变化,导致旧token失效。
|
||
|
||
如果想要开启rememberMe,可以通过如下方式开启:
|
||
```java
|
||
http
|
||
.authorizeHttpRequests(authorize -> {
|
||
authorize.requestMatchers("/login").permitAll()
|
||
.anyRequest().authenticated();
|
||
})
|
||
.formLogin(Customizer.withDefaults())
|
||
.httpBasic(Customizer.withDefaults())
|
||
.rememberMe(Customizer.withDefaults());
|
||
return http.build();
|
||
```
|
||
|
||
#### 将token持久化到数据库中
|
||
为了在多次session之间记住用户的身份信息,可以通过将token存储到数据库中,从而记住用户的身份。
|
||
|
||
#### RememberMe的接口及其实现
|
||
remember-me将会由`UsernamePasswordAuthenticationFilter`来使用,与rememberMe相关的hook位于`RememberMeServices`接口中,并且将会在适当的时机被调用。如下展示了rememberMe相关的接口定义:
|
||
```java
|
||
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
|
||
|
||
void loginFail(HttpServletRequest request, HttpServletResponse response);
|
||
|
||
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
|
||
Authentication successfulAuthentication);
|
||
```
|
||
`AbstractAuthenticationProcessingFilter`只会调用loginSuccess或loginFail方法;而`autoLogin`方法将会由`RememberMeAuthenticationFilter`进行调用,当`SecurityContextHolder`不包含`Authentication`对象时,就会调用autoLogin方法。
|
||
|
||
#### TokenBasedRememberMeService
|
||
TokenBasedRemeberMeService支持上述基于hash的rememberMe方法。`TokenBasedRememberMeServices`会生成`RememberMeAuthenticationToken`,该token将会由`RememberMeAuthenticationProvider`进行验证。
|
||
|
||
为了`TokenBasedRememberMeServices`生成的token必须能被`RememberMeAuthenticationToken`正确的校验,必须相同的`key`必须能够在两者之间进行共享。
|
||
|
||
默认情况下,TokenBasedRememberMeService使用`SHA-256`来对token进行编码。如果想要针对`TokenBasedRememberMeServices`进行自定义,可以发布自己的bean:
|
||
```java
|
||
@Bean
|
||
SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception {
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.anyRequest().authenticated()
|
||
)
|
||
.rememberMe((remember) -> remember
|
||
.rememberMeServices(rememberMeServices)
|
||
);
|
||
return http.build();
|
||
}
|
||
|
||
@Bean
|
||
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
|
||
RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256;
|
||
TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm);
|
||
// 修改编码算法
|
||
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5);
|
||
return rememberMe;
|
||
}
|
||
```
|
||
|
||
为了启用rememberMe,需要在应用上下文中包含如下bean:
|
||
```java
|
||
@Bean
|
||
RememberMeAuthenticationFilter rememberMeFilter() {
|
||
RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter();
|
||
rememberMeFilter.setRememberMeServices(rememberMeServices());
|
||
rememberMeFilter.setAuthenticationManager(theAuthenticationManager);
|
||
return rememberMeFilter;
|
||
}
|
||
|
||
@Bean
|
||
TokenBasedRememberMeServices rememberMeServices() {
|
||
TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices();
|
||
rememberMeServices.setUserDetailsService(myUserDetailsService);
|
||
rememberMeServices.setKey("springRocks");
|
||
return rememberMeServices;
|
||
}
|
||
|
||
@Bean
|
||
RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
|
||
RememberMeAuthenticationProvider rememberMeAuthenticationProvider = new RememberMeAuthenticationProvider();
|
||
rememberMeAuthenticationProvider.setKey("springRocks");
|
||
return rememberMeAuthenticationProvider;
|
||
}
|
||
```
|
||
## 匿名身份认证
|
||
Spring Security中,采用了`deny-by-default`策略,用以适配如下场景:除少数资源外(如homepage、login页面等),访问其他所有的资源都需要进行身份认证。
|
||
|
||
Spring Security提供了匿名身份认证,对于“匿名身份认证”的用户,其实际和未经认证的用户没有任何区别。匿名认证只是为配置访问控制属性提供了更加便捷的方式,在匿名认证时,即使当前`SecurityContextHolder`中存在`anonymous authentication`对象,在调用一些servlet api例如`getCallerPrincipal`方法时,方法仍然会返回null。
|
||
|
||
在使用anonymous authentication时,Spring Security的SecurityContextHolder一定含有Authentication对象,即使用户未经认证,Authentication对象也不可能为空。
|
||
|
||
### 匿名认证配置
|
||
匿名认证在Spring Security中是默认被提供的,可以对其进行自定义或者关闭。
|
||
|
||
为了提供匿名认证特性,需要存在如下三个类:`AnonymousAuthenticationToken`、`AnonymousAuthenticationProvider`、`AnonymousAuthenticationFilter`。
|
||
- `AnonymousAuthenticationToken`:该类是`Authentication`的实现类,并且存储了该匿名实体所拥有的权限`GrantedAuthority`
|
||
- `AnonymousAuthenticationProvider`:该类被整合到`ProviderManager`的provider链中,故而`AnonymousAuthenticationToken`可以被支持进行处理
|
||
- `AnonymousAuthenticationFilter`:该filter被嵌入到常规的认证机制之后,如果在执行到该filter时,当前SecurityContextHolder中不存在Authentication,那么其会自动将`AnonymousAuthenticationToken`加入到SecurityContextHolder中。
|
||
|
||
定义filter和provider的方式如下所示,filter和provider共享相同的key,故而filter创建的token可以被provider接收:
|
||
```java
|
||
<bean id="anonymousAuthFilter"
|
||
class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
|
||
<property name="key" value="foobar"/>
|
||
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
|
||
</bean>
|
||
|
||
<bean id="anonymousAuthenticationProvider"
|
||
class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
|
||
<property name="key" value="foobar"/>
|
||
</bean>
|
||
```
|
||
|
||
`userAttribute`属性值,其格式为`usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]`。
|
||
|
||
|
||
## 处理logout
|
||
如果当前项目的类路径中包含`spring-boot-starter-security`依赖,那么spring security会自动加入logout支持,针对`GET`和`POST`的`/logout`请求都会响应。
|
||
|
||
当通过get方式请求/logout时,spring security将展示一个登出页面。但如果csrf保护被关闭,那么不会展示登出确认页面,而是直接关闭。
|
||
|
||
当请求/logout时,将通过一系列logoutHandler来实现如下逻辑:
|
||
1. 将http session标记为失效(SecurityContextLogoutHandler)
|
||
2. 清空SecurityContextHolderStrategy (SecurityContextLogoutHandler)
|
||
3. 清空SecurityContextRepository(SecurityContextLogoutHandler)
|
||
4. 清空所有rememberMe authentication(TokenRememberMeServices / PersistentTokenRememberMeServices)
|
||
5. 清空任何csrf token (LogoutSuccessEventPublishingLogoutHandler)
|
||
6. 发送LogoutSuccessEvent(LogoutSuccessEventPublishingLogoutHandler)
|
||
|
||
### 自定义登出url
|
||
在filter chain中,logoutFilter位于AuthorizationFilter之前,故而没有必要显式的对/logout url执行permit操作(因为尚未执行到AuthorizationFilter之前,就会通过logoutFilter执行登出操作)。
|
||
|
||
但是,对于自定义的登出端口(默认情况下,logooutFilter只对/logout的请求进行处理),若url不为/logout,那么需要调用`permitAll`来让自定义登出端口可访问。
|
||
|
||
如果只是要修改spring security登出的url,可以通过如下方式进行修改,且不需要其他任何修改,其单单只是改变了logoutFilter匹配的url:
|
||
```java
|
||
http
|
||
.logout((logout) -> logout.logoutUrl("/my/logout/uri"))
|
||
```
|
||
|
||
但是,如果自定义了logout success endpoint(通过spring mvc建立的controller endpoint),那么需要为自定义的logout endpoint执行permit操作来允许用户访问。因为只有当spring security中的操作都完成后,才会指定自定义的endpoint操作。
|
||
|
||
可以通过如下方式为自定义的登出endpoint进行授权:
|
||
```java
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.requestMatchers("/my/success/endpoint").permitAll()
|
||
// ...
|
||
)
|
||
.logout((logout) -> logout.logoutSuccessUrl("/my/success/endpoint"))
|
||
```
|
||
> logoutSuccessUrl为logout操作执行成功后,重定向到的url,该值默认为`/login?logout`。
|
||
>
|
||
> logoutUrl则是触发logout操作的url。
|
||
|
||
## Authorization
|
||
### 结构
|
||
通过`AuthenticationManager`,`GrantedAuthority`对象集合被插入到`Authentication`对象中,该集合代表被授予给当前主体的权限。插入到Authentication中的`GrantedAuthority`集合将会后续被`AccessDecisionManager`用于判断当前主体是否拥有访问权限。
|
||
|
||
`GrantedAuthority`接口中只有一个方法:
|
||
```java
|
||
String getAuthority();
|
||
```
|
||
该方法只会被`AuthorizationManager`使用,通过该方法可以获取代表该权限的String。如果`GrantedAuthority`无法通过一个String精确的描述,那么该GrantedAuthority将被视为`复杂的`,并且该GrantedAuthority的`getAuthority`方法必须返回null。
|
||
|
||
如下展示了一个复杂权限的示例:
|
||
|
||
一个`GrantedAuthority`中存储了多个客户的权限信息,每个客户可以进行的操作和可以访问资源的权限都不相同。对于多个客户的权限信息,很难勇单个String来表示,故而,在`复杂权限`的实现里,`getAuthority`方法必须返回为null。
|
||
|
||
如果GrantedAuthority实现的getAuthority方法返回为null,这将提示AuthorizationManager需要支持该特定`GrantedAuthority`实现类的解析,从而才能读取authority实现中的内容。
|
||
|
||
Spring Security中包含一个`GrantedAuthority`的实现:`SimpleGrantedAuthority`。该实现能够让任何字符串转化为对应的GrantedAuthority。并且,在Spring Security中所有的AuthenticationProvider,都使用SimpleGrantedAuthority来注入权限到Authentication对象中。
|
||
|
||
默认情况下,基于角色的权限管理都以`ROLE_`作为代表权限字符串的开头,故而如果当前存在Authorization Rule要求security context中拥有`USER`权限,那么Spring Security中是否包含`getAuthority`返回为`ROLE_USER`的GrantedAuthority对象。
|
||
|
||
如果想要自定义基于角色的权限管理的前缀,可以通过`GrantedAuthorityDefaults`来进行自定义。通过注册`GrantedAuthorityDefaults`类型的bean对象,可以进行自定义:
|
||
```java
|
||
@Bean
|
||
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||
return new GrantedAuthorityDefaults("MYPREFIX_");
|
||
}
|
||
```
|
||
#### Invocation Handling
|
||
Spring Security通过拦截器保证了对受保护资源的访问,对于method invocation和web request都能进行拦截。AuthorizationManager既会在调用发生前判断当前是否存在调权限,也会在调用后判断是否调用结果允许被返回。
|
||
|
||
AuthorizationManager对AccessDecisionManager和AccessDecisionVoter两者都进行了取代。
|
||
|
||
AuthorizationManager被spring security的基于基于method组件、基于request组件、基于message组件调用,并且AuthorizationManager负责做最后的权限控制决定。AuthorizationManager包含有如下两个方法:
|
||
```java
|
||
AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);
|
||
|
||
default AuthorizationDecision verify(Supplier<Authentication> authentication, Object secureObject)
|
||
throws AccessDeniedException {
|
||
// ...
|
||
}
|
||
```
|
||
在调用`AuthorizationManager#check`方法时,会向其中传入为做出访问决定所需的所有相关信息。特别是传递了secure object,这令实际secure object调用中所有的参数都能够在check方法中被观测到。
|
||
|
||
例如,假定该secure object是`MethodInvocation`,通过MethodInvocation可以方便的访问调用方法和调用参数,并且在check方法中实现一系列逻辑判断当前主体是否拥有权限执行操作。
|
||
|
||
check方法会返回AuthorizationDecision类型的返回值,如果允许当前访问,应该返回一个positive AuthorizationDecision;如果拒绝当前访问,应该返回一个negative AuthorizationDecision;如果AuthorizationManager放弃做决定,则应该返回null。
|
||
|
||
`AuthorizationManager#verify`方法会调用check方法,若check方法返回negative AuthorizationDecision,则verify方法会抛出AccessDeniedException。
|
||
|
||
#### 基于委托的AuthorizationManager实现
|
||
Spring Security设计了一个委托AuthorizationManager,其可以和开发者自己实现的复数个AuthorizationManager协作。
|
||
|
||
`RequestMatcherDelegatingAuthorizationManager`将会为当前请求匹配最适合的AuthorizationManager,并将任务委托给匹配到的AuthorizationManager。对于基于方法的权限认证,可以使用`AuthorizationManagerBeforeMethodInterceptor`和`AuthorizationManagerAfterMethodInterceptor`。
|
||
|
||
通过上述的委托机制,可以将多个AuthenticationManager组合起来,并通过轮询来做Authorization decision。
|
||
|
||
##### AuthorityAuthorizationManager
|
||
`AuthorityAuthorizationManager`是spring security提供的最通用的`AuthorizationManager`。
|
||
|
||
AuthorityAuthorizationManager构造方法接收一系列的权限字符串,并且在对Authentication进行校验时,会校验构造函数中接收的权限字符串集合,其中是否有一个权限字符串在`authentication.getAuthorities`中存在,如果任一权限字符串存在,那么权限校验通过,否则权限校验失败。
|
||
|
||
##### AuthenticatedAuthorizationManager
|
||
AuthenticatedAuthorizationManager可以用于区分anonymous、fully-authenticated、remember-me请求。这可以用于如下场景:通过remember-me登录的用户只能够执行有限的操作,但是通过full-authenticated登录的用户可以执行所有的操作。
|
||
|
||
##### AuthorizationManagers
|
||
`AuthorizationManagers`类中包含静态工厂方法来将多个单独的AuthorizationManager整合在一起,构成更加复杂的表达式。
|
||
|
||
##### 自定义AuthorizationManager
|
||
可以通过实现AuthorizationManager接口来实现任何逻辑的实现类。
|
||
|
||
#### ROLE继承
|
||
通过角色继承,可以配置某些角色包含其他角色,可执行其他角色能做的所有操作。
|
||
|
||
`RoleHierarchyVoter`作为`RoleVoter`的拓展版本,可以配置`RoleHierarchy`:
|
||
|
||
```java
|
||
@Bean
|
||
static RoleHierarchy roleHierarchy() {
|
||
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
|
||
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" +
|
||
"ROLE_STAFF > ROLE_USER\n" +
|
||
"ROLE_USER > ROLE_GUEST");
|
||
return hierarchy;
|
||
}
|
||
|
||
// and, if using method security also add
|
||
@Bean
|
||
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
|
||
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
|
||
expressionHandler.setRoleHierarchy(roleHierarchy);
|
||
return expressionHandler;
|
||
}
|
||
```
|
||
在上述示例中,具有`ADMIN => STAFF => USER => GUEST`的继承关系。一个拥有`ROLE_ADMIN`角色的用户将自动拥有其他角色的权限,其中`>`符号代表包含关系。
|
||
|
||
### 基于HttpServletRequest的权限认证
|
||
spring security允许基于request的级别构建认证模型。例如,通过spring security可以构建如下权限认证模型:所有`/admin`下的资源访问都需要特定的权限,而其他的资源访问则只需要身份认证通过即可。
|
||
|
||
默认情况下,spring security需要每个请求都必须通过身份认证。故而,在每次构建HttpSecurity实例时,必须都要声明`authorization rules`。
|
||
|
||
每次在声明HttpSecurity时,都至少需要指定如下`authorization rules`,除此之外还可以指定更加复杂的`auhorization rules`:
|
||
```java
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.anyRequest().authenticated()
|
||
)
|
||
```
|
||
上述配置会指定在访问任何endpoint时,至少都需要当前用户通过身份认证。
|
||
|
||
#### 基于Request的Authorization Component实现
|
||
1. 首先,AuthorizationFilter会构建一个`Supplier<Authenticaiton>`,该Supplier将会从SecurityContextHolder中获取Authentication独享
|
||
2. AuthorizationFilter将会把`Supplier<Authentication>`和`HttpServletRequest`传递给`AuthorizationManager`。`AuthorizationManager`将会在HttpSecurity的authorizeHttpRequests指定的authorization rules中匹配到的rule并执行。
|
||
1. 如果当前鉴权操作被拒绝,AuthorizationDeniedEvent事件将会被发布,并会抛出AccessDeniedException异常,在这种情况下,ExceptionTranslationFilter将会对异常进行处理
|
||
2. 如果当前鉴权校验通过,`AuthorzationGrantedEvent`事件将会被发布,`AuthorizationFilter`将会继续执行filterChain,允许程序正常的继续执行
|
||
|
||
#### AuthorizationFilter默认情况下是SecurityFilterChain中的最后一个filter
|
||
默认情况下,AuthorizationFilter是SecurityFilterChain中的最后一个filter。这意味着spring security中身份认证、漏洞保护和其他filter中的集成功能都不需要经过授权操作。***如果向AuthorizationFilter之前加入自定义filter,那么自定义filter也不需要经过授权校验;如果向AuthorizationFilter之后加入自定义filter,那么在执行自定义filter逻辑之前,需要经过授权校验。***
|
||
|
||
> 在通过spring mvc添加自定义的endpoint时,需要明确controller method是在AuthorizationFilter之后才执行的,在执行endpoint的逻辑之前,需要保证该`authorizeHttpRequests`中定义的rules中,对endpoint的访问应该被授权。
|
||
|
||
#### Authentication查询是延迟的
|
||
`AuthorizationManager` api使用了`Supplier<Authentication>`,故而在`authorizeHttpRequests`时,如果所有请求都被允许或所有请求都被拒绝的情况下,并不会去查询Authentication,这将会令请求处理得更快。
|
||
|
||
#### authorizing endpoint
|
||
当为spring security按顺序配置了多个rules时,如果想要/endpoint只能够被拥有`USER`权限的用户访问,可以通过如下配置来指定:
|
||
```java
|
||
@Bean
|
||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.requestMatchers("/endpoint").hasAuthority("USER")
|
||
.anyRequest().authenticated()
|
||
)
|
||
// ...
|
||
|
||
return http.build();
|
||
}
|
||
```
|
||
如上所示,对rules的配置可以拆分成多个`pattern/rule`集合。
|
||
|
||
`AuthorizationFilter`将会按照顺序对这些`pattern/rule`进行处理,并对request执行匹配到的第一个rule。上述配置即意味着`/endpoint`的访问需要用户拥有USE权限,而其他资源的访问则只需要用户经过身份认证即可。
|
||
|
||
#### 请求匹配的pattern声明
|
||
如果需要声明匹配请求的pattern,可以使用`Ant`或是`regex`两种方式。
|
||
##### Ant
|
||
Ant是Spirng Security默认的请求匹配方式。通过Ant进行匹配的示例如下:
|
||
```java
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.requestMatchers("/resource/**").hasAuthority("USER")
|
||
.anyRequest().authenticated()
|
||
)
|
||
```
|
||
|
||
通过Ant,可以声明占位符并且在匹配时进行捕获,以供后续使用,示例如下:
|
||
```java
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.requestMatchers("/resource/{name}").access(new WebExpressionAuthorizationManager("#name == authentication.name"))
|
||
.anyRequest().authenticated()
|
||
)
|
||
```
|
||
上述请求匹配中,name占位符对应的值被捕获,并通过捕获的值来判断用户是否拥有访问权限。
|
||
|
||
##### regex
|
||
除了通过Ant来进行匹配外,spring security还支持通过regex来对请求进行匹配。
|
||
|
||
如下展示了一个通过regex来进行请求匹配的示例,匹配的是请求地址中包含username,并且username完全由字母和数字组成的请求:
|
||
```java
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.requestMatchers(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+")).hasAuthority("USER")
|
||
.anyRequest().denyAll()
|
||
)
|
||
```
|
||
|
||
##### 通过Http Method来进行匹配
|
||
spring security中的rules匹配还支持通过Http method来进行匹配。
|
||
|
||
如下示例代表所有GET请求都需要拥有read权限,所有的POST请求都需要拥有write权限:
|
||
```java
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.requestMatchers(HttpMethod.GET).hasAuthority("read")
|
||
.requestMatchers(HttpMethod.POST).hasAuthority("write")
|
||
.anyRequest().denyAll()
|
||
)
|
||
```
|
||
|
||
##### 自定义Matcher来进行匹配
|
||
如下示例定义了通过自定义Matcher的匹配:
|
||
```java
|
||
RequestMatcher printview = (request) -> request.getParameter("print") != null;
|
||
http
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.requestMatchers(printview).hasAuthority("print")
|
||
.anyRequest().authenticated()
|
||
)
|
||
```
|
||
|
||
#### Authorizing Request
|
||
一旦请求匹配了特定的rule,可以通过如下方式来对请求执行鉴权逻辑:
|
||
- `permitAll`:该请求无需经过权限校验,请求访问的是public endpoint,在这种情况下,Authentication并不会从session中被获取
|
||
- `denyAll`:该请求在任何场景下都是不允许的,在这种情况下,也不会从session中获取Authentication
|
||
- `hasAuthority`:该操作需要请求对应的`Authenticaiton`对象所拥有的`GrantedAuthority`集合中,存在指定的权限
|
||
- `hasRole`:类似于`hasAuthority`,但是在要求的请求中需要补充`ROLE_`前缀,`hasRole("XXX")`等价于`hasAuthority("ROLE_XXX")`
|
||
- `hasAnyAuthority`:要求请求拥有指定权限集合中的任意一个
|
||
- `hasRole`:拥有指定角色中的任意一个
|
||
- `access`:该请求将会使用自定义的AuthorityManager来判断是否拥有权限来进行访问
|
||
|
||
如下是一个较为复杂的权限认证示例:
|
||
```java
|
||
import static jakarta.servlet.DispatcherType.*;
|
||
|
||
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
|
||
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
|
||
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
|
||
|
||
@Bean
|
||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||
http
|
||
// ...
|
||
.authorizeHttpRequests(authorize -> authorize //(1)
|
||
.dispatcherTypeMatchers(FORWARD, ERROR).permitAll() //(2)
|
||
.requestMatchers("/static/**", "/signup", "/about").permitAll() //(3)
|
||
.requestMatchers("/admin/**").hasRole("ADMIN") //(4)
|
||
.requestMatchers("/db/**").access(allOf(hasAuthority("db"), hasRole("ADMIN"))) (5)
|
||
.anyRequest().denyAll() //(6)
|
||
);
|
||
|
||
return http.build();
|
||
}
|
||
```
|
||
|
||
#### 通过Spel表达式来执行权限认证
|
||
spring security将其所有authorization字段和方法封装在一个root object中,最通用的root object即是`SecurityExpressionRoot`,其是`WebSecurityExpressionRoot`的父类。spring security在执行authorization expression时,将root object应用为`StandardEvaluationContext`。
|
||
|
||
##### 使用Authorization表达式
|
||
root object提供的常用方法如下:
|
||
- `permitAll`
|
||
- `denyAll`
|
||
- `hasAuthority`
|
||
- `hasRole`
|
||
- `hasAnyAuthority`
|
||
- `hasAnyRole`
|
||
- `hasPermission`
|
||
|
||
root object中提供的常用fields如下:
|
||
- `authentication`:当前关联的Authentidcation对象
|
||
- `principal`:当前`Authentication#getPrincipal`调用返回的值
|
||
|
||
如下示例通过spel指定了认证url
|
||
```java
|
||
<http>
|
||
<intercept-url pattern="/static/**" access="permitAll"/>
|
||
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/>
|
||
<intercept-url pattern="/db/**" access="hasAuthority('db') and hasRole('ADMIN')"/>
|
||
<intercept-url pattern="/**" access="denyAll"/>
|
||
</http>
|
||
```
|
||
|
||
##### 使用path变量
|
||
如下实例为url指定了path变量,并且通过spel使用了path变量的值:
|
||
```java
|
||
<http>
|
||
<intercept-url pattern="/resource/{name}" access="#name == authentication.name"/>
|
||
<intercept-url pattern="/**" access="authenticated"/>
|
||
</http>
|
||
```
|
||
|
||
##### 使用数据库来进行权限认证
|
||
如果想要使用单独的微服务实例来执行授权操作,可以创建自定义的AuthorizationManager,并且将rule指定为anyRequest。
|
||
|
||
定义的自定义AuthorizationManager示例如下所示:
|
||
```java
|
||
@Component
|
||
public final class OpenPolicyAgentAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
|
||
@Override
|
||
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
|
||
// make request to Open Policy Agent
|
||
}
|
||
}
|
||
```
|
||
之后,可以通过HttpSecurity配置rule对应的authorizationManager:
|
||
```java
|
||
@Bean
|
||
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz) throws Exception {
|
||
http
|
||
// ...
|
||
.authorizeHttpRequests((authorize) -> authorize
|
||
.anyRequest().access(authz)
|
||
);
|
||
|
||
return http.build();
|
||
}
|
||
```
|
||
|
||
#### SecurityMatcher
|
||
类似于RequestMatcher,SecurityMatcher用于决定HttpSecurity是否应该被用于该请求,使用示例如下:
|
||
```java
|
||
@Configuration
|
||
@EnableWebSecurity
|
||
public class SecurityConfig {
|
||
|
||
@Bean
|
||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||
http
|
||
.securityMatcher("/api/**")
|
||
.authorizeHttpRequests(authorize -> authorize
|
||
.requestMatchers("/user/**").hasRole("USER")
|
||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||
.anyRequest().authenticated()
|
||
)
|
||
.formLogin(withDefaults());
|
||
return http.build();
|
||
}
|
||
}
|
||
```
|
||
|