授权管理讲解

授权管理是什么

一般我们来说,权限认证就是解决你是谁的问题,也就是验证用户身份,在这个过程中,我们通常使用登录来实现,它的核心目标是,核实用户的身份合法性

授权是在身份认证通过后,判断该用户是否有权限执行某个操作的过程。它的核心目标是,控制不同级别的用户对资源的访问权限

授权的典型场景:

  • 验证用户是否有权限访问某个 URL(如 /admin/* 仅管理员可访问)
  • 验证用户是否有权限调用某个方法(如 deleteUser() 仅超级管理员可执行)
  • 验证用户是否有权限操作某个数据(如只能修改自己创建的订单)

授权必须在认证之后进行(只有知道 “你是谁”,才能判断 “你能做什么”)。认证通过后,Spring Security 将用户的身份信息(含角色 / 权限)存入 SecurityContext,授权环节从 SecurityContext 中获取这些信息进行权限判断。

一般的授权需求有这样的两种:

  • 用户-权限-资源
    • 这是最基础的 “点对点” 授权模式,核心逻辑是 “给用户直接分配‘操作资源’的权限”,无需中间载体,权限与用户直接绑定。
    • 例如:给 “张三” 分配 “查看文档 A(资源)” 和 “编辑表格 B(资源)” 的权限 → 张三直接拥有这两个 “权限 - 资源” 组合,无其他中间环节。
  • 用户-角色-权限-资源
    • 这是工业界主流的授权模式,核心逻辑是 “先定义角色,将权限分配给角色,再将角色分配给用户”,通过 “角色” 作为中间载体,实现权限的批量管理。
    • 例:
      1. 定义 “产品经理” 角色;
      2. 给该角色分配 “查看需求文档(资源)”“编辑产品原型(资源)” 权限;
      3. 将 “张三” 关联到 “产品经理” 角色 → 张三间接获得角色对应的所有权限。

这两种授权需求,本质上是权限管理领域中经典的 DAC(自主访问控制)模型简化版RBAC(基于角色的访问控制)模型,有兴趣可以自己去看一下

权限决策依据

既然谈到了 RBACABAC 两个模型,就大家介绍下两者间的区别:

RBAC

核心思想:以角色作为权限管理的核心,每个用户被赋予一个或多个角色,而角色与权限之间存在固定的映射关系。 决策依据:当用户请求访问资源时,系统根据用户所属角色所拥有的权限进行校验。 粒度:粒度相对较粗,因为权限是绑定在角色上的,无法针对单个请求条件进行动态决策。

ABAC

核心思想:以属性(Attribute)为基础,利用用户属性、资源属性、环境属性等多个维度的条件进行权限判断。 决策依据:权限决策是基于各种属性之间的逻辑表达式和策略规则来动态确定是否允许访问。 粒度:支持非常细粒度的控制,可以针对具体属性制定规则,实现精准的权限控制。

Spring Security 中的授权方式

在这里简单介绍一下 Spring Security 中常见的授权方式和最简单的解释,方便后面了解

基于 URL 的授权

这是最常用的授权方式,通过拦截 HTTP 请求,根据请求的 URL 路径判断用户是否有权限访问。

通过 HttpSecurity 配置类中的 authorizeRequests() 方法定义 URL 与权限的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 公开访问的资源
.antMatchers("/public/**").permitAll()
// 仅管理员可访问
.antMatchers("/admin/**").hasRole("ADMIN")
// 需要特定权限
.antMatchers("/users/**").hasAuthority("USER_MANAGE")
// 其他所有请求需要认证
.anyRequest().authenticated()
.and()
.formLogin();
}
}

基于角色的访问控制(RBAC)

通过用户所属的角色(Role) 进行授权,角色通常是一组权限的集合(如 ROLE_ADMINROLE_USER)。

这种一般在数据库中也要存储用户对应的相关角色

实现方式:

  1. 在配置类中声明角色权限

    1
    2
    3
    http.authorizeRequests()
    .antMatchers("/admin/**").hasRole("ADMIN") // 等价于 hasAuthority("ROLE_ADMIN")
    .antMatchers("/user/**").hasAnyRole("ADMIN", "USER"); // 多个角色
  2. 在用户信息中关联角色

    通过 UserDetailsService 加载用户时,返回包含角色信息的 UserDetails 对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Service
    public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) {
    // 模拟从数据库查询用户
    boolean isAdmin = "admin".equals(username);
    List<String> roles = isAdmin ?
    Arrays.asList("ROLE_ADMIN") :
    Arrays.asList("ROLE_USER");

    return User.withUsername(username)
    .password(passwordEncoder.encode("password"))
    .roles(roles.toArray(new String[0])) // 关联角色
    .build();
    }
    }
    • Spring Security 中角色默认需要以 ROLE_ 为前缀(hasRole("ADMIN") 等价于 hasAuthority("ROLE_ADMIN")),这个其实我们之前讲源码的时候涉及到了

    • 可通过配置 rolePrefix("") 取消前缀限制

基于权限的访问控制(细粒度权限)

比角色更细粒度的控制,直接通过权限标识(Permission) 授权(如 user:deleteorder:view)。

实现方式:

  1. 在配置类中指定权限

    1
    2
    3
    4
    http.authorizeRequests()
    .antMatchers(HttpMethod.GET, "/users").hasAuthority("user:view")
    .antMatchers(HttpMethod.POST, "/users").hasAuthority("user:create")
    .antMatchers(HttpMethod.DELETE, "/users/**").hasAuthority("user:delete");
  2. 在用户信息中关联权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public UserDetails loadUserByUsername(String username) {
    // 为用户分配具体权限
    List<GrantedAuthority> authorities = new ArrayList<>();
    if ("admin".equals(username)) {
    authorities.add(new SimpleGrantedAuthority("user:view"));
    authorities.add(new SimpleGrantedAuthority("user:create"));
    authorities.add(new SimpleGrantedAuthority("user:delete"));
    } else {
    authorities.add(new SimpleGrantedAuthority("user:view"));
    }

    return User.withUsername(username)
    .password(passwordEncoder.encode("password"))
    .authorities(authorities) // 直接关联权限
    .build();
    }

方法级别的授权

通过注解控制方法调用权限,适用于服务层或业务逻辑层的权限控制。

实现方式:

  1. 开启方法级安全注解

    在配置类上添加 @EnableGlobalMethodSecurity 注解:

    1
    2
    3
    4
    5
    6
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用@PreAuthorize等注解
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    }
  2. 使用注解控制方法权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Service
    public class UserService {
    // 仅管理员可调用
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long userId) {
    // 业务逻辑
    }

    // 需要特定权限
    @PreAuthorize("hasAuthority('user:view')")
    public User getUser(Long userId) {
    // 业务逻辑
    }

    // 复杂表达式:只能修改自己的信息
    @PreAuthorize("authentication.principal.username == #username or hasRole('ADMIN')")
    public void updateUser(String username, UserInfo info) {
    // 业务逻辑
    }
    }

基于数据的授权(行级权限)

控制用户对具体数据对象的访问权限(如 “只能查看自己创建的订单”),是最细粒度的授权。

这种的实现方式一般就很自定义化了:

  1. 通过 SpEL 表达式结合方法参数

    1
    2
    3
    4
    @PreAuthorize("hasPermission(#orderId, 'Order', 'view')")
    public Order getOrder(Long orderId) {
    return orderRepository.findById(orderId).orElse(null);
    }
  2. 自定义 PermissionEvaluator

    实现 PermissionEvaluator 接口定义数据权限规则:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Component
    public class CustomPermissionEvaluator implements PermissionEvaluator {
    @Autowired
    private OrderRepository orderRepository;

    @Override
    public boolean hasPermission(Authentication auth, Object targetId, Object targetType, Object permission) {
    // 检查用户是否有权限操作目标数据
    String username = auth.getName();
    Long orderId = (Long) targetId;
    Order order = orderRepository.findById(orderId).orElse(null);

    // 规则:订单创建者或管理员可查看
    return order != null && (
    order.getCreator().equals(username) ||
    auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))
    );
    }

    // 其他方法实现...
    }
  3. 注册自定义 PermissionEvaluator

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Configuration
    public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Autowired
    private CustomPermissionEvaluator permissionEvaluator;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
    DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
    handler.setPermissionEvaluator(permissionEvaluator);
    return handler;
    }
    }

基于 request 的授权

在 Spring Security 中,基于 Request 的授权(Request-based Authorization)是一种针对 HTTP 请求进行的权限控制方式,它通过拦截客户端发送的 HTTP 请求(如 URL 访问、请求方法等),判断当前用户是否有权限执行该请求。

这种授权方式聚焦于 “请求资源” 本身,核心是控制 “哪些用户可以访问哪些 URL 或请求方法”,是 Web 应用中最基础也最常用的授权形式。

那么基于 Request 的授权,我们的目标需要完成以下内容

  • 限制用户对特定 URL 路径的访问(如 /admin/* 仅管理员可访问)
  • 限制用户对特定 HTTP 方法的使用(如仅允许 POST 请求创建资源)
  • 结合请求参数、IP 地址等信息进行更精细的访问控制

基于 Request 的授权主要通过 HttpSecurity 配置类中的 authorizeRequests() 方法实现,该方法提供了一系列链式 API 来定义请求与权限的映射规则。通过这个来配置对权限的控制

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
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 开启请求授权配置
.authorizeRequests()
// 1. 公开访问(无需认证)
.antMatchers("/", "/home", "/public/**").permitAll()

// 2. 仅认证用户可访问
.antMatchers("/user/**").authenticated()

// 3. 基于角色的控制
.antMatchers("/admin/**").hasRole("ADMIN")

// 4. 基于权限的控制
.antMatchers("/api/users/**").hasAuthority("USER_MANAGE")

// 5. 结合 HTTP 方法的控制
.antMatchers(HttpMethod.GET, "/api/orders").hasAuthority("ORDER_VIEW")
.antMatchers(HttpMethod.POST, "/api/orders").hasAuthority("ORDER_CREATE")

// 6. 所有其他请求必须认证
.anyRequest().authenticated()
.and()
// 配置登录方式(表单登录、OAuth2等)
.formLogin();
}
}

其中,核心匹配规则与方法如下

  1. URL 路径匹配

    • antMatchers(String... patterns):使用 Ant 风格路径匹配(如 /admin/** 匹配 /admin 下所有路径),Spring 的各个组件涉及到路径匹配的,比较新的版本中基本都用的是 Ant
    • regexMatchers(String... regexPatterns):使用正则表达式匹配 URL
    • mvcMatchers(String... patterns):基于 Spring MVC 的路径匹配(考虑控制器映射)
  2. 请求方法匹配:

    • 结合 HttpMethod 枚举限制请求类型

      1
      .antMatchers(HttpMethod.DELETE, "/api/users/**").hasRole("ADMIN")
  3. 权限判断方法

    • permitAll():允许所有用户访问(包括未认证用户)
    • denyAll():拒绝所有用户访问
    • authenticated():仅允许已认证用户访问
    • anonymous():仅允许匿名用户访问(未认证用户)
    • hasRole(String role):需要特定角色(自动拼接 ROLE_ 前缀)
    • hasAnyRole(String... roles):需要任意一个角色
    • hasAuthority(String authority):需要特定权限(不自动拼接前缀)
    • hasAnyAuthority(String... authorities):需要任意一个权限
    • access(String spelExpression):使用 SpEL 表达式自定义判断逻辑

也顺便简单提一下基于 Request 的授权的原理,其实我们之前简单的说过一些,就是通过 FilterSecurityInterceptor 过滤器实现,它是 Spring Security 过滤器链中的最后一个过滤器,负责对请求进行授权检查:

  1. 拦截请求:当客户端发送 HTTP 请求时,FilterSecurityInterceptor 拦截该请求。
  2. 获取认证信息:从 SecurityContextHolder 中获取当前用户的 Authentication 对象(包含用户权限)。
  3. 匹配规则:根据 HttpSecurity 中配置的规则,确定当前请求对应的权限要求。
  4. 授权决策:调用AccessDecisionManager(授权决策管理器),对比用户权限与请求所需权限:
    • 权限足够:允许请求继续处理(进入控制器方法)。
    • 权限不足:抛出 AccessDeniedException,并根据配置跳转至 403 页面或返回错误信息。

授权管理实例

基于request的授权——权限分配

配置思路

基于 Request 的授权主要通过HttpSecurity配置类实现,核心是通过antMatchers()mvcMatchers()regexMatchers()定义 URL 模式,再通过hasRole()hasAuthority()等方法指定访问所需的权限

其实我们最常用的方法就是通过 HttpSecurity 直接配置,在 Security 配置类中重写configure(HttpSecurity http)方法:

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
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 公开访问的路径
.antMatchers("/", "/home", "/public/**").permitAll()
// 需要ADMIN角色才能访问的路径
.antMatchers("/admin/**").hasRole("ADMIN")
// 需要USER角色或ADMIN角色才能访问的路径
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// 指定HTTP方法的权限控制
.antMatchers(HttpMethod.POST, "/api/**").hasAuthority("API_WRITE")
.antMatchers(HttpMethod.GET, "/api/**").hasAnyAuthority("API_READ", "API_WRITE")
// 除上述路径外,其他所有路径都需要认证
.anyRequest().authenticated()
.and()
.formLogin() // 配置登录页
.permitAll()
.and()
.logout() // 配置退出
.permitAll();
}
}

使用 @PreAuthorize 注解(方法级别控制)可以更灵活的权限被控制,首先在配置类上添加@EnableGlobalMethodSecurity注解开启方法级安全:

1
2
3
4
5
6
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启@PreAuthorize支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ... 其他配置
}

在 Controller 方法上使用注解:

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
@RestController
@RequestMapping("/api")
public class ApiController {

// 只允许有API_READ权限的用户访问
@PreAuthorize("hasAuthority('API_READ')")
@GetMapping("/data")
public String getData() {
return "敏感数据";
}

// 只允许ADMIN角色的用户访问
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/user")
public String createUser() {
return "用户创建成功";
}

// 复杂条件:ADMIN角色或当前用户ID与路径参数一致
@PreAuthorize("hasRole('ADMIN') or authentication.principal.id == #userId")
@GetMapping("/user/{userId}")
public String getUser(@PathVariable Long userId) {
return "用户信息";
}
}

对于更复杂的权限判断,可以实现PermissionEvaluator接口,先不展示了,极少使用

在实际开发中,权限这块是很讲究规范的

  1. 权限命名规范
    • 角色:通常用ROLE_XXX格式(如ROLE_ADMIN
    • 权限:通常用资源_操作格式(如USER_CREATEORDER_READ
  2. URL 设计与权限的对应
    • 建议 URL 路径与资源对应(如/users/**对应用户资源)
    • HTTP 方法与操作对应(GET = 查询,POST = 创建,PUT = 更新,DELETE = 删除)
  3. 权限粒度控制
    • 粗粒度:使用角色控制(如hasRole('ADMIN')
    • 细粒度:使用权限控制(如hasAuthority('USER_DELETE')
  4. 安全顺序
    • 具体的 URL 模式应该放在前面,通用的模式放在后面
    • 因为 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
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
-- 权限分配系统数据库表设计
-- 简单的RBAC模型:用户 -> 角色 -> 权限

-- 1. 用户表
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码(加密后)',
email VARCHAR(100) COMMENT '邮箱',
enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) COMMENT '用户表';

-- 2. 角色表
CREATE TABLE roles (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE COMMENT '角色名称',
description VARCHAR(200) COMMENT '角色描述',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT '角色表';

-- 3. 权限表
CREATE TABLE permissions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE COMMENT '权限名称',
resource VARCHAR(100) NOT NULL COMMENT '资源路径,如 /api/users',
action VARCHAR(20) NOT NULL COMMENT '操作类型,如 GET, POST, PUT, DELETE',
description VARCHAR(200) COMMENT '权限描述',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT '权限表';

-- 4. 用户角色关联表
CREATE TABLE user_roles (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '分配时间',
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
UNIQUE KEY uk_user_role (user_id, role_id)
) COMMENT '用户角色关联表';

-- 5. 角色权限关联表
CREATE TABLE role_permissions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_id BIGINT NOT NULL COMMENT '角色ID',
permission_id BIGINT NOT NULL COMMENT '权限ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '分配时间',
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE,
UNIQUE KEY uk_role_permission (role_id, permission_id)
) COMMENT '角色权限关联表';

-- 创建索引以提高查询性能
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_permissions_resource_action ON permissions(resource, action);

表都有了,对应是实体类和repository就不写了,我们直接来看如何配置

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
72
73
74
75
76
77
78
/**
* Spring Security安全配置
* 配置基于请求的权限验证
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomUserDetailsService customUserDetailsService;
private final CustomFilterInvocationSecurityMetadataSource securityMetadataSource;
private final CustomAccessDecisionManager accessDecisionManager;

/**
* 密码编码器Bean
* 使用BCrypt算法进行密码加密
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

/**
* 安全过滤器链配置
* 配置HTTP安全策略
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 禁用CSRF保护(REST API不需要)
.csrf(AbstractHttpConfigurer::disable)

// 配置请求授权
.authorizeHttpRequests(auth -> auth
// 公开访问的端点
.requestMatchers("/api/public/**", "/login", "/logout").permitAll()
// 其他所有请求都需要认证和授权
.anyRequest().authenticated()
)

// 配置HTTP Basic认证(可以改为表单登录)
.httpBasic(basic -> {})

// 配置表单登录
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)

// 配置登出
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)

// 配置用户详情服务
.userDetailsService(customUserDetailsService)

// 添加自定义的权限过滤器
.addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class)

.build();
}

/**
* 自定义权限过滤器
* 使用自定义的元数据源和访问决策管理器
*/
@Bean
public FilterSecurityInterceptor customFilterSecurityInterceptor() {
FilterSecurityInterceptor interceptor = new FilterSecurityInterceptor();
interceptor.setSecurityMetadataSource(securityMetadataSource);
interceptor.setAccessDecisionManager(accessDecisionManager);
return interceptor;
}
}

