继续接着上

授权管理实例

RBAC

在之前的设计中,我们严格按照了 RBAC 模型设计了三层关系

  • 用户 User:多对多关联角色,也就是用户可以被赋予一个或多个角色

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Entity
    @Table(name = "users")
    ...
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
    name = "user_roles",
    joinColumns = @JoinColumn(name = "user_id"),
    inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
  • 角色 Role:承上启下,连接用户和权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Entity
    @Table(name = "roles")
    ...
    @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
    private Set<User> users = new HashSet<>();

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
    name = "role_permissions",
    joinColumns = @JoinColumn(name = "role_id"),
    inverseJoinColumns = @JoinColumn(name = "permission_id")
    )
    private Set<Permission> permissions = new HashSet<>();
  • 权限 Permission:真正的访问控制单元,实际上,一个权限不只是一个名字,而是 HTTP 动作 + 资源路径 的组合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Column(nullable = false, length = 100)
    private String resource; // 例如 /api/management

    @Column(nullable = false, length = 20)
    private String action; // GET / POST / PUT / DELETE

    public String getAuthority() {
    return action + ":" + resource; // 例如 GET:/api/management
    }

那么串起来整体的授权链路

  • 认证阶段,Spring Security 调用 CustomUserDetailsService 加载用户和其 GrantedAuthority。(Spring Security内置的权限类)

    1
    2
    3
    4
    5
    6
    7
    8
    User user = userRepository.findByUsernameWithRolesAndPermissions(username)
    .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));

    // 角色 -> ROLE_xxx
    authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
    // 权限 -> ACTION:RESOURCE
    String authority = permission.getAuthority(); // e.g. "GET:/api/users"
    authorities.add(new SimpleGrantedAuthority(authority));
    • 用户登录后,Spring Security 里这个用户就带着两类权限:

      • 角色型权限:ROLE_ADMIN, ROLE_USER …(你可以用来做 hasRole(“ADMIN”) 之类的判断)

      • 资源权限:GET:/api/users、POST:/api/role-assignment/** …(用于精确控制 URL 访问)

  • 决策前,元数据源CustomFilterInvocationSecurityMetadataSource 根据当前请求 URL + Method,从 DB 里查出访问这个 URL 需要哪些权限。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    String requestUrl = filterInvocation.getRequestUrl();
    String httpMethod = filterInvocation.getRequest().getMethod();
    ...
    for (Permission permission : allPermissions) {
    if (pathMatcher.match(permission.getResource(), requestUrl) &&
    permission.getAction().equalsIgnoreCase(httpMethod)) {

    String authority = permission.getAuthority(); // ACTION:RESOURCE
    configAttributes.add(new SecurityConfig(authority));
    }
    }
    • 每次请求进来,这里都会遍历 DB 中的 Permission 表,看是否存在某行记录的 (action, resource) 能匹配当前请求。匹配成功就生成一个 ConfigAttribute,内容就是 GET:/api/users 这样的字符串。
    • 如果完全没匹配到,返回 null,表示这个 URL 不需要特殊权限(没有配置则默认放行)。
  • 决策器,AccessDecisionManager是CustomAccessDecisionManager比较「用户拥有的权限」和「资源需要的权限」,决定放行/拒绝。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    for (ConfigAttribute configAttribute : configAttributes) {
    String needAuthority = configAttribute.getAttribute();

    for (GrantedAuthority authority : authorities) {
    if (needAuthority.equals(authority.getAuthority())) {
    // 拥有需要的权限,放行
    return;
    }
    }
    }
    throw new AccessDeniedException("访问被拒绝:权限不足");

    只要用户的 GrantedAuthority 集合里包含任意一个所需的 ConfigAttribute,就允许访问,否则抛 AccessDeniedException,返回 403。

那么即使这么写,也是需要进行配置类的配置的,也就是 URL 授权,因为大多数情况下是两者搭配使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/login", "/logout").permitAll()
.anyRequest().authenticated()
)
.httpBasic(basic -> {})
.formLogin(form -> form
.loginPage("/login").permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.userDetailsService(customUserDetailsService)
.addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class)
.build();
}
  • 最外层规则:

    • /api/public/**/login/logout这种必须开放的出入口这种方法,必须谁都能用,直接 permitAll()

    • 其余所有请求 anyRequest().authenticated() —— 进入认证/授权流程。

  • 我们注册了自定义的 FilterSecurityInterceptor,其内部就会调用:

    • CustomFilterInvocationSecurityMetadataSource 去要 这次请求需要什么权限

    • 然后拿到了需要什么,就轮到CustomAccessDecisionManager 做“有/没有”判断。

官方文档中,这一段对应的是配置 SecurityFilterChain 并插入自定义 FilterSecurityInterceptor 的部分,这种写法是 Spring Security 6.x 的推荐风格。

基于方法的授权

基于方法授权的四大注解

除了在请求级别建模授权之外,Spring Security 还支持在方法级别建模授权。

