Spring Security - Authorization
梳理 Spring Security Authorization 的权限表示、方法鉴权、角色层级和 AuthorizationFilter。
Spring Security 主要解决三个问题:
- Authentication:认证。谁在访问;
- Authorization:权限。某用户能不能访问;
- Protection:防止一些常见的攻击;
Authentication 的验证方式可以很多,但目标都是一致的:得到一个可信的Authentication。更准确地说,用户名和密码只是认证前的凭据,认证成功后真正要长期使用的是Authentication里的 authorities。有了这些权限信息,Spring Security 才能对 URL 或方法执行鉴权。
文章的主线是:认证负责把权限放进Authentication,鉴权负责取出这些权限并判断当前资源能不能访问。
鉴权流程可以压缩成一句话:从SecurityContextHolder拿当前用户权限,再和 URL 或方法上的规则比对。
flowchart TD
Request["HTTP 请求 / 方法调用"] --> Context["SecurityContextHolder"]
Context --> Auth["Authentication"]
Auth --> Authorities["GrantedAuthority 集合"]
Request --> Rule{"访问规则来自哪里?"}
Rule --> Url["URL 规则<br/>authorizeHttpRequests"]
Rule --> Method["方法规则<br/>@PreAuthorize"]
Url --> Manager["AuthorizationManager"]
Method --> Manager
Authorities --> Manager
Manager --> Decision{"AuthorizationDecision"}
Decision -->|"granted=true"| Allow["继续执行"]
Decision -->|"granted=false"| Deny["拒绝访问<br/>401 / 403"]
classDef data fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
classDef rule fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
classDef decision fill:#fff3bf,stroke:#f9a825,color:#7a4f00
classDef deny fill:#ffe3e3,stroke:#c62828,color:#7f1d1d
class Request,Context,Auth,Authorities data
class Rule,Url,Method,Manager rule
class Decision decision
class Deny deny
Authorization 中的核心概念
Authorization的主要概念大概有三部分:
- 怎么表示权限;
- 怎么设置权限;
- 谁来鉴权;
何谓权限
权限包含在Authentication里(所以由AuthenticationManager在设置Authentication的时候设置):
1
Collection<? extends GrantedAuthority> getAuthorities();
而GrantedAuthority就是简单地用string代表权限:
1
String getAuthority();
所以说白了,权限就是string。
在Spring Security - Authentication中介绍了一个很重要的概念:role 和普通 authority 没有本质区别,role 是从 authority 里分出来的约定概念。谈及 role 时,它默认会加上ROLE_前缀。比如设置一个 role pikachu,实际相当于添加了一个ROLE_pikachu authority。
权限配置
配置权限的方式多种多样,给方法配置权限最常使用的是 SpEL:@PreAuthorize("hasAnyAuthority('admin', 'pikachu')")。
方法要求的权限来自@PreAuthorize注解。当前用户实际拥有的权限从哪里取?从SecurityContextHolder里的Authentication取。Spring Security - 架构已经介绍过,SecurityContextHolder默认使用ThreadLocal存储,所以同一请求线程中全局可获取:
1
2
3
4
5
6
7
8
static final Supplier<Authentication> AUTHENTICATION_SUPPLIER = () -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
};
最后,spring security注解相关的权限配置需要手动开启,使用@EnableGlobalMethodSecurity或更新的@EnableMethodSecurity。
谁来鉴权 - AuthorizationManager
AccessDecisionManager会获取Authentication里的权限,用于鉴权。spring更推荐使用AuthorizationManager(主要应该是它支持泛型,待判断的对象不必用object表示)。感觉语义上和AccessDecisionManager差不太多,都是返回是否鉴权通过。
接口很简单,返回一个鉴权结果AuthorizationDecision:
1
2
3
4
5
6
7
8
/**
* Determines if access is granted for a specific authentication and object.
* @param authentication the {@link Supplier} of the {@link Authentication} to check
* @param object the {@link T} object to check
* @return an {@link AuthorizationDecision} or null if no decision could be made
*/
@Nullable
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
AuthorizationManager有很多实现,RequestMatcherDelegatingAuthorizationManager是其中一个,用来判断一个http请求是否符合待校验的权限。它也只是一个代理实现,实际会委托给一堆AuthorizationManager,每个AuthorizationManager绑定一个RequestMatcher,如果当前http请求符合matcher,就使用这个AuthorizationManager鉴权。
常用的配置权限的方式是@PreAuthorize,所以spring security使用spring aop创建了两个method增强:
AuthorizationManagerBeforeMethodInterceptorAuthorizationManagerAfterMethodInterceptor
提供对@PreAuthorize注解的支持。
其实就是调用方法之前先使用AuthorizationManager校验一下@PreAuthorize注解的那个方法的权限和http里的权限是否符合。这里用到的鉴权manager是PreAuthorizeAuthorizationManager,它提供了对“方法相关的鉴权”的支持(public final class PreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation>)
层级权限 role hierarchy
权限一般存在层级概念,比如admin一般都有普通user的权限。为了让user权限的方法也支持admin权限,实现的时候,
- 要么所有可以用user权限的方法都标上admin权限(太累了);
- 要么直接给admin赋予user权限,这样所有可以用user权限的方法,admin自动就能访问了;
RoleHierarchy就是为后者准备的。它就一个方法:
1
2
Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities);
传入一个role(就是string),返回一个role数组,代表该角色拥有的所有角色权限。比如:
- 传入admin权限,返回admin、user,代表admin同时拥有user权限;
- 传入user权限,只返回user,代表user权限不包含更低级的权限;
RoleHierarchy的一个实现是RoleHierarchyImpl。允许传入这种格式的role层级:ROLE_A > ROLE_B > ROLE_C(实现的时候用大于号split字符串,解析为token),a包含b和c,b包含c。也可以使用\n拼接字符串,指定多条角色包含关系。
RoleHierarchyImpl对传入的string的处理方式就是:先按照\n把string分成好几条链,再逐一解析合并每一条链。
AuthorizationManager实际使用RoleVoter鉴权。RoleHierarchyVoter是RoleVoter的子类,获取权限的时候,使用RoleHierarchy获取(一串儿)权限。
RoleVoter的权限获取,只能获取权限本身:
1
2
3
Collection<? extends GrantedAuthority> extractAuthorities(Authentication authentication) {
return authentication.getAuthorities();
}
RoleHierarchyVoter的权限获取,可以取得本权限加上所有低层级的权限:
1
2
3
4
@Override
Collection<? extends GrantedAuthority> extractAuthorities(Authentication authentication) {
return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
}
所以我们配置一个自定义的RoleHierarchyVoter就行了。可以写成一条链,也可以像下面一样写成四条链:
1
2
3
4
5
6
7
8
@Bean
AccessDecisionVoter hierarchyVoter() {
RoleHierarchy hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" +
"ROLE_STAFF > ROLE_USER\n" +
"ROLE_USER > ROLE_GUEST");
return new RoleHierarchyVoter(hierarchy);
}
AuthorizationFilter
security filter chain上有一个filter是AuthorizationFilter,它提供了对url权限的校验。可以通过HttpSecurity#authorizeHttpRequests自定义配置修改默认行为:
1
2
3
4
5
6
7
8
9
10
@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated();
)
// ...
return http.build();
}
它决定filter chain要不要执行下去的方式非常简单:就看AuthorizationManager返回的AuthorizationDecision是否通过,通过则继续:
用到的AuthorizationManager是RequestMatcherDelegatingAuthorizationManager。
这个manager我们也可以自己配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> access)
throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().access(access)
)
// ...
return http.build();
}
@Bean
AuthorizationManager<RequestAuthorizationContext> requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
RequestMatcher permitAll =
new AndRequestMatcher(
mvcMatcherBuilder.pattern("/resources/**"),
mvcMatcherBuilder.pattern("/signup"),
mvcMatcherBuilder.pattern("/about"));
RequestMatcher admin = mvcMatcherBuilder.pattern("/admin/**");
RequestMatcher db = mvcMatcherBuilder.pattern("/db/**");
RequestMatcher any = AnyRequestMatcher.INSTANCE;
AuthorizationManager<HttpServletRequest> manager = RequestMatcherDelegatingAuthorizationManager.builder()
.add(permitAll, (context) -> new AuthorizationDecision(true))
.add(admin, AuthorityAuthorizationManager.hasRole("ADMIN"))
.add(db, AuthorityAuthorizationManager.hasRole("DBA"))
.add(any, new AuthenticatedAuthorizationManager())
.build();
return (context) -> manager.check(context.getRequest());
}
感想
鉴权并不关心SecurityContextHolder里的Authentication来自 form、basic、remember-me 还是自定义 token filter。它只关心当前Authentication包含哪些 authorities,以及这些 authorities 是否满足当前资源的访问规则。认证和鉴权的职责边界在这里非常清楚。
