Files
cn-rdms/rdms-framework/rdms-spring-boot-starter-security/README.md
2026-03-12 19:45:27 +08:00

18 KiB
Raw Blame History

rdms-spring-boot-starter-security

1. 模块定位

rdms-spring-boot-starter-security 是项目中的安全基础模块,当前实际包含两块能力:

  • 安全认证与权限校验
  • 操作日志记录

这个模块不是单纯引入 Spring Security 依赖,而是已经在项目里落了完整的自动装配链路,包括:

  • 基于 Token 的无状态认证
  • 登录用户上下文维护
  • URL 与方法级权限控制
  • 跨服务 LoginUser 透传
  • 401 / 403 统一返回
  • 操作日志采集与异步上报

2. 设计思路

2.1 认证与权限分层

这个模块把安全相关能力拆成了几层:

  1. 配置层
    通过 SecurityProperties 统一管理 token 请求头、token 参数名、白名单、mock 登录、密码加密强度等配置。

  2. 过滤器层
    通过 TokenAuthenticationFilter 解析请求中的登录信息,并把 LoginUser 放入 Spring Security 上下文。

  3. Spring Security 配置层
    通过 SecurityFilterChain 统一配置无状态认证、放行规则、异常处理和过滤器顺序。

  4. 权限服务层
    通过 SecurityFrameworkService 对外提供权限、角色、scope 判断能力,供 @PreAuthorize 等表达式直接使用。

  5. RPC 透传层
    通过 Feign RequestInterceptor 把当前 LoginUser 继续透传给下游服务。

2.2 当前认证链路

当前项目的认证链路可以理解为:

  1. 请求进入服务
  2. 优先尝试从 login-user 请求头恢复登录用户
    适用于网关或上游服务已经完成认证并透传用户信息的场景
  3. 如果没有 login-user,再尝试从 Authorization 或请求参数中获取 token
  4. 通过 OAuth2TokenCommonApi 校验 token
  5. 构造 LoginUser
  6. 放入 Spring Security 上下文,供后续权限判断、业务代码、日志记录使用

这意味着当前项目跨服务调用时,主要透传的是 LoginUser,而不是原始 Authorization

2.3 权限判断思路

权限判断没有把权限数据直接塞进本地配置,而是通过远程接口获取:

  • 权限校验走 PermissionCommonApi
  • token 校验走 OAuth2TokenCommonApi

也就是说,这个模块负责“接入和执行安全规则”,权限与 token 数据本身仍然由系统服务提供。

2.4 操作日志思路

操作日志能力不是自己从零实现注解解析,而是集成 bizlog-sdk

  • 业务代码通过 @LogRecord 声明日志
  • 本模块提供 ILogRecordService 实现
  • 最终通过 OperateLogCommonApi 异步上报操作日志

这样做的特点是:

  • 业务代码侧书写简单
  • 日志记录逻辑统一
  • 日志存储仍集中在系统服务

3. 自动装配入口

当前自动装配入口在:

  • RdmsSecurityRpcAutoConfiguration
  • RdmsSecurityAutoConfiguration
  • RdmsWebSecurityConfigurerAdapter
  • RdmsOperateLogConfiguration
  • RdmsOperateLogRpcAutoConfiguration

说明这个 starter 当前已经不是占位模块,而是有完整自动配置入口的基础模块。

4. 核心功能点

4.1 基于 Token 的无状态认证

通过 SecurityFilterChain 配置为无状态模式:

  • 禁用 Session
  • 禁用表单登录
  • 禁用 httpBasic
  • 通过自定义 TokenAuthenticationFilter 完成登录态识别

这意味着服务本身不维护 Session认证主要依赖 token 或上游透传的 login-user

4.2 登录用户上下文维护

模块内定义了 LoginUser,当前主要包含这些信息:

  • id
  • userType
  • info
  • scopes
  • expiresTime

认证成功后,LoginUser 会进入 Spring Security 上下文,后续可以通过 SecurityFrameworkUtils 获取,例如:

  • 当前用户 ID
  • 当前用户昵称
  • 当前用户部门 ID

4.3 支持两种登录信息来源

TokenAuthenticationFilter 当前支持两种方式恢复登录用户:

  1. login-user 请求头恢复
    适合网关转发、服务间调用、Feign 透传场景

  2. 从 token 恢复
    适合直接请求服务、没有上游透传 login-user 的场景

这也是为什么当前服务间调用时,即使不继续透传原始 token下游仍然可以识别当前登录用户。

4.4 跨服务 LoginUser 透传

模块内提供了 LoginUserRequestInterceptor

作用是:

  • 发起 Feign 请求时,读取当前线程中的 LoginUser
  • 将其序列化后放到 login-user 请求头
  • 下游服务再通过 TokenAuthenticationFilter 还原为登录用户上下文