主要使用的注解就这四个, @PreAuthorize@PostAuthorize@PreFilter@PostFilter,而且第一个最重要

  • @PreAuthorize:方法执行前授权

    • 方法被调用之前进行权限验证,只有验证通过,方法才会执行;如果验证失败,直接抛出AccessDeniedException(访问被拒绝),方法不会执行。它可以在方法执行前拦截非法请求,避免不必要的资源消耗。
    • @PreAuthorize的参数是Spring Security 表达式(SpEL)
      • hasRole('ADMIN'):判断用户是否拥有指定角色(注意:Spring Security 会自动给角色加ROLE_前缀,比如hasRole('ADMIN')等价于判断是否有ROLE_ADMIN角色)。
      • hasAuthority('file:upload'):判断用户是否拥有指定权限
      • principal:代表当前登录用户的认证对象(Authentication),可以获取用户信息,比如principal.username获取用户名。
      • #参数名:引用方法的参数,比如#userId引用方法的userId参数。和逻辑运算符:&&(且)、||(或)、!(非)。
  • @PostAuthorize:方法执行后授权

    • 先执行方法,再在方法执行完成后进行权限验证。如果验证失败,依然会抛出AccessDeniedException,但此时方法已经执行完毕,有人说这个极少用,但是我比较爱用,但是请务必注意,这个注解不能阻止方法执行,只能在方法执行后判断是否允许返回结果,因此仅适用于无副作用的方法(比如查询方法),绝对不能用于有修改操作的方法(比如新增、删除、更新)。
    • @PostAuthorize中可以通过returnObject引用方法的返回值,这是它的核心特性。例如,想要用户查询自己的信息验证是否是自己的信息或自己是否是管理员,就可以这样写@PostAuthorize("hasRole('ADMIN') || returnObject.id == principal.id")
  • @PreFilter:方法执行前过滤集合参数

    • 方法执行前,对方法的集合类型参数进行过滤,只保留符合条件的元素,再将过滤后的集合传入方法。这个我是真没咋用过

    • 参数

      • value:过滤条件(SpEL 表达式),用filterObject表示集合中的每个元素。

      • filterTarget:如果方法有多个集合参数,指定要过滤的参数名(必选)。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        // 示例1:过滤单个集合参数(无需filterTarget)
        // 场景:只保留金额大于100的订单(filterObject代表集合中的每个Order对象)
        @PreFilter("filterObject.amount > 100")
        public void processOrders(List<Order> orders) {
        orders.forEach(order -> System.out.println("处理订单:" + order.getId() + ",金额:" + order.getAmount()));
        }

        // 示例2:过滤多个集合参数(指定filterTarget)
        // 场景:过滤userIds集合,只保留等于当前用户ID的元素
        @PreFilter(value = "filterObject == principal.id", filterTarget = "userIds")
        public void batchQuery(List<Long> userIds, List<String> orderNos) {
        userIds.forEach(userId -> System.out.println("查询用户ID:" + userId));
        }
    • @PostFilter:方法执行后过滤集合返回值

      • 这个比上面用的多,这个是方法执行完成后,对方法的集合类型返回值进行过滤,只返回符合条件的元素。

      • filterObject表示返回集合中的每个元素,通过 SpEL 表达式判断是否保留。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        // 场景:查询所有产品后,只返回当前用户有权限查看的产品(filterObject代表每个Product对象)
        @PostFilter("hasRole('ADMIN') || filterObject.ownerId == principal.id")
        public List<Product> listAllProducts() {
        // 模拟从数据库查询所有产品(方法先执行)
        List<Product> products = new ArrayList<>();
        products.add(new Product(1L, "手机", 1L)); // 所有者ID:1
        products.add(new Product(2L, "电脑", 2L)); // 所有者ID:2
        return products;
        }

别忘了在配置类上添加@EnableMethodSecurity,当然,旧版本的@EnableGlobalMethodSecurity也不是不行

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // 开启Pre/Post系列注解(默认就是true,可省略)
securedEnabled = true, // 可选:开启@Secured注解
jsr250Enabled = true // 可选:开启@RolesAllowed注解
)
public class MethodSecurityConfig {
// 无需额外配置,注解已生效
}

注意,这些注解可以用在方法上:

  • 用在类上:对类中所有方法生效。
  • 用在方法上:只对当前方法生效(优先级高于类上的注解)。

使用方法级别的权限控制

先开启方法安全注解

image-20251218202501370

