授权
以我自己的学习习惯,我喜欢先学会怎么用再去学习深一些,所以我先借助示例来学会怎么用,然后再借助文档中的架构和更详细的内容来深入和补充
先是基本的概念
SpringSecurity的高级授权能力是其受欢迎的原因之一,它能通过简单的配置实现对不同role授予不同的访问权限,而授权的配置方式可以通过俩种,一种是通过uri来配置,这种需要再security的配置文件中配置,一种是方法上的配置,简单说就是加个注解,但是底层还是比使用要复杂很多的,我打算先学会用再去学原理
基于之前的认证的学习使用的配置的示例,该如何去使用SpringSecurity的授权功能呢
授权Authorization是在认证Authentication后进行的,授权的配置主要依赖SpringSecurity Filter Chain中的authorizeHttpRequests部分,以及认证过程中设置到Authentication对象中的权限消息Authorities,这个Authorities通常是角色或者更细粒度的表示
授权判断就是检查当前用户的Authentication对象中的Authorities是否满足访问某个资源所需要的权限需求
配置授权的方式有俩种,一种是基于请求URI的配置,需要在SpringSecurity的配置文件config上配置。
还有一种是基于方法也就是基于特定接口做的授权规则,先来看看这俩种分别如何配置,再去研究他们是如何工作的
基于请求URI配置
简单说如何配置的话就是直接在SecurityFilterChain上通过authorizeHttpRequests来配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register").permitAll() // 登录注册公开访问
// 只有拥有 ADMIN 角色的用户才能访问 /admin/** 下的资源
.requestMatchers("/admin/**").hasRole("ADMIN")
// 拥有 ADMIN 或 EDITOR 任意一个角色的用户才能访问 /articles/edit/**
.requestMatchers("/articles/edit/**").hasAnyRole("ADMIN", "EDITOR")
// 拥有某个细粒度权限(Authority)的用户才能访问,例如需要 "read:users" 权限
.requestMatchers("/api/users").hasAuthority("read:users")
// 拥有 "write:articles" 权限或 ADMIN 角色的用户才能访问 /articles/new
.requestMatchers("/articles/new").access("hasAuthority('write:articles') or hasRole('ADMIN')")
.anyRequest().authenticated() // 其他所有请求必须已认证(即 SecurityContextHolder 中有 Authentication 对象,不管权限是什么)
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
还有更简单的例子
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
这个例子简单说就是直接对所有的http请求都要求进行认证
进一步地,讲一下匹配规则
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resource/**").hasAuthority("USER")
.anyRequest().authenticated()
)
还有就是带有路径参数的
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resource/{name}").access(new WebExpressionAuthorizationManager("#name == authentication.name"))
.anyRequest().authenticated()
)//路径参数name要等于context的Authentication实例的name
如何进行测试呢
@WithMockUser(authorities="USER")//授予了User的role权限进行测试
@Test
void endpointWhenUserAuthorityThenAuthorized() {
this.mvc.perform(get("/endpoint/jon"))
.andExpect(status().isOk());
}
@WithMockUser
@Test
void endpointWhenNotUserAuthorityThenForbidden() {
this.mvc.perform(get("/endpoint/jon"))
.andExpect(status().isForbidden());
}
@Test
void anyWhenUnauthenticatedThenUnauthorized() {
this.mvc.perform(get("/any"))
.andExpect(status().isUnauthorized())
}
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+")).hasAuthority("USER")
.anyRequest().denyAll()
)
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(HttpMethod.GET).hasAuthority("read")
.requestMatchers(HttpMethod.POST).hasAuthority("write")
.anyRequest().denyAll()
)
根据Dispatcher Type来限定
先讲一下什么是Dispatcher Type
Dispatcher Type就是分发类型
例如在JavaWeb中,一个请求可以被不同方式分发到Servlet或Controller
例如
直接访问,被Servlet或者Controller访问,被错误页面机制分发
由于SpringSecurity默认只对用户直接发起的请求进行授权控制,所有要在dispatcherTypeMatchers来进行配置
http
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/endpoint").permitAll()
.anyRequest().denyAll()
)
自定义Matcher
RequestMatcher printview = (request) -> request.getParameter("print") != null;
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(printview).hasAuthority("print")
.anyRequest().authenticated()
)
常见的授权表达式的字段和方法
permitAll - 该请求不需要授权即可调用;注意,在这种情况下,将不会从 session 中检索 Authentication。
denyAll - 该请求在任何情况下都是不允许的;注意在这种情况下,永远不会从会话中检索 Authentication。
hasAuthority - 请求要求 Authentication 的 GrantedAuthority 符合给定值。
hasRole - hasAuthority 的快捷方式,前缀为 ROLE_ 或任何配置为默认前缀的内容。
hasAnyAuthority - 请求要 Authentication 具有符合任何给定值的 GrantedAuthority。
hasAnyRole - hasAnyAuthority 的一个快捷方式,其前缀为 ROLE_ 或任何被配置为默认的前缀。
hasPermission - 用于对象级授权的 PermissionEvaluator 实例的 hook。
这里是对最常见字段的简要介绍:
authentication - 与此方法调用相关的 Authentication 实例。
principal - 与此方法调用相关的 Authentication#getPrincipal。
SecurtiyMather
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // ← 只处理 /api/** 下的请求
.authorizeHttpRequests(...) // ← 配置授权规则
.addFilterBefore(...) // ← 添加自定义过滤器
.formLogin(...); // ← 登录方式配置
return http.build();
}
@Bean
public SecurityFilterChain publicSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/public/**") // ← 只处理 /public/** 下的请求
.authorizeHttpRequests(...)
.permitAll(); // ← 所有人都可以访问
return http.build();
}
Request授权组件时如何工作的
使用AuthorizationFilter构造一个Supplier,从SecurityContextHolder中检索一个Authentication
其中Supplier可以看作一个用来延迟加载Authentication的供应商,需要时再取出来
使用Supplier的原因是
- 请求在过滤器链中可能会被转发(forward)、包含(include)多次
- 有些情况可能还没有完全解析完身份(如异步请求)
- 使用
Supplier
可以保证每次获取的是最新的身份信息
将Supplier<Authentication>和HttpServletRequest传递给AuthorizationManager,AuthorizationManager将请求与authorizationHttpRequests中的模式匹配
如果授权被拒绝,会发布一个 AuthorizationDeniedEvent,并抛出一个 AccessDeniedException
。在这种情况下,ExceptionTranslationFilter 会处理 AccessDeniedException
。
如果访问被授权,就会 发布一个AuthorizationGrantedEvent,AuthorizationFilter
继续进行 FilterChain,允许应用程序正常处理
那么如何基于方法安全呢(Method Security)
要想启动方法级别首先需要在SpringSecurity的配置类中启动
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
// ... 其他导入
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用方法级别安全
// @EnableGlobalMethodSecurity // 旧版本可能使用这个注解
// ... 其他注解
public class SecurityConfig {
// ... 其他 Bean 和配置 ...
}
启动之后有多种使用的注解
1.PreAuthorize
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')") // 只有拥有 ADMIN 角色的用户才能调用此方法
public List<User> getAllUsers() {
// ... 获取所有用户逻辑 ...
}
@PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')") // 拥有 ADMIN 或 EDITOR 任意一个角色即可
public void createPost(Post post) {
// ... 创建文章逻辑 ...
}
@PreAuthorize("hasAuthority('delete:user')") // 拥有 'delete:user' 权限的用户才能调用
public void deleteUser(Long userId) {
// ... 删除用户逻辑 ...
}
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')") // 用户只能查看自己的信息,或者 ADMIN 可以查看所有用户
public User getUserById(Long userId) {
// ... 获取指定ID用户逻辑 ...
}
}
PreAuthorize是在方法执行之前进行授权
2.PostAuthorize
在方法执行之后进行授权检查,可以访问方法的返回值 (returnObject
)。不太常用,因为它在方法执行后才检查,如果方法有副作用(比如删除了数据),即使授权失败也无法撤销。主要用于检查用户是否有权查看返回的结果。
@PostAuthorize("returnObject.owner == authentication.principal.username or hasRole('ADMIN')") // 返回的文章只有作者本人或 ADMIN 可见
public Post getPostById(Long postId) {
// ... 获取文章逻辑 ...
// 返回 Post 对象
}
3.Secured,RolesAllowed
@Secured({"ROLE_ADMIN", "ROLE_USER"})
:一个更简单的注解,只能检查指定的角色或GrantedAuthority
名称。不如@PreAuthorize
灵活,不能使用 SpEL 表达式。需要@EnableGlobalMethodSecurity(securedEnabled = true)
。@RolesAllowed({"ADMIN", "USER"})
:JSR 250 标准的注解,功能类似@Secured
,也是检查角色。角色名不需要加 "ROLE_" 前缀(Spring Security 会自动处理)。需要@EnableGlobalMethodSecurity(jsr250Enabled = true)
方法安全工作原理
Method Security是基于SpringAOP构建的,可以根据实际要求修改默认配置
PreAuthorize是检查入门资格,而PostAuthorize是检查返回值是否符合一定要求
例如
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
这里的PreAuthorize是判断role的要求
而PostAuthorize是判断return的Object的owner属性是否符合要求
内部的整体流程是
这个图描绘得非常清晰
首先是使用了SpringAOP为readCustomer实现了Pre和Post俩个Authrized?过程
在进行readCustomer之前,调用AuthorizationManagerBeforeMethodInterceptor,这个拦截器又会调用PreAuthorizeAuthorizationManager去检查,然后它又会使用MethodSecurityExpressionHandler去解析SpEL表达式, 并从包含 Supplier<Authentication> 和 MethodInvocation 的 MethodSecurityExpressionRoot 构建相应的 EvaluationContext。"
根据评估,如果通过就调用方法,否者就会发布一个AuthorizationDeniedEvent,并抛出AccessDeniedException,然后ExceptionTranslationFilter会捕获并响应一个403状态码
Post的过程也是相似的,这里就不说了