但是你无论使用哪种方式,都需要确保用户的权限信息能被正确加载:

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
/**
* 自定义用户详情服务
* 用于Spring Security从数据库加载用户信息和权限
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

/**
* 根据用户名加载用户详情
* Spring Security会调用此方法来验证用户
*
* @param username 用户名
* @return UserDetails 用户详情信息
* @throws UsernameNotFoundException 用户不存在异常
*/
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.debug("尝试加载用户: {}", username);

// 从数据库查找用户,同时加载角色和权限信息
User user = userRepository.findByUsernameWithRolesAndPermissions(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));

// 构建权限列表
List<GrantedAuthority> authorities = buildUserAuthorities(user.getRoles());

log.debug("用户 {} 加载成功,拥有权限: {}", username, authorities);

// 返回Spring Security的UserDetails对象
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.disabled(!user.getEnabled())
.authorities(authorities)
.build();
}

/**
* 构建用户权限列表
* 包含角色权限和具体的操作权限
*
* @param roles 用户角色集合
* @return 权限列表
*/
private List<GrantedAuthority> buildUserAuthorities(Set<Role> roles) {
List<GrantedAuthority> authorities = new ArrayList<>();

for (Role role : roles) {
// 添加角色权限(以ROLE_开头)
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));

// 添加具体权限(格式:ACTION:RESOURCE)
for (Permission permission : role.getPermissions()) {
String authority = permission.getAuthority(); // 返回 "ACTION:RESOURCE" 格式
authorities.add(new SimpleGrantedAuthority(authority));
}
}