然后编写一个服务去控制和演示

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* 方法级授权示例 Service
* 演示 @PreAuthorize / @PostAuthorize / @PostFilter 等注解
*/
@Service
@Slf4j
public class MethodSecurityDemoService {

/**
* 只有 ADMIN 角色可以调用
*/
@PreAuthorize("hasRole('ADMIN')")
public String adminOnlyOperation() {
log.info("执行 adminOnlyOperation");
return "只有 ADMIN 角色才能看到的敏感信息";
}

/**
* USER 或 ADMIN 都可以调用
*/
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public String userOrAdminOperation() {
log.info("执行 userOrAdminOperation");
return "USER 或 ADMIN 都可以访问的业务数据";
}

/**
* 只有当前用户自己或管理员可以查看敏感资料
* 通过方法参数与认证用户进行对比
*/
@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
public Map<String, Object> loadUserPrivateProfile(String username) {
log.info("加载用户 {} 的私有资料", username);
return Map.of(
"username", username,
"phone", "138-****-0000",
"address", "某省某市某小区",
"note", "只有本人或管理员可以看到"
);
}

/**
* 使用 @PostAuthorize 在方法执行后做结果检查
* 这里演示一个简单场景:只有当结果中的 owner 和当前登录用户一致时才允许返回
*/
@PostAuthorize("returnObject['owner'] == authentication.name or hasRole('ADMIN')")
public Map<String, Object> loadDocument(String docId, String ownerUsername) {
log.info("加载文档 {},所有者 {}", docId, ownerUsername);
// 模拟查询到的文档
return Map.of(
"docId", docId,
"owner", ownerUsername,
"content", "这是文档的内容(示例)"
);
}

/**
* 使用 @PostFilter 对集合结果做过滤
* 这里只保留以 "public-" 开头的项目名
*/
@PostFilter("filterObject.startsWith('public-')")
public List<String> loadAllProjects() {
log.info("加载项目列表并在返回时做过滤");
return List.of(
"public-homepage",
"public-docs",
"internal-admin",
"internal-billing"
);
}
}

控制器就略了,然后编写一些测试用例

image-20251218202951466

测试发现,只在方法上开启一些注解,完全的可以实现权限控制

image-20251218203206695

所以实际上,这才是用到的最多的内容,那个错了的是正则匹配写错了)

授权管理的流程

授权管理前做了什么

回忆,当一个请求进入系统,它会经历以下流程:

  • 身份确认 (Authentication):首先,系统通过过滤器链确认“你是谁”。默认生成的Authentication对象处于未认证状态,登录时会交由AuthenticationManager负责进行认证。只有经过认证的 Authentication 对象才会被存入 SecurityContextHolder 后,授权检查才会开始。在用户有后续请求时,可从Authentication中检查权限。
  • 请求拦截 (Interception):由 AuthorizationFilterFilterSecurityInterceptor拦截请求。
  • 决策判定 (Decision Making):拦截器将当前的“用户信息”与“资源要求的权限”交给 AuthorizationManager。如果通过,则访问资源;否则抛出 AccessDeniedException

我们怎么知道这些过滤器在执行?其实我们只要开启 Spring Security 的debug调试模式,开发时就可以在控制台看到这些过滤器的执行顺序,如下:

image-20251218205005632
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@2b67869,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@1a89a869,
org.springframework.security.web.context.SecurityContextHolderFilter@5325581f,
org.springframework.security.web.header.HeaderWriterFilter@54a0bd59,
org.springframework.web.filter.CorsFilter@4aa445c9,
org.springframework.security.web.authentication.logout.LogoutFilter@30067021,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@446293f2,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@4b5a6fb1,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7521bf9,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@2019876e,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@6045242,
org.springframework.security.web.access.ExceptionTranslationFilter@38c9f081,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@470f757f,
org.springframework.security.web.access.intercept.AuthorizationFilter@7fa63d32
]

