18 KiB
rdms-spring-boot-starter-security
1. 模块定位
rdms-spring-boot-starter-security 是项目中的安全基础模块,当前实际包含两块能力:
- 安全认证与权限校验
- 操作日志记录
这个模块不是单纯引入 Spring Security 依赖,而是已经在项目里落了完整的自动装配链路,包括:
- 基于 Token 的无状态认证
- 登录用户上下文维护
- URL 与方法级权限控制
- 跨服务
LoginUser透传 - 401 / 403 统一返回
- 操作日志采集与异步上报
2. 设计思路
2.1 认证与权限分层
这个模块把安全相关能力拆成了几层:
-
配置层
通过SecurityProperties统一管理 token 请求头、token 参数名、白名单、mock 登录、密码加密强度等配置。 -
过滤器层
通过TokenAuthenticationFilter解析请求中的登录信息,并把LoginUser放入 Spring Security 上下文。 -
Spring Security 配置层
通过SecurityFilterChain统一配置无状态认证、放行规则、异常处理和过滤器顺序。 -
权限服务层
通过SecurityFrameworkService对外提供权限、角色、scope 判断能力,供@PreAuthorize等表达式直接使用。 -
RPC 透传层
通过 FeignRequestInterceptor把当前LoginUser继续透传给下游服务。
2.2 当前认证链路
当前项目的认证链路可以理解为:
- 请求进入服务
- 优先尝试从
login-user请求头恢复登录用户
适用于网关或上游服务已经完成认证并透传用户信息的场景 - 如果没有
login-user,再尝试从Authorization或请求参数中获取 token - 通过
OAuth2TokenCommonApi校验 token - 构造
LoginUser - 放入 Spring Security 上下文,供后续权限判断、业务代码、日志记录使用
这意味着当前项目跨服务调用时,主要透传的是 LoginUser,而不是原始 Authorization。
2.3 权限判断思路
权限判断没有把权限数据直接塞进本地配置,而是通过远程接口获取:
- 权限校验走
PermissionCommonApi - token 校验走
OAuth2TokenCommonApi
也就是说,这个模块负责“接入和执行安全规则”,权限与 token 数据本身仍然由系统服务提供。
2.4 操作日志思路
操作日志能力不是自己从零实现注解解析,而是集成 bizlog-sdk:
- 业务代码通过
@LogRecord声明日志 - 本模块提供
ILogRecordService实现 - 最终通过
OperateLogCommonApi异步上报操作日志
这样做的特点是:
- 业务代码侧书写简单
- 日志记录逻辑统一
- 日志存储仍集中在系统服务
3. 自动装配入口
当前自动装配入口在:
RdmsSecurityRpcAutoConfigurationRdmsSecurityAutoConfigurationRdmsWebSecurityConfigurerAdapterRdmsOperateLogConfigurationRdmsOperateLogRpcAutoConfiguration
说明这个 starter 当前已经不是占位模块,而是有完整自动配置入口的基础模块。
4. 核心功能点
4.1 基于 Token 的无状态认证
通过 SecurityFilterChain 配置为无状态模式:
- 禁用 Session
- 禁用表单登录
- 禁用 httpBasic
- 通过自定义
TokenAuthenticationFilter完成登录态识别
这意味着服务本身不维护 Session,认证主要依赖 token 或上游透传的 login-user。
4.2 登录用户上下文维护
模块内定义了 LoginUser,当前主要包含这些信息:
iduserTypeinfoscopesexpiresTime
认证成功后,LoginUser 会进入 Spring Security 上下文,后续可以通过 SecurityFrameworkUtils 获取,例如:
- 当前用户 ID
- 当前用户昵称
- 当前用户部门 ID
4.3 支持两种登录信息来源
TokenAuthenticationFilter 当前支持两种方式恢复登录用户:
-
从
login-user请求头恢复
适合网关转发、服务间调用、Feign 透传场景 -
从 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) {
...
}
当前支持的能力包括:
hasPermissionhasAnyPermissionshasRolehasAnyRoleshasScopehasAnyScopes
4.7 权限与角色远程校验
权限、角色判断不是本地硬编码,而是通过 PermissionCommonApi 远程校验。
为了减少频繁 RPC 调用,当前还做了本地 Guava 缓存:
- 权限判断缓存 1 分钟
- 角色判断缓存 1 分钟
scope 判断则直接基于当前 LoginUser.scopes 完成。
4.8 401 / 403 统一处理
模块内提供了统一异常处理器:
AuthenticationEntryPointImpl:未登录访问需要认证的资源时返回 401AccessDeniedHandlerImpl:已登录但权限不足时返回 403
这样前端收到的是统一 JSON 结构,而不是默认的 Spring Security 页面或跳转行为。
4.9 SecurityContext 跨线程传递
模块把 SecurityContextHolder 的策略切换成了 TransmittableThreadLocalSecurityContextHolderStrategy。
作用是:
- 在
@Async等异步线程场景下,尽量保留当前安全上下文 - 减少使用普通
ThreadLocal时上下文丢失的问题
4.10 支持开发期 mock 登录
当配置开启后,可以使用 mock token 构造登录用户,便于本地调试。
这块能力由 SecurityProperties 中的以下配置控制:
mock-enablemock-secret
这类能力只适合开发调试环境,不适合生产环境开启。
4.11 操作日志能力
这个模块还同时集成了操作日志能力。
当前实现方式是:
- 通过
@EnableLogRecord开启日志能力 - 业务方法上使用
@LogRecord - 本模块的
LogRecordServiceImpl收集日志内容 - 自动补充用户信息、请求信息、链路追踪信息
- 通过
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 的字段名,默认是Authorizationtoken-parameter:请求参数里 token 的字段名,主要兼容不能方便传 header 的场景mock-enable/mock-secret:开发期 mock 登录控制permit-all-urls:额外白名单 URLpassword-encoder-length:BCrypt 加密强度
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,还会继续做权限校验
也就是说,一个普通接口最常见的开发方式是:
- 默认需要登录
- 再按需要加权限注解
例如:
@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 接口可以按三类理解
从实际开发角度,可以把接口简单分成三类:
-
匿名可访问
这类接口不要求登录。
常见做法:- 使用
@PermitAll - 或加入
rdms.security.permit-all-urls白名单
- 使用
-
登录即可访问
这类接口要求登录,但不校验具体 permission。
常见做法:- 不加
@PermitAll - 不加白名单
- 也不加
@PreAuthorize
在当前配置下,这类接口会被默认的
authenticated()规则保护:- 未登录无法访问
- 已登录可以访问
- 不加
-
登录后还要校验具体权限
这类接口除了要求登录,还要求具备具体权限点。
常见做法:- 使用
@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 为什么跨服务还能拿到当前用户
当前项目的链路是这样的:
- 前端把 token 传给网关
- 网关校验 token 后,把用户信息转成
login-user请求头 - 下游服务读取
login-user,恢复LoginUser - 如果下游服务再通过 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 开发时最容易混淆的几个点
-
这个模块负责“认证接入和权限执行”,不负责 token 签发
token 的签发和存储仍在系统服务等其他模块。 -
当前跨服务透传的是
login-user,不是原始 token
所以下游服务能识别当前用户,不等于它一直拿着原始Authorization。 -
单体部署(不经过网关)建议在入口层剥离
login-user请求头,避免外部伪造登录态- Nginx 示例:
# 去除客户端伪造的 login-user 头 proxy_set_header login-user "";
- Nginx 示例:
-
不加
@PreAuthorize不代表匿名可访问
默认仍然需要登录,只是不会进一步做权限点校验。 -
@PermitAll和permit-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 透传
- 更细的权限表达式
- 更完整的审计日志字段