return authorities;
}
}

简单写个接口

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
/**
* 用户管理控制器
* 提供用户相关的REST API接口
* 需要相应权限才能访问
*/
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {

private final UserService userService;

/**
* 获取所有用户列表
* 需要权限:GET:/api/users
*/
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
log.info("获取所有用户列表");
List<User> users = userService.findAll();
return ResponseEntity.ok(users);
}

/**
* 根据用户名获取用户信息
* 需要权限:GET:/api/users
*/
@GetMapping("/{username}")
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
log.info("获取用户信息: {}", username);
return userService.findByUsername(username)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}

我们发送这样的一个请求试试

1
curl http://localhost:8080/api/public/info

可以看到我们访问公共接口是可以的

image-20250927204926561

访问一个受保护的资源看看,可以看到失败了

1
http://localhost:8080/api/users
image-20250927205110728

测试一个正确的,发现是可以的

image-20250927210802815

基于request的授权——请求未授权处理

配置思路

在 Spring Security 基于 Request 的授权体系中,“请求未授权处理” 是保障用户体验和系统安全性的关键环节 —— 当用户访问无权限的 URL 时,系统需要返回明确、友好且符合业务场景的响应(而非默认的 403 页面)。

先简单说一下请求未授权该怎么做,这个后面会从底层分析

  1. 触发条件:用户请求的 URL 匹配了 Spring Security 的授权规则(如/admin/**ROLE_ADMIN),但用户身份不满足权限要求(如仅拥有ROLE_USER)。
  2. 核心异常:Spring Security 会抛出AccessDeniedException(访问拒绝异常),该异常是未授权处理的 “入口信号”。
  3. 处理链路:
    • 异常先被ExceptionTranslationFilter拦截(Spring Security 内置过滤器,负责处理认证 / 授权异常);
    • 过滤器判断异常类型:若为AccessDeniedException,则委托给「未授权处理器」处理;
    • 处理器根据场景(如 Web 页面、API 接口)返回对应响应(如跳转页面、JSON 提示)。

一般情况下,我们会通过HttpSecurity配置全局的未授权处理器,适用于所有请求的统一处理(如前后端分离项目中,所有未授权请求均返回 JSON)。核心是实现AccessDeniedHandler接口,自定义响应内容。

  • 实现自定义 AccessDeniedHandler

    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
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.stereotype.Component;

    import java.io.IOException;
    import java.nio.charset.StandardCharsets;

    /**
    * 自定义未授权处理器:前后端分离场景下返回JSON
    */
    @Component
    public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request,
    HttpServletResponse response,
    AccessDeniedException accessDeniedException) throws IOException {
    // 1. 设置响应格式和编码(避免中文乱码)
    response.setContentType("application/json;charset=UTF-8");
    response.setCharacterEncoding(StandardCharsets.UTF_8.name());

    // 2. 设置响应状态码(403 Forbidden 是未授权的标准状态码)
    response.setStatus(HttpServletResponse.SC_FORBIDDEN);

    // 3. 构造响应体(可根据业务自定义字段,如code、message、timestamp)
    String jsonResponse = "{" +
    "\"code\":403," +
    "\"message\":\"您没有访问该资源的权限,请联系管理员\"," +
    "\"path\":\"" + request.getRequestURI() + "\"," +
    "\"timestamp\":" + System.currentTimeMillis() +
    "}";

    // 4. 写入响应
    response.getWriter().write(jsonResponse);
    }
    }
  • 在 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
    27
    28
    29
    30
    31
    32
    33
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.web.SecurityFilterChain;
    import org.springframework.security.web.access.AccessDeniedHandler;

    @Configuration
    public class SecurityConfig {

    // 注入自定义未授权处理器
    @Autowired
    private AccessDeniedHandler customAccessDeniedHandler;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
    // 1. 配置授权规则(示例:/admin/**需ADMIN角色)
    .authorizeHttpRequests(auth -> auth
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .anyRequest().authenticated()
    )
    // 2. 关联未授权处理器:所有未授权请求均走自定义逻辑
    .exceptionHandling(ex -> ex
    .accessDeniedHandler(customAccessDeniedHandler)
    )
    // 其他配置(如登录、退出)...
    .formLogin(form -> form.permitAll())
    .logout(logout -> logout.permitAll());

    return http.build();
    }
    }