也就是

  1. DisableEncodeUrlFilter
  2. WebAsyncManagerIntegrationFilter
  3. SecurityContextHolderFilter(Spring Security 6.x 新增,替代旧的SecurityContextPersistenceFilter
  4. HeaderWriterFilter
  5. CorsFilter(跨域过滤器)
  6. LogoutFilter(登出过滤器)
  7. UsernamePasswordAuthenticationFilter(账号密码登录过滤器)
  8. BasicAuthenticationFilter(Basic 认证过滤器)
  9. RequestCacheAwareFilter(请求缓存过滤器)
  10. SecurityContextHolderAwareRequestFilter(请求包装过滤器)
  11. AnonymousAuthenticationFilter(匿名认证过滤器)
  12. ExceptionTranslationFilter(异常转换过滤器)
  13. FilterSecurityInterceptor(过滤器安全拦截器)
  14. AuthorizationFilter(授权过滤器,Spring Security 6.x 新增)

可以发现,过滤器的顺序是 Spring Security 根据功能优先级设计的,核心原则是:先处理基础上下文 / 异步,再处理跨域 / 头信息,再处理认证 / 登出,最后处理授权 / 异常

其中,SecurityContextHolderFilter管理SecurityContext(用户认证信息的上下文)的创建和销毁,是整个 Security 的基础,它的执行时机非常早,当请求来临时它会从SecuritContextRepository中把SecurityContext对象取出来,然后放入SecurityContextHolderThreadLocal 中。在所有拦截器都处理完成后,再把Security Context存入SecurityContext Repository,并清除 SecurityContextHolder 内的 SecurityContext 引用。

和认证授权直接相关的过滤器是 AbstractAuthenticationProcessingFilterUsernamePasswordAuthenticationFilter还有AuthorizationFilter

你可能又会问,怎么没看到AbstractAuthenticationProcessingFilter 这个过滤器呢?这是因为它是一个抽象的父类,其内部定义了认证处理的过程,UsernamePasswordAuthenticationFilter 就继承自 AbstractAuthenticationProcessingFilter,处理密码登录的认证。而 GenericFilterBean 是 Spring 框架中的过滤器类,最终的父接口是Filter

image-20251218205649819

这是简单回忆了一下认证部分,因为这是授权前要做的事情

获取认证信息——从 SecurityContext 获取 Authentication

请求到达认证过滤器,准备调用管理器

上面说,AuthorizationFilter是 Spring Security 过滤器链中负责授权的关键过滤器,它的执行时机在 认证过滤器(如 UsernamePasswordAuthenticationFilter) 之后 —— 也就是说,只有用户完成认证(Authentication被存入SecurityContext),才会进入授权环节。

AuthorizationFilter实现了Filter接口,核心逻辑在doFilter(ServletRequest request, ServletResponse response, FilterChain chain)方法中

image-20251219194831294

其中,先将将原生请求转换为Spring Security的包装请求,方便携带安全上下文,然后经过一些策略判断和错误跳过之后,开始调用核心授权逻辑

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
else {
// 4.1 生成「已过滤」标记的属性名,并存入request中(防止重复过滤)
String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

try {
// 4.2 调用AuthorizationManager的check方法,完成授权决策
// 第一个参数:Supplier<Authentication>,延迟获取认证信息(懒加载)
// 第二个参数:当前HTTP请求
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);

// 4.3 发布授权事件(默认不发布,可自定义事件监听器)这里对接授权事件这部分
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);

// 4.4 授权失败:决策不为空且未授权,抛出AccessDeniedException(403)
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}

// 4.5 授权成功:放行请求到下一个过滤器/目标资源(如Controller)
chain.doFilter(request, response);
} finally {
// 4.6 最终:移除request中的「已过滤」标记(避免影响后续请求)
request.removeAttribute(alreadyFilteredAttributeName);
}
}

this::getAuthentication是获取认证信息的关键入口

1
2
3
4
5
private Authentication getAuthentication() {
// 从SecurityContextHolderStrategy中获取SecurityContext,再获取Authentication认证信息
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
。。。。
}

其中,每个不同的AuthorizationManager都有着不同的授权检查实现,但是逻辑大差不差,都是匹配当前请求的权限规则,从Authentication中获取用户的权限列表,然后对比用户权限与资源权限要求,返回AuthorizationDecisionisGranted=true/false

image-20251219195422194

获取认证信息

其中,SecurityContextHolder是获取SecurityContext的工具类,本身不存储数据,而是委托给SecurityContextHolderStrategy策略类处理。

image-20251219195745824

SecurityContext存储在当前线程的ThreadLocal中,因此Authentication也是线程隔离的,这保证了多线程环境下用户信息不会混乱。基于ThreadLocal存储SecurityContext,就叫ThreadLocalSecurityContextHolderStrategy

SecurityContextHolder是如何获取SecurityContext的?而SecurityContext中又有什么内容是上述权限认证中必备的?

其中,SecurityContextHolder类加载时会执行initialize()方法

image-20251219200418974

然后根据strategyName决定使用哪种SecurityContextHolderStrategy也就是存储SecurityContext的策略

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
32
private static void initializeStrategy() {
if ("MODE_PRE_INITIALIZED".equals(strategyName)) {
// 预初始化模式:要求提前通过setContextHolderStrategy设置策略
Assert.state(strategy != null, "When using MODE_PRE_INITIALIZED, setContextHolderStrategy must be called with the fully constructed strategy");
} else {
// 1. 如果没有配置strategyName,默认使用MODE_THREADLOCAL
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL";
}

// 2. 根据strategyName创建对应的策略实例
if (strategyName.equals("MODE_THREADLOCAL")) {
// 默认策略:基于ThreadLocal存储SecurityContext(线程隔离)
strategy = new ThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
// 子线程可继承的策略:基于InheritableThreadLocal(适用于异步线程)
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_GLOBAL")) {
// 全局策略:单一实例存储(适用于单线程环境,几乎不用)
strategy = new GlobalSecurityContextHolderStrategy();
} else {
// 自定义策略:通过类名反射创建
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
}
}

然后, 获取 SecurityContext 的核心方法就是,getContext()方法,当调用SecurityContextHolder.getContext()时,本质是调用策略实例的getContext()方法

image-20251219200524123

类加载时初始化默认策略是ThreadLocalSecurityContextHolderStrategy

image-20251219200558788