当前项目跨服务透传的重点是“登录用户信息透传”,不是“原始 token 透传”。

4.5 URL 级访问控制

RdmsWebSecurityConfigurerAdapter 里统一配置了请求访问规则,当前支持:

  • 静态资源默认放行
  • @PermitAll 标注的方法或类自动放行
  • rdms.security.permit-all-urls 配置的 URL 放行
  • 各业务模块通过 AuthorizeRequestsCustomizer 继续追加自定义规则
  • 其余请求默认需要认证

其中 AuthorizeRequestsCustomizer 是一个扩展点,适合各模块自己补充 URL 安全规则。

例如 WebSocket 模块就是通过继承这个类,把自己的连接地址加入放行规则。

4.6 方法级权限校验

模块开启了 @EnableMethodSecurity,并注册了名为 ss 的权限服务 Bean。

因此业务代码可以直接写:

@PreAuthorize("@ss.hasPermission('system:user:create')")
public Long createUser(UserSaveReqVO reqVO) {
    ...
}

当前支持的能力包括:

  • hasPermission
  • hasAnyPermissions
  • hasRole
  • hasAnyRoles
  • hasScope
  • hasAnyScopes

4.7 权限与角色远程校验

权限、角色判断不是本地硬编码,而是通过 PermissionCommonApi 远程校验。

为了减少频繁 RPC 调用,当前还做了本地 Guava 缓存:

  • 权限判断缓存 1 分钟
  • 角色判断缓存 1 分钟

scope 判断则直接基于当前 LoginUser.scopes 完成。

4.8 401 / 403 统一处理

模块内提供了统一异常处理器:

  • AuthenticationEntryPointImpl:未登录访问需要认证的资源时返回 401
  • AccessDeniedHandlerImpl:已登录但权限不足时返回 403

这样前端收到的是统一 JSON 结构,而不是默认的 Spring Security 页面或跳转行为。

4.9 SecurityContext 跨线程传递

模块把 SecurityContextHolder 的策略切换成了 TransmittableThreadLocalSecurityContextHolderStrategy

作用是:

  • @Async 等异步线程场景下,尽量保留当前安全上下文
  • 减少使用普通 ThreadLocal 时上下文丢失的问题

4.10 支持开发期 mock 登录

当配置开启后,可以使用 mock token 构造登录用户,便于本地调试。

这块能力由 SecurityProperties 中的以下配置控制:

  • mock-enable
  • mock-secret

这类能力只适合开发调试环境,不适合生产环境开启。

4.11 操作日志能力

这个模块还同时集成了操作日志能力。

当前实现方式是:

  1. 通过 @EnableLogRecord 开启日志能力
  2. 业务方法上使用 @LogRecord
  3. 本模块的 LogRecordServiceImpl 收集日志内容
  4. 自动补充用户信息、请求信息、链路追踪信息
  5. 通过 OperateLogCommonApi 异步上报

日志中会补充的典型信息包括:

  • 用户 ID
  • 用户类型
  • 请求方法
  • 请求 URL
  • 请求 IP
  • User-Agent
  • TraceId

5. 配置项

当前可见的核心配置在 rdms.security 下:

rdms:
  security:
    token-header: Authorization
    token-parameter: token
    mock-enable: false
    mock-secret: test
    permit-all-urls:
      - /admin-api/system/auth/login
    password-encoder-length: 4

说明:

  • token-header:请求头里 token 的字段名,默认是 Authorization
  • token-parameter:请求参数里 token 的字段名,主要兼容不能方便传 header 的场景
  • mock-enable / mock-secret:开发期 mock 登录控制
  • permit-all-urls:额外白名单 URL
  • password-encoder-lengthBCrypt 加密强度

6. 快速上手

如果从“一个开发者接手后该怎么用”来理解,这个模块最常见的上手路径如下。

6.1 引入依赖后,会自动得到什么

业务模块引入这个 starter 后,默认会自动得到这些能力:

  • Spring Security 过滤链
  • Token 认证过滤器
  • @EnableMethodSecurity
  • @ss 权限表达式 Bean
  • 401 / 403 统一返回
  • Feign 的 login-user 透传
  • @LogRecord 操作日志能力

也就是说,通常不需要你再手写一套 Security 配置类,除非业务模块有额外的特殊规则。

6.2 最小配置怎么写

最常见的基础配置如下:

rdms:
  security:
    token-header: Authorization
    token-parameter: token
    mock-enable: false
    permit-all-urls:
      - /admin-api/system/auth/login
      - /admin-api/system/auth/refresh-token

如果没有特殊需求,通常只需要确认:

  • 哪些接口要放行
  • token 从哪个 header 取