前后端分离的项目,一般情况下我们就这么些,适用于所有未授权请求需统一响应逻辑的场景

但是,如果我们希望按请求类型差异化处理,这种情况下一般是传统项目

首先是实现差异化处理器,在CustomAccessDeniedHandler中增加请求类型判断(通过Accept请求头或 URL 后缀区分):

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
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
// 1. 判断请求类型:是否为API请求(示例:URL以/api/开头 或 Accept头包含application/json)
boolean isApiRequest = request.getRequestURI().startsWith("/api/")
|| request.getHeader("Accept").contains("application/json");

if (isApiRequest) {
// 2. API请求:返回JSON
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
String json = "{" +
"\"code\":403," +
"\"message\":\"API访问权限不足\"," +
"\"path\":\"" + request.getRequestURI() + "\"" +
"}";
response.getWriter().write(json);
} else {
// 3. 页面请求:跳转至自定义403页面(需提前创建403.html或403.jsp)
response.sendRedirect(request.getContextPath() + "/error/403");
}
}
}

然后配置 403 页面的访问权限,要不然就死循环了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// 允许匿名访问403页面
.requestMatchers("/error/403").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.accessDeniedHandler(customAccessDeniedHandler)
);

return http.build();
}

二如果有更细的需求,就是若某几个特殊接口需要自定义未授权响应(与全局逻辑不同),可通过「方法级注解 + 异常捕获」实现,核心是利用 Spring 的@ExceptionHandler捕获接口抛出的AccessDeniedException,然后进行差异化处理,不展示了,用的很少。