其中,ThreadLocalSecurityContextHolderStrategy中的方法SecurityContext包装成Supplier<SecurityContext>存储在ThreadLocal中,实现延迟初始化(只有真正调用get()时才会创建SecurityContext实例)。

1
2
3
4
5
6
7
8
9
public Supplier<SecurityContext> getDeferredContext() {
// 从ThreadLocal中获取已存储的Supplier
Supplier<SecurityContext> result = (Supplier) contextHolder.get();
// 如果Supplier为空,创建新的Supplier并放入ThreadLocal
if (result == null) {
....
}
return result;
}

SecurityContext是一个接口,其唯一的核心实现类是SecurityContextImpl

image-20251219200926181
image-20251219200917964

可以看到SecurityContext最核心的内容就是Authentication对象,这个对象正是授权认证流程中必不可少的核心数据

image-20251219200949856

既然SecurityContext的核心是Authentication,那么Authentication中的哪些属性是授权认证必备的?我们看Authentication接口的核心方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface Authentication extends Principal, Serializable {
// 用户的权限列表(如ROLE_ADMIN、PERMISSION_EDIT)——授权时需要对比的核心数据
Collection<? extends GrantedAuthority> getAuthorities();

// 用户的凭证(如密码,认证后通常会清空)——认证时使用,授权时一般用不到
Object getCredentials();

// 请求的详细信息(如IP地址,可选)——非必备
Object getDetails();

// 用户的身份信息(如用户名、用户ID)——标识当前用户,认证/授权时的主体
Object getPrincipal();

// 是否已认证(boolean)——授权时首先判断的标志:只有已认证的用户才会执行授权逻辑
boolean isAuthenticated();

// 设置认证状态
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

这部分也就是

  • 请求到达授权入口,AuthorizationFilterdoFilter方法被触发,再进入核心授权逻辑
  • 认证信息获取方法AuthorizationManager.check方法的第一个参数this::getAuthentication延迟获取认证信息
  • 然后通过SecurityContextHolder获取SecurityContext,再拿到其中的Authentication对象,这是授权认证的唯一必备数据
  • 然后进入授权决策,所有AuthorizationManager的实现逻辑大概一致,核心是权限匹配

委托决策并且处理结果

在 Web 环境下,通常会进入RequestMatcherDelegatingAuthorizationManager

为什么捏?

1
2
3
4
5
6
7
// HttpSecurity中的authorizeHttpRequests方法
public HttpSecurity authorizeHttpRequests(Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer) throws Exception {
ApplicationContext context = this.getContext();
// 初始化AuthorizeHttpRequestsConfigurer,其核心是构建RequestMatcher与AuthorizationManager的映射
authorizeHttpRequestsCustomizer.customize(((AuthorizeHttpRequestsConfigurer)this.getOrApply(new AuthorizeHttpRequestsConfigurer(context))).getRegistry());
return this;
}

然后你点进去AuthorizationManagerRequestMatcherRegistry就会发现这个传进来的时候已经是RequestMatcherDelegatingAuthorizationManager

image-20251219202256308

RequestMatcherDelegatingAuthorizationManager的核心属性是存储RequestMatcherAuthorizationManager的映射关系

image-20251219202422096

它会遍历配置好的规则,找到匹配当前 URL 的 AuthorizationManager(例如 AuthorityAuthorizationManager 用于检查 hasRole)。

  • 首先,规则从哪来,通过requestMatchers("/admin/**").hasRole("ADMIN")配置的规则,会被RequestMatcherDelegatingAuthorizationManager.Builder封装成RequestMatcherEntryRequestMatcher+AuthorizationManager),存入mappings列表。就是上面红字标记的 List,具体是如何构建的就不讲了。

  • 然后,RequestMatcherDelegatingAuthorizationManagercheck方法遍历mappings,通过RequestMatcher匹配当前 URL,匹配成功后调用对应AuthorizationManager(如AuthorityAuthorizationManager)的check方法完成权限判断。

    • ```java public AuthorizationDecision check(Supplier authentication, HttpServletRequest request) { // 日志:记录正在授权的请求(如GET /admin/user) if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format(“Authorizing %s”, request)); }

      // 核心:遍历所有配置的规则(mappings列表)
      for(RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
          // 步骤1:获取当前规则的RequestMatcher
          RequestMatcher matcher = mapping.getRequestMatcher();
          // 步骤2:匹配当前请求(返回MatchResult,包含是否匹配、路径变量等)
          RequestMatcher.MatchResult matchResult = matcher.matcher(request);
          if (matchResult.isMatch()) { // 匹配成功
              // 步骤3:获取对应的AuthorizationManager(如AuthorityAuthorizationManager)
              AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
              if (this.logger.isTraceEnabled()) {
                  this.logger.trace(LogMessage.format("Checking authorization on %s using %s", request, manager));
              }
              // 步骤4:委托给该manager执行权限检查,返回决策结果
              return manager.check(authentication, new RequestAuthorizationContext(request, matchResult.getVariables()));
          }
      }
      
      // 没有匹配到任何规则,返回拒绝授权(DENY = new AuthorizationDecision(false))
      if (this.logger.isTraceEnabled()) {
          this.logger.trace(LogMessage.of(() -> "Denying request since did not find matching RequestMatcher"));
      }
      return DENY;

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19

      那么示例的`AuthorityAuthorizationManager`是如何实现的

      当`RequestMatcherDelegatingAuthorizationManager`委托给`AuthorityAuthorizationManager`后,后者的`check`方法会完成具体的权限检查

      - `hasRole`方法的预处理:添加`ROLE_`前缀

      ```java
      public static <T> AuthorityAuthorizationManager<T> hasRole(String role) {
      Assert.notNull(role, "role cannot be null");
      // 校验:角色不能以ROLE_开头,否则抛出异常(因为框架会自动添加)
      Assert.isTrue(!role.startsWith("ROLE_"), () -> role + " should not start with ROLE_ since ROLE_ is automatically prepended when using hasRole. Consider using hasAuthority instead.");
      return hasAuthority("ROLE_" + role); // 自动添加ROLE_前缀
      }

      public static <T> AuthorityAuthorizationManager<T> hasAuthority(String authority) {
      Assert.notNull(authority, "authority cannot be null");
      return new AuthorityAuthorizationManager<T>(new String[]{authority});
      }

  • check方法的权限判断

    1
    2
    3
    4
    5
    6
    7
    private final AuthoritiesAuthorizationManager delegate = new AuthoritiesAuthorizationManager();
    private final Set<String> authorities; // 存储需要检查的权限/角色(如ROLE_ADMIN)

    public AuthorizationDecision check(Supplier<Authentication> authentication, T object) {
    // 委托给AuthoritiesAuthorizationManager的check方法,传入需要的权限列表
    return this.delegate.check(authentication, this.authorities);
    }
    • AuthoritiesAuthorizationManager是真正的权限校验器,AuthorityAuthorizationManager只是一个封装层(门面)。核心逻辑就算比对用户权限和需要的权限,至少一个就通过
    • 入参:authentication(用户认证信息)、authorities(需要的权限列表,如[ROLE_ADMIN])。