6.3 一个新接口,默认是什么状态

默认情况下:

  • 没有显式放行的接口,都会要求登录
  • 已登录后,如果方法上写了 @PreAuthorize,还会继续做权限校验

也就是说,一个普通接口最常见的开发方式是:

  1. 默认需要登录
  2. 再按需要加权限注解

例如:

@PreAuthorize("@ss.hasPermission('system:user:query')")
@GetMapping("/page")
public CommonResult<PageResult<UserRespVO>> getUserPage(UserPageReqVO reqVO) {
    ...
}

6.4 哪些接口不需要登录,怎么做

有两种常用方式。

方式一:在接口或类上标 @PermitAll

@PermitAll
@GetMapping("/public-info")
public CommonResult<String> getPublicInfo() {
    ...
}

方式二:通过配置白名单放行

rdms:
  security:
    permit-all-urls:
      - /admin-api/system/auth/login
      - /admin-api/system/auth/refresh-token

实践建议:

  • 固定的公共接口、登录接口、刷新 token 接口,更适合放配置白名单
  • 个别无需登录的业务接口,更适合直接写 @PermitAll

6.5 需要权限控制,怎么做

最常用的是在 Controller 或 Service 方法上写 @PreAuthorize

@PreAuthorize("@ss.hasPermission('system:user:create')")
@PostMapping("/create")
public CommonResult<Long> createUser(@Valid @RequestBody UserSaveReqVO reqVO) {
    ...
}

当前可直接使用的表达式包括:

  • @ss.hasPermission('xxx')
  • @ss.hasAnyPermissions('a', 'b')
  • @ss.hasRole('admin')
  • @ss.hasAnyRoles('admin', 'manager')
  • @ss.hasScope('user.read')
  • @ss.hasAnyScopes('a', 'b')

最常见的是 hasPermission

6.5.1 接口可以按三类理解

从实际开发角度,可以把接口简单分成三类:

  1. 匿名可访问
    这类接口不要求登录。
    常见做法:

    • 使用 @PermitAll
    • 或加入 rdms.security.permit-all-urls 白名单
  2. 登录即可访问
    这类接口要求登录,但不校验具体 permission。
    常见做法:

    • 不加 @PermitAll
    • 不加白名单
    • 也不加 @PreAuthorize

    在当前配置下,这类接口会被默认的 authenticated() 规则保护:

    • 未登录无法访问
    • 已登录可以访问
  3. 登录后还要校验具体权限
    这类接口除了要求登录,还要求具备具体权限点。
    常见做法:

    • 使用 @PreAuthorize("@ss.hasPermission('xxx')")

一个比较实用的开发策略是:

  • 大部分普通接口只要求登录
  • 删除、导出、审批、分配等敏感动作再加具体权限校验

6.6 权限校验是怎么触发的

很多时候容易疑惑:@PreAuthorize("@ss.hasPermission('system:dept:update')") 这行代码本身没有手动调用 PermissionCommonApi,那远程权限校验是什么时候发生的?

当前链路可以直接理解成下面这几步:

请求进入服务
-> TokenAuthenticationFilter 完成登录态恢复
-> 请求到达 Controller / Service 方法前
-> Spring Security 拦截 @PreAuthorize
-> 解析表达式 @ss.hasPermission('system:dept:update')
-> 调用 Spring 容器里的 ss Bean
-> 进入 SecurityFrameworkServiceImpl.hasPermission(...)
-> 转到 hasAnyPermissions(...)
-> 先查本地 1 分钟缓存
-> 缓存未命中时,调用 PermissionCommonApi 远程判断权限
-> 返回 true允许进入方法
-> 返回 false拒绝访问返回 403

可以把它理解为:

  • @PreAuthorize 负责“触发权限判断”
  • @ss 负责“找到权限判断 Bean”
  • SecurityFrameworkServiceImpl 负责“执行权限判断逻辑”
  • PermissionCommonApi 负责“向权限服务查询结果”

所以真正触发 PermissionCommonApi 的,不是 Controller 代码手动调用,而是 Spring Security 在执行 @PreAuthorize 表达式时自动触发。

6.7 业务代码里怎么拿当前登录用户

最常用的是通过 SecurityFrameworkUtils 获取:

Long userId = SecurityFrameworkUtils.getLoginUserId();
String nickname = SecurityFrameworkUtils.getLoginUserNickname();
Long deptId = SecurityFrameworkUtils.getLoginUserDeptId();

如果需要更完整的信息:

LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();

适用场景:

  • 新增、修改数据时记录操作人
  • 查询当前用户自己的数据
  • 记录审计字段

6.8 为什么跨服务还能拿到当前用户