总之,Spring Security 的未授权处理核心是围绕AccessDeniedHandler接口扩展

实例编写

梳理一下我们要写什么

首先我们肯定要有一个统一异常处理器 - 处理授权失败和认证异常,对了,ApiResponse 也要有,这俩知道就行,不屑了

那么,继续上面的流程,要前后端分离,那么就需要当没有授权的时候返回 JSON 错误,那么就要自定义 AccessDeniedHandler 接口实现类,处理 AccessDeniedException。然后在 Spring Security 配置类中注册该处理器。

那么在这之前,我需要做一件事,当用户尝试访问受保护资源但未登录(或登录状态失效)时,我希望提供定制化的响应,也就是用户已登录,但缺乏访问资源所需的权限,是由 AccessDeniedHandler 处理的(前文已讲)。

但是还有一种情况就是用户未登录、登录凭证过期或无效(如 Token 失效),此时访问受保护资源会触发该场景,也算是一种未授权,也就是实现 AuthenticationEntryPoint 接口的自定类,但实际业务中,我们可能需要定制响应(如返回 JSON 格式的错误信息、重定向到自定义登录页等),因此需要通过 CustomAuthenticationEntryPoint 实现个性化处理。

AuthenticationEntryPoint 专门负责处理 “未认证” 场景,其默认行为是:

  • 对于 Web 应用,默认跳转到登录页面;
  • 对于 API 接口,默认返回 401 状态码和简单提示。
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
/**
* 自定义认证入口点
* 处理未认证用户访问受保护资源的情况
*/
@Component
@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper = new ObjectMapper();

/**
* 处理认证异常
* 当用户未认证就访问受保护资源时被调用
*/
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {

log.warn("未认证访问 - IP: {}, 路径: {}, 方法: {}, User-Agent: {}",
getClientIpAddress(request),
request.getRequestURI(),
request.getMethod(),
request.getHeader("User-Agent"));

// 设置响应头
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());

// 创建错误响应
ApiResponse<Void> errorResponse = ApiResponse.<Void>unauthorized("请先登录后再访问")
.path(request.getRequestURI());

// 写入响应
String jsonResponse = objectMapper.writeValueAsString(errorResponse);
response.getWriter().write(jsonResponse);
}

/**
* 获取客户端真实IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
return xForwardedFor.split(",")[0].trim();
}

String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
return xRealIp;
}

return request.getRemoteAddr();
}
}

接着编写实现 AccessDeniedHandler 接口的访问拒绝处理器

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
/**
* 自定义访问拒绝处理器
* 处理已认证用户访问无权限资源的情况
*/
@Component
@Slf4j
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

private final ObjectMapper objectMapper = new ObjectMapper();

/**
* 处理访问拒绝异常
* 当已认证用户访问无权限资源时被调用
*/
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication != null ? authentication.getName() : "匿名用户";

log.warn("权限不足访问 - 用户: {}, IP: {}, 路径: {}, 方法: {}, 权限: {}",
username,
getClientIpAddress(request),
request.getRequestURI(),
request.getMethod(),
authentication != null ? authentication.getAuthorities() : "无");

// 设置响应头
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());

// 创建错误响应
ApiResponse<Void> errorResponse = ApiResponse.<Void>forbidden("权限不足,无法访问该资源")
.path(request.getRequestURI());

// 写入响应
String jsonResponse = objectMapper.writeValueAsString(errorResponse);
response.getWriter().write(jsonResponse);
}

/**
* 获取客户端真实IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
return xForwardedFor.split(",")[0].trim();
}

String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
return xRealIp;
}

return request.getRemoteAddr();
}
}

更新SecurityConfig 配置,在配置异常处理位置把我们上面写的东西配置进去

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* Spring Security安全配置
* 配置基于请求的权限验证和异常处理
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomUserDetailsService customUserDetailsService;
private final CustomFilterInvocationSecurityMetadataSource securityMetadataSource;
private final CustomAccessDecisionManager accessDecisionManager;
private final CustomAuthenticationEntryPoint authenticationEntryPoint;
private final CustomAccessDeniedHandler accessDeniedHandler;

/**
* 密码编码器Bean
* 使用BCrypt算法进行密码加密
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

/**
* 安全过滤器链配置
* 配置HTTP安全策略、异常处理
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 禁用CSRF保护(REST API不需要)
.csrf(AbstractHttpConfigurer::disable)

// 配置请求授权
.authorizeHttpRequests(auth -> auth
// 公开访问的端点
.requestMatchers(
"/api/public/**",
"/login",
"/logout",
"/error",
"/favicon.ico"
).permitAll()
// 其他所有请求都需要认证和授权
.anyRequest().authenticated()
)

// 配置HTTP Basic认证
.httpBasic(basic -> {})

// 配置表单登录
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)

// 配置登出
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)

// 配置异常处理
.exceptionHandling(exceptions -> exceptions
// 配置认证入口点(处理未认证访问)
.authenticationEntryPoint(authenticationEntryPoint)
// 配置访问拒绝处理器(处理权限不足访问)
.accessDeniedHandler(accessDeniedHandler)
)

// 配置用户详情服务
.userDetailsService(customUserDetailsService)

// 添加自定义的权限过滤器
.addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class)

.build();
}

/**
* 自定义权限过滤器
* 使用自定义的元数据源和访问决策管理器
*/
@Bean
public FilterSecurityInterceptor customFilterSecurityInterceptor() {
FilterSecurityInterceptor interceptor = new FilterSecurityInterceptor();
interceptor.setSecurityMetadataSource(securityMetadataSource);
interceptor.setAccessDecisionManager(accessDecisionManager);
return interceptor;
}
}