那么,AuthoritiesAuthorizationManager的权限检查就涉及到权限检查的最后部分了,授权决策结果的生成

首先,如果返回的 AuthorizationDecisionisGranted() == true,则继续执行 filterChain.doFilter。如果为 false,则由 AccessDeniedHandler 处理后续逻辑(如返回 403 页面)。

AuthoritiesAuthorizationManager最终生成授权决策结果的核心类

1
2
3
4
5
6
public AuthorityAuthorizationDecision check(Supplier<Authentication> authentication, Collection<String> authorities) {
// 核心:判断用户是否拥有指定权限,返回布尔值granted
boolean granted = this.isGranted((Authentication)authentication.get(), authorities);
// 封装成AuthorityAuthorizationDecision(AuthorizationDecision的子类)返回
return new AuthorityAuthorizationDecision(granted, AuthorityUtils.createAuthorityList(authorities));
}

那么isGranted + isAuthorized就算其中的核心,涉及到权限匹配的核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private boolean isGranted(Authentication authentication, Collection<String> authorities) {
// 前置条件:Authentication不为空(用户已认证)
return authentication != null && this.isAuthorized(authentication, authorities);
}

private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
// 遍历用户的所有权限(包含角色层级,如ROLE_ADMIN包含ROLE_USER)
for(GrantedAuthority grantedAuthority : this.getGrantedAuthorities(authentication)) {
// 只要用户有一个权限匹配,就返回true
if (authorities.contains(grantedAuthority.getAuthority())) {
return true;
}
}
// 没有匹配的权限,返回false
return false;
}
  • getGrantedAuthorities:通过RoleHierarchy处理角色层级(如配置ROLE_ADMIN > ROLE_USER,则拥有ROLE_ADMIN的用户也会被认为拥有ROLE_USER)。

RequestMatcherDelegatingAuthorizationManagerAuthorizationDecision返回给AuthorizationFilter后,AuthorizationFilterdoFilter方法会处理这个结果,这个上面也说了

image-20251219203432423
  • isGranted () = true→ 执行 filterChain.doFilterisGranted () = false → 抛出异常并触发 AccessDeniedHandler,决定授权失败之后应该如何做

这部分的时序图如下

