From df5101160b3fb15f8ac956d54ab3a2be6ced2ee1 Mon Sep 17 00:00:00 2001 From: asahi Date: Fri, 22 Mar 2024 21:04:16 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=85=E8=AF=BBspring=20security=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spring/Spring Security/Spring Security.md | 225 ++++++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/spring/Spring Security/Spring Security.md b/spring/Spring Security/Spring Security.md index b79b707..492dce0 100644 --- a/spring/Spring Security/Spring Security.md +++ b/spring/Spring Security/Spring Security.md @@ -51,6 +51,22 @@ - [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 @@ -911,18 +927,227 @@ static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHiera ``` 在上述示例中,具有`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`,该Supplier将会从SecurityContextHolder中获取Authentication独享 +2. AuthorizationFilter将会把`Supplier`和`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`,故而在`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 + + + + + + +``` + +##### 使用path变量 +如下实例为url指定了path变量,并且通过spel使用了path变量的值: +```java + + + + +``` + +##### 使用数据库来进行权限认证 +如果想要使用单独的微服务实例来执行授权操作,可以创建自定义的AuthorizationManager,并且将rule指定为anyRequest。 + +定义的自定义AuthorizationManager示例如下所示: +```java +@Component +public final class OpenPolicyAgentAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext context) { + // make request to Open Policy Agent + } +} +``` +之后,可以通过HttpSecurity配置rule对应的authorizationManager: +```java +@Bean +SecurityFilterChain web(HttpSecurity http, AuthorizationManager 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(); + } +} +```