控制器我就不写了,我们直接测试一下

image-20250928201223325
image-20250928202304210

成功了

基于request的授权——角色分配

配置思路

在 Spring Security 基于 Request 的授权体系中,角色分配是控制 “哪些用户能访问哪些资源” 的核心环节,本质是将 “用户 - 角色 - 资源(URL)” 三者建立关联。

Spring Security 默认规则中,角色需以ROLE_为前缀(如ROLE_ADMIN),权限无需前缀;通过hasRole("ADMIN")判断角色时,框架会自动拼接ROLE_前缀,等价于hasAuthority("ROLE_ADMIN")

有那么一种情况就是静态角色分配,指在 Security 配置类中直接硬编码 URL 与角色的关联关系,无需依赖数据库,适用于角色和 URL 固定的简单项目(如小型内部工具)。

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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class UserConfig {

// 密码加密器(Spring Security 5+强制要求密码加密)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

// 内存用户:模拟2个用户(admin有ROLE_ADMIN,user有ROLE_USER)
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userManager = new InMemoryUserDetailsManager();

// 管理员用户:角色ROLE_ADMIN
userManager.createUser(User.withUsername("admin")
.password(passwordEncoder().encode("123456"))
.roles("ADMIN") // 等价于authorities("ROLE_ADMIN")
.build());

// 普通用户:角色ROLE_USER
userManager.createUser(User.withUsername("user")
.password(passwordEncoder().encode("123456"))
.roles("USER") // 等价于authorities("ROLE_USER")
.build());

return userManager;
}
}

然后在SecurityFilterChain中通过hasRole()/hasAnyRole()指定 URL 所需角色:

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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 关闭CSRF(前后端分离项目可关闭,传统项目需开启)
.csrf(csrf -> csrf.disable())
// 2. 配置URL-角色关联
.authorizeHttpRequests(auth -> auth
// 公开访问:首页、登录页
.requestMatchers("/", "/login").permitAll()
// 仅ROLE_ADMIN可访问:管理员后台
.requestMatchers("/admin/**").hasRole("ADMIN")
// ROLE_USER或ROLE_ADMIN可访问:用户中心
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// 其他所有请求需认证(无论角色)
.anyRequest().authenticated()
)
// 3. 配置默认登录页(Spring Security提供)
.formLogin(form -> form.permitAll());

return http.build();
}
}

这种用到很少的,但是动态角色分配,也就是从数据库中查出来的,是基于这个的思路来的

企业级项目中,角色、用户、URL 权限通常存储在数据库中(支持动态增删角色、调整权限),核心是通过「自定义UserDetailsService加载用户角色」+「动态配置 URL - 角色关联」实现。

首先是这样的数据库表结构如何设计,角色分配需至少 3 张表(用户表、角色表、用户 - 角色关联表),若需细粒度权限控制,可增加 “权限表” 和 “角色 - 权限关联表”,此处以 “用户 - 角色” 二级模型为例:

表名 核心字段 说明
sys_user id, username, password, status 存储用户基本信息
sys_role id, role_name, role_code, remark 存储角色(如 role_code=ROLE_ADMIN)
sys_user_role id, user_id, role_id 用户与角色的多对多关联

然后,通过UserDetailsService从数据库查询用户,并将用户的SysRole转换为 Spring Security 认可的GrantedAuthority

所以,这时候,我们就要拿出来 实现了 UserDetailsService 接口的 CustomUserDetailsService 类来提供用户信息,因为我们这时候也需要 loadUserByUsername(String username) 方法,这个方法的作用是什么再说一次

是根据用户名(或其他唯一标识,如手机号、邮箱)从数据源(数据库、缓存、LDAP 等)中查询用户信息(包括用户名、密码、角色、权限等)。

然后将查询到的用户信息封装为 UserDetails 类型的对象返回,供 Spring Security 内部使用。

授权的前提是明确当前登录用户拥有哪些角色(如 ROLE_ADMINROLE_USER)或权限(如 user:read)。这些信息需要通过 CustomUserDetailsService 从数据源中查询并封装到 UserDetails 对象中。

这支撑了 Spring Security 的授权决策,Spring Security 在进行授权判断时,会从 UserDetails 对象中提取用户的角色 / 权限信息,与请求所需的角色 / 权限进行比对。如果没有 CustomUserDetailsService 提供的用户角色数据,授权决策就失去了依据。

在实际业务中,用户的角色可能会动态变化(如管理员在后台修改用户角色)。CustomUserDetailsService 每次认证时都会从数据源查询最新的角色信息,确保授权基于最新的用户角色状态。

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
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class CustomUserDetailsService implements UserDetailsService {

private final SysUserRepository sysUserRepository;

// 构造器注入(Spring 4.3+支持)
public CustomUserDetailsService(SysUserRepository sysUserRepository) {
this.sysUserRepository = sysUserRepository;
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查询用户(不存在则抛异常)
SysUser sysUser = sysUserRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));

// 2. 检查用户状态(禁用则抛异常)
if (sysUser.getStatus() == 0) {
throw new UsernameNotFoundException("用户已禁用:" + username);
}