sequenceDiagram
    participant AuthFilter as AuthorizationFilter
    participant DelegatingManager as RequestMatcherDelegatingAuthorizationManager
    participant AuthoritiesManager as AuthoritiesAuthorizationManager
    participant FilterChain as FilterChain
    participant AccessDeniedHandler as AccessDeniedHandler
    participant Controller as Controller

    # 情况1:isGranted() = true
    AuthFilter->>DelegatingManager: check(auth, request)
    DelegatingManager->>AuthoritiesManager: check(auth, authorities)
    AuthoritiesManager->>DelegatingManager: return isGranted=true
    DelegatingManager->>AuthFilter: return AuthorizationDecision(true)
    AuthFilter->>FilterChain: chain.doFilter(request, response)
    FilterChain->>Controller: 执行目标资源逻辑
    Controller->>Client: 返回响应

    # 情况2:isGranted() = false
    AuthFilter->>DelegatingManager: check(auth, request)
    DelegatingManager->>AuthoritiesManager: check(auth, authorities)
    AuthoritiesManager->>DelegatingManager: return isGranted=false
    DelegatingManager->>AuthFilter: return AuthorizationDecision(false)
    AuthFilter->>AuthFilter: throw AccessDeniedException
    AuthFilter->>AccessDeniedHandler: 调用handle方法
    AccessDeniedHandler->>Client: 返回403 Forbidden(或自定义页面/JSON)

方法级别安全

URL 权限控制是靠 Filter(过滤器) 在请求进入 Servlet 之前拦截;而方法级别安全是靠 AOP Proxy(代理对象) 在方法执行前后拦截。

当你在一个 Bean 的方法上加上 @PreAuthorize 时,Spring 会在启动时为该 Bean 生成一个代理对象。

  • 非安全调用Controller -> Service.doSomething()
  • 安全调用Controller -> Proxy.doSomething() -> [授权检查] -> Service.doSomething()

方法拦截器

首先,方法拦截器主要用到的就是这两个,AuthorizationManagerBeforeMethodInterceptorAuthorizationManagerAfterMethodInterceptor,一个前置,一个后置,是 AOP 的环绕通知实现

  • 前置拦截(Before):方法执行执行授权逻辑(如@PreAuthorize@Secured),授权失败则直接抛出异常,不执行方法。
  • 后置拦截(After):方法执行执行授权逻辑(如@PostAuthorize),即使方法执行成功,授权失败仍会抛出异常。

他们都实现了MethodInterceptor也就是 AOP 联盟接口和PointcutAdvisor切面切点,能被 Spring AOP 自动识别并织入目标方法。

其中,前置拦截AuthorizationManagerBeforeMethodInterceptor对应@PreAuthorize,@Secured等,是最常用的方法授权方式

  • Spring 启动时,AuthorizationManagerBeforeMethodInterceptor会作为PointcutAdvisor被 Spring AOP 识别,根据切点匹配目标方法,并织入拦截逻辑。

  • 然后,当目标方法被调用时,AOP 会触发拦截器的invoke方法,分别调用下一步和下下步

    1
    2
    3
    4
    5
    6
    public Object invoke(MethodInvocation mi) throws Throwable {
    // 步骤3:执行授权逻辑
    this.attemptAuthorization(mi);
    // 步骤4:授权成功,执行目标方法
    return mi.proceed();
    }
  • 然后,attemptAuthorization方法就是迁至拦截器的核心授权逻辑,至于这个就是复用上面的内容了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private void attemptAuthorization(MethodInvocation mi) {
    this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
    // 调用AuthorizationManager的check方法,获取授权决策
    AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
    // 发布授权事件(默认空实现)
    this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
    // 授权失败,抛出AccessDeniedException
    if (decision != null && !decision.isGranted()) {
    this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager " + this.authorizationManager + " and decision " + decision));
    throw new AccessDeniedException("Access Denied");
    } else {
    this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
    }
    }
  • 之后,getAuthentication方法 ,获取用户认证信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    private Authentication getAuthentication() {
    // 从SecurityContext中获取Authentication(和Web授权逻辑一致)
    Authentication authentication = ((SecurityContextHolderStrategy)this.securityContextHolderStrategy.get()).getContext().getAuthentication();
    if (authentication == null) {
    // 无认证信息,抛出401异常
    throw new AuthenticationCredentialsNotFoundException("An Authentication object was not found in the SecurityContext");
    } else {
    return authentication;
    }
    }
  • 授权结果处理

    • 授权成功mi.proceed()执行目标方法,返回方法结果。
    • 授权失败:抛出AccessDeniedException,被全局异常处理器捕获(返回 403 响应)。

对了,源码中拦截器的order属性决定了执行顺序:

  • @PreAuthorizeAuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder()(优先级高)。
  • @SecuredAuthorizationInterceptorsOrder.SECURED.getOrder()(优先级次之)。
  • @Jsr250AuthorizationInterceptorsOrder.JSR250.getOrder()(优先级最低)。
  • @PostAuthorize:默认500(方法执行后,无冲突)。
  • 意义:如果一个方法同时带有多个注解,优先级高的拦截器先执行授权逻辑。
image-20251219204807437

