# 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` - `tenantId` - `scopes` - `expiresTime` - `visitTenantId` 认证成功后,`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。 因此业务代码可以直接写: ```java @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` 下: ```yaml 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-length`:BCrypt 加密强度 ## 6. 快速上手 如果从“一个开发者接手后该怎么用”来理解,这个模块最常见的上手路径如下。 ### 6.1 引入依赖后,会自动得到什么 业务模块引入这个 starter 后,默认会自动得到这些能力: - Spring Security 过滤链 - Token 认证过滤器 - `@EnableMethodSecurity` - `@ss` 权限表达式 Bean - 401 / 403 统一返回 - Feign 的 `login-user` 透传 - `@LogRecord` 操作日志能力 也就是说,通常不需要你再手写一套 Security 配置类,除非业务模块有额外的特殊规则。 ### 6.2 最小配置怎么写 最常见的基础配置如下: ```yaml 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. 再按需要加权限注解 例如: ```java @PreAuthorize("@ss.hasPermission('system:user:query')") @GetMapping("/page") public CommonResult> getUserPage(UserPageReqVO reqVO) { ... } ``` ### 6.4 哪些接口不需要登录,怎么做 有两种常用方式。 方式一:在接口或类上标 `@PermitAll` ```java @PermitAll @GetMapping("/public-info") public CommonResult getPublicInfo() { ... } ``` 方式二:通过配置白名单放行 ```yaml rdms: security: permit-all-urls: - /admin-api/system/auth/login - /admin-api/system/auth/refresh-token ``` 实践建议: - 固定的公共接口、登录接口、刷新 token 接口,更适合放配置白名单 - 个别无需登录的业务接口,更适合直接写 `@PermitAll` ### 6.5 需要权限控制,怎么做 最常用的是在 Controller 或 Service 方法上写 `@PreAuthorize`: ```java @PreAuthorize("@ss.hasPermission('system:user:create')") @PostMapping("/create") public CommonResult 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`,那远程权限校验是什么时候发生的? 当前链路可以直接理解成下面这几步: ```text 请求进入服务 -> 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` 获取: ```java Long userId = SecurityFrameworkUtils.getLoginUserId(); String nickname = SecurityFrameworkUtils.getLoginUserNickname(); Long deptId = SecurityFrameworkUtils.getLoginUserDeptId(); ``` 如果需要更完整的信息: ```java 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`。 例如: ```java public class XxxAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { @Override public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { registry.requestMatchers("/xxx/**").permitAll(); } } ``` 常见场景: - WebSocket 握手地址 - 第三方回调接口 - 某些模块自己的公共入口 ### 6.10 需要记录操作日志,怎么做 在业务方法上直接加 `@LogRecord`: ```java @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. 不加 `@PreAuthorize` 不代表匿名可访问 默认仍然需要登录,只是不会进一步做权限点校验。 4. `@PermitAll` 和 `permit-all-urls` 都是放行登录校验 放行后通常也不会再走权限控制。 ## 7. 常见使用方式 ### 7.1 方法权限控制 ```java @PreAuthorize("@ss.hasPermission('system:user:query')") @GetMapping("/page") public CommonResult> getUserPage(UserPageReqVO reqVO) { ... } ``` 适用场景: - 控制某个接口必须具备某个权限点 - 细粒度控制新增、修改、删除、导出等操作 ### 7.2 放行无需登录的接口 方式一:直接标注 `@PermitAll` ```java @PermitAll @GetMapping("/public-info") public CommonResult getPublicInfo() { ... } ``` 方式二:通过配置加入白名单 ```yaml rdms: security: permit-all-urls: - /admin-api/system/auth/login - /admin-api/system/auth/refresh-token ``` ### 7.3 为某个模块补充 URL 安全规则 如果某个模块有自己的特殊放行需求,可以继承 `AuthorizeRequestsCustomizer`: ```java public class XxxAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { @Override public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { registry.requestMatchers("/xxx/**").permitAll(); } } ``` 适用场景: - WebSocket 握手地址放行 - 回调接口放行 - 某些特殊模块追加自己的 URL 规则 ### 7.4 记录操作日志 ```java @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 透传 - 更细的权限表达式 - 更完整的审计日志字段 - 更严格的跨租户访问控制