// 3. 将用户的角色转换为GrantedAuthority(核心:角色编码必须带ROLE_前缀)
List<GrantedAuthority> authorities = sysUser.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleCode())) // 如"ROLE_ADMIN"
.collect(Collectors.toList());

// 4. 封装为Spring Security的UserDetails对象
return new User(
sysUser.getUsername(),
sysUser.getPassword(), // 数据库存储的是加密后的密码,无需再次加密
authorities
);
}
}

除了 URL 级别的角色分配,还可通过@PreAuthorize注解在Controller 方法上直接指定角色,实现更细粒度的控制(如同一 URL 的不同 HTTP 方法需不同角色)。

在配置类上添加@EnableMethodSecurity,然后在 Controller 方法上使用注解 @PreAuthorize就可以了就不细说了

实际编码

这个要写很多相关的dto,太几把多了,不写了,就说相关组件了

spring security 进行授权是这样的一个过程

  1. 请求拦截FilterSecurityInterceptor 拦截请求
  2. 权限匹配CustomFilterInvocationSecurityMetadataSource 根据 URL + Method 查找所需权限
  3. 权限决策CustomAccessDecisionManager 检查用户是否拥有所需权限
  4. 访问控制:根据决策结果允许或拒绝访问

首先,我们肯定是要写一个关于角色分配的服务,这里repository也要进行扩展

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
/**
* 角色分配服务
* 提供角色分配相关的业务逻辑
*/
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class RoleAssignmentService {

private final UserRepository userRepository;
private final RoleRepository roleRepository;

/**
* 为单个用户分配角色
* @param userId 用户ID
* @param roleName 角色名称
* @param reason 分配原因
* @return 分配结果
*/
public RoleAssignmentDto.RoleAssignmentResponse assignRoleToUser(Long userId, String roleName, String reason) {
log.info("为用户 {} 分配角色 {}, 原因: {}", userId, roleName, reason);

try {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("用户不存在: " + userId));

Role role = roleRepository.findByName(roleName)
.orElseThrow(() -> new RuntimeException("角色不存在: " + roleName));

// 检查用户是否已有该角色
if (user.getRoles().contains(role)) {
log.warn("用户 {} 已经拥有角色 {}", user.getUsername(), roleName);
return new RoleAssignmentDto.RoleAssignmentResponse(
userId, user.getUsername(), roleName, "ASSIGN",
false, "用户已拥有该角色", LocalDateTime.now()
);
}

// 分配角色
user.addRole(role);
userRepository.save(user);

log.info("成功为用户 {} 分配角色 {}", user.getUsername(), roleName);
return new RoleAssignmentDto.RoleAssignmentResponse(
userId, user.getUsername(), roleName, "ASSIGN",
true, null, LocalDateTime.now()
);

} catch (Exception e) {
log.error("为用户 {} 分配角色 {} 失败: {}", userId, roleName, e.getMessage());
return new RoleAssignmentDto.RoleAssignmentResponse(
userId, null, roleName, "ASSIGN",
false, e.getMessage(), LocalDateTime.now()
);
}
}

/**
* 移除用户角色
* @param userId 用户ID
* @param roleName 角色名称
* @param reason 移除原因
* @return 操作结果
*/
public RoleAssignmentDto.RoleAssignmentResponse removeRoleFromUser(Long userId, String roleName, String reason) {
log.info("移除用户 {} 的角色 {}, 原因: {}", userId, roleName, reason);

try {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("用户不存在: " + userId));

Role role = roleRepository.findByName(roleName)
.orElseThrow(() -> new RuntimeException("角色不存在: " + roleName));

// 检查用户是否有该角色
if (!user.getRoles().contains(role)) {
log.warn("用户 {} 没有角色 {}", user.getUsername(), roleName);
return new RoleAssignmentDto.RoleAssignmentResponse(
userId, user.getUsername(), roleName, "REMOVE",
false, "用户没有该角色", LocalDateTime.now()
);
}

// 移除角色
user.removeRole(role);
userRepository.save(user);

log.info("成功移除用户 {} 的角色 {}", user.getUsername(), roleName);
return new RoleAssignmentDto.RoleAssignmentResponse(
userId, user.getUsername(), roleName, "REMOVE",
true, null, LocalDateTime.now()
);

} catch (Exception e) {
log.error("移除用户 {} 的角色 {} 失败: {}", userId, roleName, e.getMessage());
return new RoleAssignmentDto.RoleAssignmentResponse(
userId, null, roleName, "REMOVE",
false, e.getMessage(), LocalDateTime.now()
);
}
}

/**
* 批量角色分配
* @param request 批量分配请求
* @return 分配结果列表
*/
public List<RoleAssignmentDto.RoleAssignmentResponse> batchRoleAssignment(
RoleAssignmentDto.BatchRoleAssignmentRequest request) {

log.info("开始批量角色分配: 操作={}, 用户数={}, 角色数={}",
request.getOperation(), request.getUserIds().size(), request.getRoleNames().size());

List<RoleAssignmentDto.RoleAssignmentResponse> results = new ArrayList<>();

// 为每个用户分配/移除所有指定角色
for (Long userId : request.getUserIds()) {
for (String roleName : request.getRoleNames()) {
RoleAssignmentDto.RoleAssignmentResponse result;

if ("ASSIGN".equalsIgnoreCase(request.getOperation())) {
result = assignRoleToUser(userId, roleName, request.getReason());
} else if ("REMOVE".equalsIgnoreCase(request.getOperation())) {
result = removeRoleFromUser(userId, roleName, request.getReason());
} else {
result = new RoleAssignmentDto.RoleAssignmentResponse(
userId, null, roleName, request.getOperation(),
false, "不支持的操作类型: " + request.getOperation(), LocalDateTime.now()
);
}

results.add(result);
}
}

long successCount = results.stream().mapToLong(r -> r.isSuccess() ? 1 : 0).sum();
log.info("批量角色分配完成: 总数={}, 成功={}, 失败={}",
results.size(), successCount, results.size() - successCount);

return results;
}