其中,后置拦截AuthorizationManagerAfterMethodInterceptor就算方法执行后授权,对应@PostAuthorize注解

  • 首先,执行目标方法

    1
    2
    3
    4
    5
    6
    7
    8
    public Object invoke(MethodInvocation mi) throws Throwable {
    // 步骤1:先执行目标方法,获取返回值
    Object result = mi.proceed();
    // 步骤2:执行授权逻辑(传入方法返回值)
    this.attemptAuthorization(mi, result);
    // 步骤3:返回方法结果
    return result;
    }
  • 然后attemptAuthorization方法 —— 传入返回值授权

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private void attemptAuthorization(MethodInvocation mi, Object result) {
    this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
    // 封装方法调用和返回值为MethodInvocationResult
    MethodInvocationResult object = new MethodInvocationResult(mi, result);
    // 调用AuthorizationManager的check方法(可读取返回值)
    AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object);
    this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, decision);
    if (decision != null && !decision.isGranted()) {
    throw new AccessDeniedException("Access Denied");
    }
    }

AuthorizationManager决策者判断

AuthorizationManager是方法授权的统筹者,负责读取方法上的注解表达式、调用表达式处理器解析表达式、根据解析结果生成授权决策(AuthorizationDecision)。前置和后置分别是PreAuthorizeAuthorizationManagerPostAuthorizeAuthorizationManager,以PreAuthorizeAuthorizationManager为例

首先,将PreAuthorizeAuthorizationManager绑定到拦截器,只有先把决策者和拦截器绑定,执行时拦截器才能调用决策者。我们从AuthorizationManagerBeforeMethodInterceptor的静态工厂方法源码入手:

image-20251220150815430

它会匹配带有@PreAuthorize注解的方法,然后创建拦截器实例,传入切点和PreAuthorizeAuthorizationManager,最后标注一下执行的顺序

之后,拦截器的构造方法中会存入决策者的实例

image-20251220151119352

然后,你只需要在配置类上添加@EnableMethodSecurity,底层会自动执行上述静态方法,创建AuthorizationManagerBeforeMethodInterceptor(绑定PreAuthorizeAuthorizationManager)并注册到 Spring 容器

因为@EnableMethodSecurity导入了MethodSecurityConfiguration,该配置类会调用AuthorizationManagerBeforeMethodInterceptor.preAuthorize()等方法,创建并注册拦截器实例。

上面这些步骤完成了AuthorizationManager决策者与对应的拦截器绑定然后指定切点。之后Spring 自动注册拦截器为 AOP 的Advisor,使其能织入目标方法的调用流程。

之后就是方法调用触发拦截器的invoke方法,拦截器调用PreAuthorizeAuthorizationManagercheck方法完成授权决策,形成 “拦截器→决策者” 的调用链路。

  • 当你调用带有@PreAuthorize的方法,对应的拦截器AuthorizationManagerBeforeMethodInterceptor调用invoke方法

    image-20251220151622721
  • invoke 调用attemptAuthorization方法,它调用决策者的authorize方法,进行权限认证

    image-20251220151847703
    image-20251220151937077
  • 最后,权限决策者调用PreAuthorizeAuthorizationManagerauthorize方法执行授权决策,这个就是权限决策者的核心权限验证方法

那么,注解上的 SpEL 表达式是如何被处理的呢?

image-20251220150307870

首先,PreAuthorizeExpressionAttributeRegistry:是一个缓存注册表,作用是:

  1. 扫描方法上的@PreAuthorize注解,提取其中的 SpEL 表达式(如"hasRole('ADMIN')"),封装成ExpressionAttribute对象。
  2. 缓存表达式结果,避免每次方法调用都重新扫描注解(提升性能)。
  3. 持有MethodSecurityExpressionHandler实例,供后续解析表达式使用。

这之后就算涉及到表达式处理的内容了

表达式处理器

MethodSecurityExpressionHandler是方法授权中表达式的处理器,其中最主要的就算负责解析 SpEL 表达式,创建表达式执行的上下文,而且它支持方法参数 / 返回值的引用,是 SpEL 表达式能生效的核心。

MethodSecurityExpressionHandler是一个接口,继承自SecurityExpressionHandler,是处理方法级 SpEL 表达式的核心接口。其核心作用是为 SpEL 表达式提供执行环境和解析能力

image-20251220152524703

DefaultMethodSecurityExpressionHandlerMethodSecurityExpressionHandler的默认实现,其核心能力包括:

image-20251220152547962
  • 创建EvaluationContext上下文,包含:

    1. 用户认证信息Authentication对象(可通过authentication引用)。
    2. 方法参数:方法的参数(可通过#参数名引用,如#id)。
    3. 内置权限方法:如hasRole()hasAuthority()isAuthenticated()等(这些方法由SecurityExpressionRoot提供)。
  • 解析 SpEL 表达式

    @PreAuthorize中的字符串表达式(如"hasRole('ADMIN') and #id == authentication.principal.id")解析为 Spring 的Expression对象,供后续执行。

  • 支持方法返回值和过滤

    • setReturnObject:将方法返回值放入上下文,支持returnObject引用。
    • filter:处理@PreFilter/@PostFilter注解,过滤集合中的元素(如@PostFilter("filterObject.userId == authentication.principal.id"))。

这块有缘再见了