当前项目的链路是这样的:

  1. 前端把 token 传给网关
  2. 网关校验 token 后,把用户信息转成 login-user 请求头
  3. 下游服务读取 login-user,恢复 LoginUser
  4. 如果下游服务再通过 Feign 调别的服务,LoginUserRequestInterceptor 会继续透传 login-user

所以开发时可以这样理解:

  • Web 入口主要靠 token 建立登录态
  • 服务间调用主要靠 login-user 继续传递登录态

6.9 什么时候需要自己补 URL 规则

如果某个模块有特殊放行需求,可以继承 AuthorizeRequestsCustomizer

例如:

public class XxxAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {

    @Override
    public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
        registry.requestMatchers("/xxx/**").permitAll();
    }
}

常见场景:

  • WebSocket 握手地址
  • 第三方回调接口
  • 某些模块自己的公共入口

6.10 需要记录操作日志,怎么做

在业务方法上直接加 @LogRecord

@LogRecord(type = "SYSTEM_USER", subType = "CREATE", bizNo = "{{#user.id}}",
        success = "创建用户成功")
public Long createUser(UserSaveReqVO reqVO) {
    ...
}

这个注解更适合加在“有明确业务动作”的方法上,例如:

  • 新增用户
  • 修改角色
  • 删除字典
  • 分配权限

6.11 开发时最容易混淆的几个点

  1. 这个模块负责“认证接入和权限执行”,不负责 token 签发
    token 的签发和存储仍在系统服务等其他模块。

  2. 当前跨服务透传的是 login-user,不是原始 token
    所以下游服务能识别当前用户,不等于它一直拿着原始 Authorization

  3. 单体部署(不经过网关)建议在入口层剥离 login-user 请求头,避免外部伪造登录态

    • Nginx 示例:
      # 去除客户端伪造的 login-user 头
      proxy_set_header login-user "";
      
  4. 不加 @PreAuthorize 不代表匿名可访问
    默认仍然需要登录,只是不会进一步做权限点校验。

  5. @PermitAllpermit-all-urls 都是放行登录校验
    放行后通常也不会再走权限控制。

7. 常见使用方式

7.1 方法权限控制

@PreAuthorize("@ss.hasPermission('system:user:query')")
@GetMapping("/page")
public CommonResult<PageResult<UserRespVO>> getUserPage(UserPageReqVO reqVO) {
    ...
}

适用场景:

  • 控制某个接口必须具备某个权限点
  • 细粒度控制新增、修改、删除、导出等操作

7.2 放行无需登录的接口

方式一:直接标注 @PermitAll

@PermitAll
@GetMapping("/public-info")
public CommonResult<String> getPublicInfo() {
    ...
}

方式二:通过配置加入白名单

rdms:
  security:
    permit-all-urls:
      - /admin-api/system/auth/login
      - /admin-api/system/auth/refresh-token

7.3 为某个模块补充 URL 安全规则

如果某个模块有自己的特殊放行需求,可以继承 AuthorizeRequestsCustomizer

public class XxxAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {

    @Override
    public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
        registry.requestMatchers("/xxx/**").permitAll();
    }
}

适用场景:

  • WebSocket 握手地址放行
  • 回调接口放行
  • 某些特殊模块追加自己的 URL 规则

7.4 记录操作日志

@LogRecord(type = "SYSTEM_USER", subType = "CREATE", bizNo = "{{#user.id}}",
        success = "创建用户成功")
public Long createUser(UserSaveReqVO reqVO) {
    ...
}

适用场景:

  • 新增、修改、删除等需要审计的核心业务动作
  • 希望把操作人、请求来源、业务对象编号一起记录下来

8. 模块边界

阅读这个模块时,需要把“它负责什么”和“它不负责什么”分开看。

当前它负责:

  • 接入 Spring Security
  • 解析 token 或 login-user
  • 构建登录用户上下文
  • 提供权限判断能力
  • 统一未登录 / 未授权响应
  • 透传 LoginUser
  • 集成操作日志记录

当前它不负责:

  • token 的签发
  • 权限数据本身的维护
  • 用户、角色、菜单、权限模型的存储
  • 原始 Authorization 的 Feign 透传

这些数据来源和业务模型,当前仍由系统服务等其他模块提供。

9. 总结

rdms-spring-boot-starter-security 当前已经是一个实际落地的基础模块,不是占位模块。

从当前代码看,它的核心价值在于:

  • 统一接入认证与权限体系
  • 把登录用户上下文在 Web、Spring Security、Feign 调用之间串起来
  • 给业务层提供简单直接的权限表达式入口
  • 把操作日志能力收进统一基础设施

如果后续继续扩展,这个模块仍然适合承接更多安全通用能力,例如:

  • 原始 token 透传
  • 更细的权限表达式
  • 更完整的审计日志字段