/**
* 获取用户的角色和权限信息
* @param userId 用户ID
* @return 用户角色信息
*/
@Transactional(readOnly = true)
public RoleAssignmentDto.UserRoleInfo getUserRoleInfo(Long userId) {
User user = userRepository.findByIdWithRolesAndPermissions(userId)
.orElseThrow(() -> new RuntimeException("用户不存在: " + userId));

// 收集所有角色名称
List<String> roleNames = user.getRoles().stream()
.map(Role::getName)
.sorted()
.collect(Collectors.toList());

// 收集所有权限名称(去重)
Set<String> permissionSet = user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(Permission::getName)
.collect(Collectors.toSet());

List<String> permissions = permissionSet.stream()
.sorted()
.collect(Collectors.toList());

return new RoleAssignmentDto.UserRoleInfo(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getEnabled(),
roleNames,
permissions,
user.getCreatedAt()
);
}

/**
* 获取角色的权限信息
* @param roleName 角色名称
* @return 角色权限信息
*/
@Transactional(readOnly = true)
public RoleAssignmentDto.RolePermissionInfo getRolePermissionInfo(String roleName) {
Role role = roleRepository.findByNameWithUsersAndPermissions(roleName)
.orElseThrow(() -> new RuntimeException("角色不存在: " + roleName));

// 收集权限名称
List<String> permissions = role.getPermissions().stream()
.map(Permission::getName)
.sorted()
.collect(Collectors.toList());

return new RoleAssignmentDto.RolePermissionInfo(
role.getId(),
role.getName(),
role.getDescription(),
role.getUsers().size(),
permissions,
role.getCreatedAt()
);
}

/**
* 获取所有角色的简要信息
* @return 角色信息列表
*/
@Transactional(readOnly = true)
public List<RoleAssignmentDto.RolePermissionInfo> getAllRolesInfo() {
List<Role> roles = roleRepository.findAllWithUsersAndPermissions();

return roles.stream()
.map(role -> {
List<String> permissions = role.getPermissions().stream()
.map(Permission::getName)
.sorted()
.collect(Collectors.toList());

return new RoleAssignmentDto.RolePermissionInfo(
role.getId(),
role.getName(),
role.getDescription(),
role.getUsers().size(),
permissions,
role.getCreatedAt()
);
})
.collect(Collectors.toList());
}

/**
* 检查用户是否具有指定角色
* @param userId 用户ID
* @param roleName 角色名称
* @return 是否具有角色
*/
@Transactional(readOnly = true)
public boolean userHasRole(Long userId, String roleName) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("用户不存在: " + userId));

return user.getRoles().stream()
.anyMatch(role -> role.getName().equals(roleName));
}

/**
* 获取拥有指定角色的所有用户
* @param roleName 角色名称
* @return 用户角色信息列表
*/
@Transactional(readOnly = true)
public List<RoleAssignmentDto.UserRoleInfo> getUsersByRole(String roleName) {
Role role = roleRepository.findByNameWithUsersAndPermissions(roleName)
.orElseThrow(() -> new RuntimeException("角色不存在: " + roleName));

return role.getUsers().stream()
.map(user -> {
List<String> roleNames = user.getRoles().stream()
.map(Role::getName)
.sorted()
.collect(Collectors.toList());

Set<String> permissionSet = user.getRoles().stream()
.flatMap(r -> r.getPermissions().stream())
.map(Permission::getName)
.collect(Collectors.toSet());

List<String> permissions = permissionSet.stream()
.sorted()
.collect(Collectors.toList());

return new RoleAssignmentDto.UserRoleInfo(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getEnabled(),
roleNames,
permissions,
user.getCreatedAt()
);
})
.collect(Collectors.toList());
}
}

然后直接写对应的控制器就行了,这里我没有展示,但是,我要说之前写过的一个东西

就是 CustomFilterInvocationSecurityMetadataSource 这个类,通过通过 Ant 路径匹配器将请求 URL 和 HTTP 方法与数据库中的权限配置进行匹配,分配完了在这里进行校验,是 Spring Security 基于 Request 授权的核心组件,它实现了动态的 URL-权限映射机制。

当请求到达时,Spring Security 的过滤器链会调用该方法,获取当前请求所需的权限(ConfigAttribute 集合)。

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
// // 根据当前请求(URL + HTTP 方法),从数据源中查询访问该资源所需的权限(或角色),并封装成 ConfigAttribute 集合返回给 Spring Security,供后续授权决策使用。
@Override
public Collection<ConfigAttribute> getAttributes(Object object) {
// 通过强制转换获取 FilterInvocation 对象,用于提取请求的 URL 和 HTTP 方法
FilterInvocation filterInvocation = (FilterInvocation) object;
String requestUrl = filterInvocation.getRequestUrl(); // 请求的路径,如 "/api/users/123"
String httpMethod = filterInvocation.getRequest().getMethod(); // HTTP方法,如 "GET"

// 数据源(数据库等)中查询所有预定义的权限配置
List<Permission> allPermissions = permissionRepository.findAll();

// 遍历所有权限配置,判断当前请求(URL + 方法)是否与权限配置中的 resource(URL 模式)和 action(HTTP 方法)匹配。
List<ConfigAttribute> configAttributes = new ArrayList<>();
for (Permission permission : allPermissions) {
// 双重匹配:URL模式 + HTTP方法
if (pathMatcher.match(permission.getResource(), requestUrl) &&
permission.getAction().equalsIgnoreCase(httpMethod)) {

String authority = permission.getAuthority(); // "GET:/api/users/**"
configAttributes.add(new SecurityConfig(authority));
}
}

return configAttributes.isEmpty() ? null : configAttributes;
}

该方法实现了 “动态权限元数据提取”,这也是实现不同情况不同权限的核心

  1. 当请求到达时,Spring Security 的过滤器链会调用该方法,获取当前请求所需的权限(ConfigAttribute 集合)。
  2. Spring Security 会将这些权限与当前登录用户的权限(从 UserDetailsService 中获取)进行比对。
  3. 如果用户拥有所需权限,则允许访问;否则,触发 AccessDeniedException,由 AccessDeniedHandler 处理(即 “未授权” 场景)。

我们进行测试

image-20250928212907783
image-20250928212926941