Files
youlai-boot/docs/security-refactor-plan.md
Ray.Hao 2441efa9ba refactor(security): Ports & Adapters 架构重构|解除反向依赖|抽离 Security Starter 准备
- 新增 2 个端口接口(UserAuthenticationPort / PermissionPort)+ 1 个适配器(PermissionAdapter)
- SecurityUserDetails 用 roles(Set<String>) 替代 authorities,消除 Jackson 序列化问题
- 删除 UserSession,Redis / JWT 直存 SecurityUserDetails
- SecurityUserDetailsService / PermissionService 注入 Port 替代 system Service
2026-07-05 16:53:46 +08:00

80 KiB
Raw Blame History

Security 框架重构与 Starter 抽离方案

状态:待实施 范围framework/security 模块 目标:消除模型冗余 → 端口/适配器解耦 → 抽离为独立 Spring Boot Starter 适用youlai-boot单模块和 youlai-boot-multi多模块 最后更新2026-07-04 合并自deep-research-report.mdPorts & Adapters 研究)+ 原 security-refactor-plan.md(模型消除教程) 实施策略:不保留历史登录态;重构发布后所有旧 Token 失效,用户重新登录


目录


、文档使用指南AI 协作必读)

本节专为 AI 协作设计。任何 AI 在修改 security 相关代码前,必须先阅读本节。

0.1 文档定位

本文档是 youlai-boot security 模块的唯一权威重构方案,合并了早期的架构研究(deep-research-report.md)和实施计划。所有 security 相关的架构变更、代码修改、命名约定均以本文档为准。

0.2 如何使用本文档

你的目标 阅读顺序
理解为什么要改 第一节 → 第二节
执行单模块重构 第三节 → 第四节 → 第八节P0→P1→P2→P3
执行多模块重构 第三节 → 第五节 → 第八节P0→P1→P2→P3
抽离为 Starter实操 先完成第四/五节 → 第六节 → 6.11(实操步骤)
发布到 Maven Central 6.12GPG + Sonatype + deploy
其他项目接入 Starter 6.13(引入依赖 + 实现适配器 + 验证)
修改 security 代码 第九节AI 协作指南)→ 第十节(审查清单)
新增端口/适配器 第二节 2.3 → 第九节 9.3

0.3 关键决策摘要(不可违背)

编号 决策 原因
D1 端口数量固定为 2 个UserAuthenticationPortPermissionPort RolePort 冗余——roles 已包含在 SecurityUser 中;在线用户不纳入本次重构
D2 UserAuthInfo 重命名为 SecurityUser 作为端口返回模型 SecurityUser 语义更中性,作为 Starter 公开 API 更规范;原 UserAuthInfo 偏向"认证信息"但实际承载用户安全数据;重命名而非新建,改动最小
D3 SecurityUserDetails.authorities 字段类型 Collection<SimpleGrantedAuthority>Set<String> roles 消除序列化问题,getAuthorities() 改为实时计算
D4 删除 UserSession,发布后旧 Token 全部失效 其 5 个字段 100% 是 SecurityUserDetails 的子集,中间层无意义;不保留历史登录态
D5 端口接口放在 framework.security.port 包,适配器放在 system.security.adapter 物理隔离端口定义与实现,符合六边形架构
D6 Starter 模块命名为 youlai-security-spring-boot-starter 遵循 Spring Boot 官方 starter 命名规范
D7 SocialPlatformEnum 下沉到 youlai-common 后再定义端口 端口和 Starter 不允许出现任何 com.youlai.boot.system.* 引用
D8 Starter 不内置业务 JSON 响应写出逻辑,不依赖 ResponseWriter 认证失败和鉴权失败走 Spring Security 标准异常与使用方 AuthenticationEntryPoint / AccessDeniedHandler

0.4 代码中的空占位文件说明

⚠️ 重要发现:当前 framework/security/service/ 下已存在 4 个空文件,表明端口模式已被原开发者规划但未实施

空文件 推测用途 重构后处理
UserAuthQueryService.java 用户认证查询接口占位 删除,由 port/UserAuthenticationPort.java 替代
RolePermissionService.java 角色权限查询接口占位 删除,由 port/PermissionPort.java 替代
WxMaUserAuthQueryService.java 微信小程序认证查询占位 删除,合并入 UserAuthenticationPort.getAuthInfoByOpenid()
WxMaBindInfo.java 微信绑定信息模型占位 删除,微信绑定流程留在使用方 Provider 内实现

一、问题诊断

1.1 模型冗余3 → 2

当前三个模型字段高度重叠(SecurityUserUserAuthInfo 重命名后的名称,见 D2

字段 SecurityUser
(原 UserAuthInfo
SecurityUserDetails UserSession
userId
username
deptId
dataScopes
roles
authorities
password
enabled
nickname
status

数据流SecurityUser → SecurityUserDetails → UserSession → SecurityUserDetails,中间经历两次无意义字段拷贝。UserSession 的 5 个字段 100% 是 SecurityUserDetails 的子集,存在的唯一原因是 SimpleGrantedAuthority 不便序列化。

1.2 框架 ↔ 业务循环依赖

framework.security ──→ systemSecurityUserDetailsService 直接注入 UserService、UserSocialService
framework.security ──→ systemPermissionService 直接注入 RoleMenuService
system ──→ framework.securitySecurityUtils、SecurityUser、SecurityUserDetails

SecurityUserDetailsServicePermissionService 中大量 import com.youlai.boot.system.*,违反了依赖方向。在多模块项目中,这导致 youlai-framework 无法独立于 youlai-system 发布。

1.3 序列化问题(已临时修复)

JacksonJsonRedisSerializer<Object> 反序列化时丢失类型信息,UserSession 被还原为 LinkedHashMap

当前修复方案:提取 JsonMapper 为共享 Bean使用 convertValue 显式转换。 已生效,但 UserSession 中间层本身是不必要的。

1.4 现状 vs 目标对比

维度 当前实现(耦合严重) 目标实现Ports & Adapters
依赖关系 framework/security 直接 import com.youlai.boot.system.* 安全模块仅引用自身端口接口,不持有 system 引用
用户认证 SecurityUserDetailsService 注入 UserService 查询 注入 UserAuthenticationPort,由 system 侧适配器实现
权限校验 PermissionService 注入 RoleMenuService 注入 PermissionPort,由 system 侧适配器实现
角色获取 SecurityUserDetails 构造时转换 authorities roles 字段直接存储 Set<String>getAuthorities() 实时计算
在线用户 无专门接口 不纳入本次 Security Starter 重构
序列化 UserSession 中间层 + convertValue 修补 直接存取 SecurityUserDetails(纯 JDK 类型)
可测试性 安全代码测试需引入 system 服务 针对端口接口 Mock 测试,解耦
模块化 无法独立发布 security 模块 端口定义可随 Starter 独立发布

二、架构原则与设计决策

2.1 Ports & Adapters 模式(精简)

六边形架构核心思想:应用核心业务通过抽象的**端口Port与外部通信,外部实现通过适配器Adapter**连入端口。

  • 端口Port:定义安全模块需要的功能接口,放在 framework.security.port 包。命名以 Port 后缀结尾。
  • 适配器Adapter:在 system 模块实现端口接口,内部调用具体 Service。标注 @Component
  • 解耦效果:安全模块编译期不持有任何 com.youlai.boot.system.* 引用。

2.2 端口定义决策

端口 方法 职责 是否核心
UserAuthenticationPort getAuthInfoByUsername(String)
getAuthInfoByMobile(String)
getAuthInfoByOpenid(SocialPlatformEnum, String)
用户认证信息查询
PermissionPort getRolePerms(Set<String> roleCodes) 角色权限集合查询

为什么不设 RolePort 角色编码roles已作为 Set<String> 包含在 SecurityUser 中,登录时一次性获取,无需单独端口查询。SecurityUtils.getRoles() 直接从 SecurityUserDetails.roles 字段取值即可。

为什么不做在线用户端口? 在线用户管理属于业务运营能力,不属于认证鉴权最小内核。本次重构不提供 OnlineUserPort,后续如需在线用户功能,由业务模块基于登录/登出事件自行实现,不进入 Security Starter。

2.3 命名规范

类型 命名规则 示例
端口接口 <领域意图>Port UserAuthenticationPortPermissionPort
适配器实现 <领域意图>Adapter UserAuthenticationAdapterPermissionAdapter
端口包路径 framework.security.port
适配器包路径 system.security.adapter(单模块)
youlai-system/.../security/adapter(多模块)
端口返回模型 SecurityUser(由 UserAuthInfo 重命名,纯 POJO无 system 依赖)

禁止:使用 Service/Query 后缀命名端口(如 UserAuthQueryService),这与业务 Service 混淆。当前代码中的空文件 UserAuthQueryService.javaRolePermissionService.java 就是错误命名的占位,应删除并用 Port 后缀替代。


三、方案设计

3.1 消除 UserSession

核心改动SecurityUserDetailsCollection<SimpleGrantedAuthority> authorities 替换为 Set<String> rolesgetAuthorities() 改为实时计算。

@Data
@NoArgsConstructor
public class SecurityUserDetails implements UserDetails {

    private Long userId;
    private String username;
    private String password;
    private Boolean enabled;
    private Long deptId;
    private List<RoleDataScope> dataScopes;
    private Set<String> roles;  // ← 替换 authorities

    public SecurityUserDetails(SecurityUser user) {
        this.userId = user.getUserId();
        this.username = user.getUsername();
        this.password = user.getPassword();
        this.enabled = ObjectUtil.equal(user.getStatus(), 1);
        this.deptId = user.getDeptId();
        this.dataScopes = user.getDataScopes();
        this.roles = user.getRoles();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (CollectionUtil.isEmpty(roles)) {
            return Collections.emptySet();
        }
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role))
            .collect(Collectors.toSet());
    }

    // 其他 UserDetails 方法不变...
}

效果:所有字段均为纯 JDK 类型,SecurityUserDetails 可直接序列化存储,UserSession 文件删除。

3.2 端口/适配器解耦

┌─────────────────────────────────┐     ┌──────────────────────────────────┐
│      framework/security         │     │           system                 │
│  ┌───────────────────────────┐  │     │  ┌────────────────────────────┐  │
│  │  port/                    │  │     │  │  security/adapter/         │  │
│  │  ├ UserAuthenticationPort │◄─┼─────┼──│  ├ UserAuthenticationAdapter│  │
│  │  └ PermissionPort         │◄─┼─────┼──│  └ PermissionAdapter        │  │
│  └───────────────────────────┘  │     │  └────────────────────────────┘  │
│  ┌───────────────────────────┐  │     │  ┌────────────────────────────┐  │
│  │  service/                 │  │     │  │  service/                  │  │
│  │  ├ SecurityUserDetailsService  │──┼─────┼──│  (注入 Port不注入 system│  │
│  │  └ PermissionService      │──┼─────┼──│                             │  │
│  └───────────────────────────┘  │     │  └────────────────────────────┘  │
└─────────────────────────────────┘     └──────────────────────────────────┘
         ↑ 编译期零 system 依赖                     ↑ 运行期注入 Port 实现

端口定义framework 层):

端口 方法 职责
UserAuthenticationPort getAuthInfoByUsername(String)
getAuthInfoByMobile(String)
getAuthInfoByOpenid(platform, openid)
用户认证信息查询
PermissionPort getRolePerms(Set<String> roleCodes) 角色权限集合查询

适配器实现system 层):

适配器 实现端口 委托目标
UserAuthenticationAdapter UserAuthenticationPort UserService + UserSocialService
PermissionAdapter PermissionPort RoleMenuService

3.3 SecurityUtils 简化

// 之前5 步链式getAuthorities → filter ROLE_ → strip
// 之后:直接取 roles 字段
public static Set<String> getRoles() {
    return getUser().map(SecurityUserDetails::getRoles).orElse(Set.of());
}

3.4 Token 管理器适配

  • RedisTokenManager:直接存储 SecurityUserDetailspassword 置 null删除 UserSession 中间层和 buildUserDetails 重建逻辑
  • JwtTokenManagerJWT claims 使用 roles 字段保存不带 ROLE_ 前缀的角色编码;parseToken 只解析 rolesgetAuthorities() 实时补 ROLE_ 前缀
  • 旧 Token 策略:不接受历史 JWT / Redis Token。发布时清理 Redis 中 auth:* 登录态 key并轮换 security.session.jwt.secret-key,所有用户重新登录

四、单模块重构youlai-boot

4.1 端口接口定义

先将 SocialPlatformEnum 下沉到 youlai-common,再新建包 framework.security.port

framework/security/
├── port/
│   ├── UserAuthenticationPort.java      # 新增
│   └── PermissionPort.java              # 新增
├── model/
│   ├── SecurityUserDetails.java              # 改造
│   ├── SecurityUser.java                # 重命名(原 UserAuthInfo
│   └── UserSession.java                 # 删除
├── service/
│   ├── SecurityUserDetailsService.java       # 改造(注入 Port
│   ├── PermissionService.java           # 改造(注入 Port
│   ├── UserAuthQueryService.java        # 删除(空占位)
│   ├── RolePermissionService.java       # 删除(空占位)
│   ├── WxMaUserAuthQueryService.java    # 删除(空占位)
│   └── WxMaBindInfo.java                # 删除(空占位,如在 model 包则一并清理)
└── ...
// framework/security/port/UserAuthenticationPort.java
package com.youlai.boot.framework.security.port;

import com.youlai.boot.framework.security.model.SecurityUser;
import com.youlai.boot.common.enums.SocialPlatformEnum;

/**
 * 用户认证信息查询端口。
 * <p>
 * 由 system 模块提供适配器实现framework 层通过此接口获取认证数据。
 * 框架层不直接依赖 system 模块的 Service。
 *
 * @see com.youlai.boot.system.security.adapter.UserAuthenticationAdapter
 */
public interface UserAuthenticationPort {

    /**
     * 根据用户名查询认证信息。
     *
     * @param username 用户名
     * @return 认证信息,不存在返回 null
     */
    SecurityUser getAuthInfoByUsername(String username);

    /**
     * 根据手机号查询认证信息。
     *
     * @param mobile 手机号
     * @return 认证信息,不存在返回 null
     */
    SecurityUser getAuthInfoByMobile(String mobile);

    /**
     * 根据第三方 openid 查询认证信息。
     *
     * @param platform 第三方平台
     * @param openid   openid
     * @return 认证信息,未绑定返回 null
     */
    SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid);
}
// framework/security/port/PermissionPort.java
package com.youlai.boot.framework.security.port;

import java.util.Set;

/**
 * 权限查询端口。
 * <p>
 * 由 system 模块提供适配器实现framework 层通过此接口获取角色权限集合。
 *
 * @see com.youlai.boot.system.security.adapter.PermissionAdapter
 */
public interface PermissionPort {

    /**
     * 根据角色编码集合查询权限标识集合。
     *
     * @param roleCodes 角色编码集合
     * @return 权限标识集合,如 "sys:user:create"
     */
    Set<String> getRolePerms(Set<String> roleCodes);
}

硬性要求UserAuthenticationPort 必须引用 com.youlai.boot.common.enums.SocialPlatformEnum。端口接口中禁止出现任何 com.youlai.boot.system.* 引用。

4.2 SecurityUserDetails 改造

// framework/security/model/SecurityUserDetails.java
// 改动点:
// 1. authorities 字段类型 Collection<SimpleGrantedAuthority> → Set<String> roles
// 2. 构造函数直接赋值 roles
// 3. getAuthorities() 改为实时计算

@Data
@NoArgsConstructor
public class SecurityUserDetails implements UserDetails {

    private Long userId;
    private String username;
    private String password;
    private Boolean enabled;
    private Long deptId;
    private List<RoleDataScope> dataScopes;
    private Set<String> roles;  // ← 替换 authorities

    public SecurityUserDetails(SecurityUser user) {
        this.userId = user.getUserId();
        this.username = user.getUsername();
        this.password = user.getPassword();
        this.enabled = ObjectUtil.equal(user.getStatus(), 1);
        this.deptId = user.getDeptId();
        this.dataScopes = user.getDataScopes();
        this.roles = user.getRoles();  // ← 直接赋值,无需转换
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (CollectionUtil.isEmpty(roles)) {
            return Collections.emptySet();
        }
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role))
            .collect(Collectors.toSet());
    }
}

4.3 适配器实现

在 system 模块新建 system.security.adapter 包:

// system/security/adapter/UserAuthenticationAdapter.java
package com.youlai.boot.system.security.adapter;

import com.youlai.boot.framework.security.model.SecurityUser;
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
import com.youlai.boot.common.enums.SocialPlatformEnum;
import com.youlai.boot.system.service.UserService;
import com.youlai.boot.system.service.UserSocialService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
 * 用户认证信息查询适配器。
 * <p>
 * 实现 framework 层的 UserAuthenticationPort委托 system 层服务完成查询。
 * 此类是端口与系统服务的唯一耦合点。
 */
@Component
@RequiredArgsConstructor
public class UserAuthenticationAdapter implements UserAuthenticationPort {

    private final UserService userService;
    private final UserSocialService userSocialService;

    @Override
    public SecurityUser getAuthInfoByUsername(String username) {
        return userService.getAuthInfoByUsername(username);
    }

    @Override
    public SecurityUser getAuthInfoByMobile(String mobile) {
        return userService.getAuthInfoByMobile(mobile);
    }

    @Override
    public SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid) {
        return userSocialService.getAuthInfoByOpenid(platform, openid);
    }
}
// system/security/adapter/PermissionAdapter.java
package com.youlai.boot.system.security.adapter;

import com.youlai.boot.framework.security.port.PermissionPort;
import com.youlai.boot.system.service.RoleMenuService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Set;

/**
 * 权限查询适配器。
 * <p>
 * 实现 framework 层的 PermissionPort委托 RoleMenuService 查询权限。
 */
@Component
@RequiredArgsConstructor
public class PermissionAdapter implements PermissionPort {

    private final RoleMenuService roleMenuService;

    @Override
    public Set<String> getRolePerms(Set<String> roleCodes) {
        return roleMenuService.getRolePermsByRoleCodes(roleCodes);
    }
}

4.4 服务层改造

// framework/security/service/SecurityUserDetailsService.java改造后
package com.youlai.boot.framework.security.service;

import com.youlai.boot.framework.security.model.SecurityUserDetails;
import com.youlai.boot.framework.security.model.SecurityUser;
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;

/**
 * 系统用户认证 DetailsService。
 * <p>
 * 通过 UserAuthenticationPort 获取认证信息,不直接依赖 system 模块。
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class SecurityUserDetailsService implements UserDetailsService {

    private final UserAuthenticationPort userAuthPort;  // ← 替换 UserService

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            SecurityUser SecurityUser = userAuthPort.getAuthInfoByUsername(username);
            if (SecurityUser == null) {
                throw new UsernameNotFoundException(username);
            }
            return new SecurityUserDetails(SecurityUser);
        } catch (Exception e) {
            log.error("认证异常:{}", e.getMessage());
            throw e;
        }
    }
}
// framework/security/service/PermissionService.java改造后
package com.youlai.boot.framework.security.service;

import com.youlai.boot.framework.security.port.PermissionPort;
import com.youlai.boot.framework.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.PatternMatchUtils;
import java.util.Set;

@Component("ss")
@RequiredArgsConstructor
@Slf4j
public class PermissionService {

    private final PermissionPort permissionPort;  // ← 替换 RoleMenuService

    public boolean hasPerm(String requiredPerm) {
        // ... 前置校验不变(超管放行等)...
        Set<String> roleCodes = SecurityUtils.getRoles();
        Set<String> rolePerms = permissionPort.getRolePerms(roleCodes);  // ← 通过 Port 调用
        // ... 后续逻辑不变(通配符匹配等)...
    }
}

4.5 Token 管理器适配

RedisTokenManager 改动:

// 之前UserSession session = new UserSession(user);
//       redisTemplate.opsForValue().set(key, session);
// 之后:直接存 SecurityUserDetailspassword 置 null
SecurityUserDetails sessionUser = new SecurityUserDetails();
sessionUser.setUserId(user.getUserId());
sessionUser.setUsername(user.getUsername());
sessionUser.setDeptId(user.getDeptId());
sessionUser.setDataScopes(user.getDataScopes());
sessionUser.setRoles(user.getRoles());
sessionUser.setEnabled(user.isEnabled());
sessionUser.setPassword(null);  // 不存密码
redisTemplate.opsForValue().set(key, sessionUser);
// 之前UserSession raw = jsonMapper.convertValue(obj, UserSession.class);
//       return buildUserDetails(raw);
// 之后:直接还原
SecurityUserDetails raw = jsonMapper.convertValue(obj, SecurityUserDetails.class);
return raw;

JwtTokenManager 改动:

// parseToken 中:
// 之前:从 claims 取 authorities构造 Set<SimpleGrantedAuthority>
// 之后:从 claims 取 rolesSet<String>),直接 setRoles()
Set<String> roles = claims.get(JwtClaimConstants.ROLES);
securityUserDetails.setRoles(roles);
// getAuthorities() 会在运行时自动 add ROLE_ 前缀

同步修改 JwtClaimConstants

// 删除 AUTHORITIES新增 ROLES
String ROLES = "roles";

上线发布时执行登录态失效:

1. 清理 Redis 中 auth:* 登录态 key
2. 轮换 security.session.jwt.secret-key
3. 通知前端收到 401 后跳转登录页

4.6 单模块改动清单

文件 操作 改动
framework/security/port/UserAuthenticationPort.java 新增 端口接口
framework/security/port/PermissionPort.java 新增 端口接口
framework/security/model/SecurityUserDetails.java 改造 authoritiesrolesgetAuthorities() 改为计算
framework/security/model/UserSession.java 删除 不再需要
framework/security/service/SecurityUserDetailsService.java 改造 注入 UserAuthenticationPort 替代 UserService
framework/security/service/PermissionService.java 改造 注入 PermissionPort 替代 RoleMenuService
framework/security/config/SecurityConfig.java 改造 移除 UserService 直接注入Provider 改走端口或留在使用方
framework/security/service/UserAuthQueryService.java 删除 空占位文件
framework/security/service/RolePermissionService.java 删除 空占位文件
framework/security/service/WxMaUserAuthQueryService.java 删除 空占位文件
framework/security/model/WxMaBindInfo.java 删除 空占位文件
framework/security/util/SecurityUtils.java 改造 getRoles() 简化为直接取 roles 字段
framework/security/token/RedisTokenManager.java 改造 SecurityUserDetails,删除 UserSession 引用和 buildUserDetails
framework/security/token/JwtTokenManager.java 改造 parseTokenroles 替代 authorities
common/constant/JwtClaimConstants.java 改造 删除 AUTHORITIES,新增 ROLES = "roles"
common/enums/SocialPlatformEnum.java 新增/迁移 system.enums 下沉到 common
system/security/adapter/UserAuthenticationAdapter.java 新增 适配器实现
system/security/adapter/PermissionAdapter.java 新增 适配器实现

五、多模块重构youlai-boot-multi

多模块项目中,youlai-framework 是独立 Maven 模块,youlai-system 是另一个模块。核心区别:端口定义在 youlai-framework,适配器实现在 youlai-system

5.1 模块依赖关系

youlai-framework端口定义 + 基础设施)  ←──依赖── youlai-system适配器实现 + 业务)
       ↑                                              ↑
       └──依赖── youlai-auth、youlai-application 等

关键youlai-frameworkpom.xml 不依赖 youlai-systemyoulai-system 依赖 youlai-framework,实现端口接口。

5.2 当前多模块状态

多模块项目已将 SecurityUserDetailsServicePermissionService 移至 youlai-system/security/service/,但代码仍直接 import system 服务,循环依赖未解决。youlai-framework/security/service/ 目录为空。

⚠️ 版本不一致提醒:根 POM 声明 4.3.1,而 youlai-frameworkyoulai-system 的 parent 版本为 4.3.0。重构时应一并修正。

5.3 端口接口定义youlai-framework

youlai-framework 模块新建 port 包:

youlai-framework/src/main/java/com/youlai/boot/framework/security/
├── port/
│   ├── UserAuthenticationPort.java      # 新增
│   └── PermissionPort.java              # 新增
├── model/
│   ├── SecurityUserDetails.java              # 改造(同单模块 4.2
│   ├── SecurityUser.java                # 重命名(原 UserAuthInfo
│   └── UserSession.java                 # 删除
├── token/
│   ├── TokenManager.java                # 不变
│   ├── JwtTokenManager.java             # 改造(同单模块 4.5
│   └── RedisTokenManager.java           # 改造(同单模块 4.5
├── util/
│   └── SecurityUtils.java               # 改造(同单模块 3.3
└── service/                             # 保持为空(服务在 system 模块)

端口接口代码与单模块完全相同(见 4.1),仅包名一致。

5.4 适配器实现youlai-system

youlai-system 模块新建 adapter 包(当前不存在 adapter 目录,需创建):

youlai-system/src/main/java/com/youlai/boot/system/security/
├── adapter/                               # ← 新建目录
│   ├── UserAuthenticationAdapter.java     # 新增
│   └── PermissionAdapter.java             # 新增
├── service/
│   ├── SecurityUserDetailsService.java         # 改造(注入 Port
│   └── PermissionService.java             # 改造(注入 Port
├── provider/
│   ├── SmsAuthenticationProvider.java     # 使用方业务 Provider留在 system
│   └── WxMaAuthenticationProvider.java    # 使用方业务 Provider留在 system
└── config/
    └── SecurityConfig.java                # 检查注入对象

适配器代码与单模块完全相同(见 4.3),仅位于 youlai-system 模块。

5.5 服务层改造youlai-system

SecurityUserDetailsServicePermissionService 已在 youlai-system 模块中,改造方式与单模块相同(见 4.4),注入端口接口替代直接服务引用。

SmsAuthenticationProviderWxMaAuthenticationProvider 属于使用方业务认证流程,留在 youlai-system。它们可以直接调用 UserService / UserSocialService 完成验证码、微信绑定、session_key 更新等业务动作,但不得迁入 Starter。

5.6 POM 依赖检查

<!-- youlai-framework/pom.xml不依赖 youlai-system -->
<dependencies>
    <dependency>
        <groupId>com.youlai</groupId>
        <artifactId>youlai-common</artifactId>
    </dependency>
    <!-- 不包含 youlai-system -->
</dependencies>

<!-- youlai-system/pom.xml依赖 youlai-framework -->
<dependencies>
    <dependency>
        <groupId>com.youlai</groupId>
        <artifactId>youlai-framework</artifactId>
    </dependency>
</dependencies>

5.7 多模块改动清单

模块 文件 操作
youlai-framework security/port/UserAuthenticationPort.java 新增
youlai-framework security/port/PermissionPort.java 新增
youlai-framework security/model/SecurityUserDetails.java 改造
youlai-framework security/model/UserSession.java 删除
youlai-framework security/util/SecurityUtils.java 改造
youlai-framework security/token/JwtTokenManager.java 改造
youlai-framework security/token/RedisTokenManager.java 改造
youlai-framework pom.xml 确认不含 youlai-system 依赖
youlai-system security/adapter/UserAuthenticationAdapter.java 新增
youlai-system security/adapter/PermissionAdapter.java 新增
youlai-system security/service/SecurityUserDetailsService.java 改造
youlai-system security/service/PermissionService.java 改造
youlai-system security/provider/SmsAuthenticationProvider.java 保留在使用方,不迁入 Starter
youlai-system security/provider/WxMaAuthenticationProvider.java 保留在使用方,不迁入 Starter
youlai-system pom.xml parent 版本修正为 4.3.1

六、Security Starter 抽离与发布方案

前提:完成第四节(单模块)或第五节(多模块)的端口/适配器解耦后,方可执行本节。

6.1 可行性分析

维度 当前状态 Starter 要求 差距
端口定义 散落在 framework.security 需独立模块 需新建 Maven 模块
自动装配 spring.factories / AutoConfiguration.imports 需 SPI 注册 需新建自动配置类
配置属性 SecurityProperties 已有 @ConfigurationProperties @EnableConfigurationProperties 改造
Token 实现 固定保留 JwtTokenManagerRedisTokenManager 两个实现 security.session.type 条件装配 改造
业务耦合 SocialPlatformEnum 引用 system 包 需下沉到 common 改造
依赖管理 security 与 cache/mybatis/captcha 混在 framework 需独立依赖树 新建 pom.xml

结论:可行。端口/适配器解耦是前置条件,解耦后 security 模块编译期零 system 依赖,符合 Starter 抽离要求。

6.2 Starter 模块设计

youlai-boot-multi/(多模块项目根)
├── youlai-common/                    # 常量、枚举SocialPlatformEnum 下沉至此)
├── youlai-security-spring-boot-starter/   # ← 新增Security Starter
│   ├── src/main/java/com/youlai/boot/framework/security/
│   │   ├── autoconfigure/            # 自动配置类
│   │   │   ├── SecurityAutoConfiguration.java
│   │   │   ├── JwtTokenAutoConfiguration.java
│   │   │   └── RedisTokenAutoConfiguration.java
│   │   ├── port/                     # 端口接口(对外契约)
│   │   │   ├── UserAuthenticationPort.java
│   │   │   └── PermissionPort.java
│   │   ├── model/                    # 安全模型
│   │   │   ├── SecurityUserDetails.java
│   │   │   ├── SecurityUser.java
│   │   │   ├── RoleDataScope.java
│   │   │   └── ...Token 模型等)
│   │   ├── service/                  # 框架层服务
│   │   │   ├── SecurityUserDetailsService.java
│   │   │   └── PermissionService.java
│   │   ├── token/                    # Token 管理器
│   │   │   ├── TokenManager.java
│   │   │   ├── JwtTokenManager.java
│   │   │   └── RedisTokenManager.java
│   │   ├── filter/                   # Token 认证过滤器
│   │   │   └── TokenAuthenticationFilter.java
│   │   ├── exception/                # 异常
│   │   ├── util/                     # SecurityUtils
│   │   └── config/                   # SecurityProperties
│   ├── src/main/resources/
│   │   └── META-INF/
│   │       └── spring/
│   │           └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│   └── pom.xml
├── youlai-system/                    # 提供 Adapter 实现
├── youlai-framework/                 # 剥离 security 后,保留 cache/mybatis 等
└── youlai-application/               # 引用 starter

6.3 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.youlai</groupId>
        <artifactId>youlai-boot</artifactId>
        <version>4.3.1</version>
    </parent>

    <artifactId>youlai-security-spring-boot-starter</artifactId>
    <description>youlai Security Spring Boot Starter - 认证鉴权自动装配</description>

    <dependencies>
        <!-- 内部依赖 -->
        <dependency>
            <groupId>com.youlai</groupId>
            <artifactId>youlai-common</artifactId>
        </dependency>

        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- RedisJWT tokenVersion/黑名单与 Redis-Token 均依赖 RedisTemplateStarter 固定携带 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- JWT沿用项目当前 Hutool JWT 实现,不引入 jjwt 双体系 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>

        <!-- 自动配置元数据IDE 提示用) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
    </dependencies>
</project>

关键设计Starter 固定携带 Security、Web、Redis、Hutool JWT 所需依赖。使用方不再拼装 JWT/Redis 依赖,降低接入复杂度。

6.4 自动配置类

// autoconfigure/SecurityAutoConfiguration.java
package com.youlai.boot.framework.security.autoconfigure;

import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
import com.youlai.boot.framework.security.port.PermissionPort;
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
import com.youlai.boot.framework.security.service.PermissionService;
import com.youlai.boot.framework.security.service.SecurityUserDetailsService;
import com.youlai.boot.framework.security.token.TokenManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.core.userdetails.UserDetailsService;
import tools.jackson.databind.json.JsonMapper;

/**
 * Security 自动配置入口。
 * <p>
 * 装配条件classpath 存在 SecurityProperties 类。
 * 使用方可通过 spring.autoconfigure.exclude 排除整体自动配置。
 */
@AutoConfiguration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityAutoConfiguration {

    /**
     * 密码编码器,默认 BCrypt。
     */
    @Bean
    @ConditionalOnMissingBean(PasswordEncoder.class)
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * JSON 映射器,供 RedisTokenManager 反序列化使用。
     */
    @Bean
    @ConditionalOnMissingBean(JsonMapper.class)
    public JsonMapper jsonMapper() {
        return JsonMapper.builder().findAndAddModules().build();
    }

    /**
     * 用户认证服务。
     * <p>
     * 需要使用方提供 UserAuthenticationPort 实现。
     */
    @Bean
    @ConditionalOnMissingBean(UserDetailsService.class)
    public UserDetailsService userDetailsService(UserAuthenticationPort userAuthenticationPort) {
        return new SecurityUserDetailsService(userAuthenticationPort);
    }

    /**
     * 权限服务 BeanSpEL @ss.hasPerm(...) 使用)。
     * <p>
     * 需要 PermissionPort 实现,否则启动失败——
     * 使用方必须提供 PermissionAdapter。
     */
    @Bean("ss")
    @ConditionalOnMissingBean(name = "ss")
    @ConditionalOnProperty(prefix = "security", name = "enabled", havingValue = "true", matchIfMissing = true)
    public PermissionService permissionService(PermissionPort permissionPort) {
        return new PermissionService(permissionPort);
    }

    /**
     * Token 认证过滤器。
     * <p>
     * 过滤器不写业务 JSON 响应;无效 Token 抛出 Spring Security AuthenticationException
     * 由使用方 SecurityConfig 中的 AuthenticationEntryPoint 统一处理。
     */
    @Bean
    @ConditionalOnMissingBean(TokenAuthenticationFilter.class)
    public TokenAuthenticationFilter tokenAuthenticationFilter(TokenManager tokenManager) {
        return new TokenAuthenticationFilter(tokenManager);
    }
}
// autoconfigure/JwtTokenAutoConfiguration.java
package com.youlai.boot.framework.security.autoconfigure;

import com.youlai.boot.framework.security.token.JwtTokenManager;
import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.framework.security.config.SecurityProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * JWT Token 管理器自动配置。
 * <p>
 * 装配条件:
 * 1. classpath 存在 cn.hutool.jwt.JWTUtil
 * 2. security.session.type = jwt
 */
@AutoConfiguration
@ConditionalOnClass(name = "cn.hutool.jwt.JWTUtil")
@ConditionalOnProperty(prefix = "security.session", name = "type", havingValue = "jwt", matchIfMissing = true)
public class JwtTokenAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(TokenManager.class)
    public TokenManager jwtTokenManager(SecurityProperties properties,
                                        RedisTemplate<String, Object> redisTemplate) {
        return new JwtTokenManager(properties, redisTemplate);
    }
}
// autoconfigure/RedisTokenAutoConfiguration.java
package com.youlai.boot.framework.security.autoconfigure;

import com.youlai.boot.framework.security.token.RedisTokenManager;
import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.framework.security.config.SecurityProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.context.annotation.Bean;
import tools.jackson.databind.json.JsonMapper;

/**
 * Redis Token 管理器自动配置。
 * <p>
 * 装配条件:
 * 1. classpath 存在 RedisTemplate
 * 2. security.session.type = redis-token
 */
@AutoConfiguration
@ConditionalOnClass(RedisTemplate.class)
@ConditionalOnProperty(prefix = "security.session", name = "type", havingValue = "redis-token")
public class RedisTokenAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(TokenManager.class)
    public TokenManager redisTokenManager(SecurityProperties properties,
                                          RedisTemplate<String, Object> redisTemplate,
                                          JsonMapper jsonMapper) {
        return new RedisTokenManager(properties, redisTemplate, jsonMapper);
    }
}

6.5 SPI 注册文件

src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.youlai.boot.framework.security.autoconfigure.SecurityAutoConfiguration
com.youlai.boot.framework.security.autoconfigure.JwtTokenAutoConfiguration
com.youlai.boot.framework.security.autoconfigure.RedisTokenAutoConfiguration

Spring Boot 3.x+ 使用 AutoConfiguration.imports(非 spring.factories)。本项目 Spring Boot 4.0,必须使用此方式。

6.6 配置属性

# application.yml使用方配置示例
security:
  enabled: true
  session:
    type: jwt                      # jwt | redis-token
    access-token-time-to-live: 7200
    refresh-token-time-to-live: 604800
    jwt:
      secret-key: ${JWT_SECRET:replace-with-strong-secret}
    redis-token:
      allow-multi-login: true
  ignore-urls:
    - /api/v1/auth/login/**
    - /api/v1/auth/refresh-token
  unsecured-urls:
    - /doc.html
    - /swagger-ui/**
    - /v3/api-docs/**
// config/SecurityProperties.javaStarter 内)
@Data
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
    private boolean enabled = true;
    private SessionConfig session = new SessionConfig();
    private String[] ignoreUrls = new String[0];
    private String[] unsecuredUrls = new String[0];

    @Data
    public static class SessionConfig {
        private String type = "jwt";
        private Integer accessTokenTimeToLive = 7200;
        private Integer refreshTokenTimeToLive = 604800;
        private JwtConfig jwt = new JwtConfig();
        private RedisTokenConfig redisToken = new RedisTokenConfig();
    }

    @Data
    public static class JwtConfig {
        private String secretKey;
    }

    @Data
    public static class RedisTokenConfig {
        private Boolean allowMultiLogin = true;
    }
}

6.7 枚举下沉处理

UserAuthenticationPort.getAuthInfoByOpenid(SocialPlatformEnum, String) 必须引用公共枚举。定义端口前先处理:

方案:将 SocialPlatformEnumyoulai-system 下沉到 youlai-common

# 之前
youlai-system/src/main/java/com/youlai/boot/system/enums/SocialPlatformEnum.java

# 之后
youlai-common/src/main/java/com/youlai/boot/common/enums/SocialPlatformEnum.java

同步修改所有引用方的 import 路径。Starter 的 UserAuthenticationPort 只引用 com.youlai.boot.common.enums.SocialPlatformEnum

6.8 接入契约摘要

使用方 pom.xml

<dependency>
    <groupId>com.youlai</groupId>
    <artifactId>youlai-security-spring-boot-starter</artifactId>
    <version>4.3.1</version>
</dependency>

使用方实现 Adapter(必须):

// 使用方的 system 模块必须提供两个适配器
@Component
public class MyUserAuthenticationAdapter implements UserAuthenticationPort { ... }

@Component
public class MyPermissionAdapter implements PermissionPort { ... }

如果使用方未提供 UserAuthenticationPortPermissionPort 的 BeanStarter 启动时会因构造器注入失败而报错——这是预期行为,强制使用方实现契约。

6.9 迁移路径

阶段 1端口/适配器解耦(第四节/第五节)
    ↓
阶段 2枚举下沉SocialPlatformEnum → youlai-common
    ↓
阶段 3创建 youlai-security-spring-boot-starter 模块
    ↓
阶段 4将 framework/security 代码迁移至 Starter 模块
    ↓
阶段 5编写自动配置类 + SPI 文件
    ↓
阶段 6youlai-framework 移除 security 子包,依赖改为引用 Starter
    ↓
阶段 7youlai-system 的 Adapter 确认无变化(实现同一端口接口)
    ↓
阶段 8集成测试 → 发布

6.10 Starter 边界界定

归属 Starter 归属使用方system 模块) 归属 youlai-common
port/ 端口接口 adapter/ 适配器实现 SecurityConstants 常量
model/ 安全模型 SecurityConfig Web 安全配置 SocialPlatformEnum 枚举
service/ SecurityUserDetailsService、PermissionService provider/ 认证 Provider Result 统一响应
token/ Token 管理器 enums/ 公共枚举
filter/TokenAuthenticationFilter filter/CaptchaValidationFilter
exception/ 安全异常 AuthenticationEntryPointAccessDeniedHandler
util/ SecurityUtils
autoconfigure/ 自动配置

注意SecurityConfig@EnableWebSecurity 配置类归使用方因为不同项目的安全规则放行路径、CORS 等)不同,不应由 Starter 强制装配。Starter 仅提供 SecurityProperties 供使用方读取配置。

异常响应边界Starter 不提供 MyAuthenticationEntryPointMyAccessDeniedHandlerResponseWriterTokenAuthenticationFilter 只负责解析 Token 和填充 SecurityContext;无效 Token 抛出 Spring Security AuthenticationException。最终 HTTP 状态码、JSON 结构、错误码由使用方的 AuthenticationEntryPointAccessDeniedHandler 决定。

6.11 实操步骤(命令行操作)

以下以多模块项目 youlai-boot-multi 为例,演示从零创建 Starter 模块到代码迁移的完整命令行操作。单模块项目参考第四节先完成端口解耦,再按 6.11.3 起的步骤操作。

6.11.1 重命名 UserAuthInfo → SecurityUser

# Windows PowerShell
cd d:\Project\youlai-admin\youlai-boot-multi\youlai-framework

# 重命名文件
Rename-Item -Path "src\main\java\com\youlai\boot\framework\security\model\UserAuthInfo.java" `
            -NewName "SecurityUser.java"
# Linux/macOS
cd d:/Project/youlai-admin/youlai-boot-multi/youlai-framework
mv src/main/java/com/youlai/boot/framework/security/model/UserAuthInfo.java \
   src/main/java/com/youlai/boot/framework/security/model/SecurityUser.java

然后用 IDE 全局替换Ctrl+Shift+R

  • 类名 UserAuthInfoSecurityUser
  • 检查所有 import ...UserAuthInfoimport ...SecurityUser
  • 更新 Javadoc 中的 @author 等引用

6.11.2 创建 Starter Maven 模块

# 在多模块项目根目录下创建新模块目录
cd d:\Project\youlai-admin\youlai-boot-multi

mkdir youlai-security-spring-boot-starter
mkdir youlai-security-spring-boot-starter\src\main\java\com\youlai\boot\framework\security
mkdir youlai-security-spring-boot-starter\src\main\resources\META-INF\spring

创建 youlai-security-spring-boot-starter/pom.xml(内容见 6.3 节)。

6.11.3 在根 POM 注册新模块

<!-- youlai-boot-multi/pom.xml 的 <modules> 中添加 -->
<module>youlai-security-spring-boot-starter</module>

6.11.4 迁移代码到 Starter 模块

# 将 framework/security 下的代码移动到 Starter 模块
$src = "youlai-framework\src\main\java\com\youlai\boot\framework\security"
$dst = "youlai-security-spring-boot-starter\src\main\java\com\youlai\boot\framework\security"

# 移动 Starter 内核子包
Move-Item -Path "$src\port" -Destination "$dst\port" -Force
Move-Item -Path "$src\model" -Destination "$dst\model" -Force
Move-Item -Path "$src\service" -Destination "$dst\service" -Force
Move-Item -Path "$src\token" -Destination "$dst\token" -Force
Move-Item -Path "$src\exception" -Destination "$dst\exception" -Force
Move-Item -Path "$src\util" -Destination "$dst\util" -Force

# 只迁移 TokenAuthenticationFilterCaptchaValidationFilter 留在使用方
mkdir "$dst\filter"
Move-Item -Path "$src\filter\TokenAuthenticationFilter.java" -Destination "$dst\filter\TokenAuthenticationFilter.java" -Force

# 只迁移 SecurityPropertiesPasswordEncoder 由 SecurityAutoConfiguration 提供
mkdir "$dst\config"
Move-Item -Path "$src\config\SecurityProperties.java" -Destination "$dst\config\SecurityProperties.java" -Force

# 新建 autoconfigure 包
mkdir "$dst\autoconfigure"

# 移动 resources
Move-Item -Path "youlai-framework\src\main\resources\..." `
          -Destination "youlai-security-spring-boot-starter\src\main\resources\..." -Force

关键provider/handler/CaptchaValidationFilterPasswordEncoderConfig 不迁移。Provider 和验证码属于使用方业务流程handler 和 ResponseWriter 属于使用方响应格式PasswordEncoder Bean 由 SecurityAutoConfiguration 统一提供。

6.11.5 创建自动配置类和 SPI 文件

# 创建 3 个自动配置类(内容见 6.4 节)
# SecurityAutoConfiguration.java
# JwtTokenAutoConfiguration.java
# RedisTokenAutoConfiguration.java

# 创建 SPI 注册文件
$spiPath = "youlai-security-spring-boot-starter\src\main\resources\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports"
Set-Content -Path $spiPath -Encoding UTF8 -Value @"
com.youlai.boot.framework.security.autoconfigure.SecurityAutoConfiguration
com.youlai.boot.framework.security.autoconfigure.JwtTokenAutoConfiguration
com.youlai.boot.framework.security.autoconfigure.RedisTokenAutoConfiguration
"@

6.11.6 修改 youlai-framework 的 pom.xml

<!-- youlai-framework/pom.xml添加 Starter 依赖,移除已迁移的 security 相关依赖 -->
<dependencies>
    <dependency>
        <groupId>com.youlai</groupId>
        <artifactId>youlai-common</artifactId>
    </dependency>

    <!-- 引入 Security Starter替代原来散落的 security 代码) -->
    <dependency>
        <groupId>com.youlai</groupId>
        <artifactId>youlai-security-spring-boot-starter</artifactId>
    </dependency>

    <!-- 移除 spring-boot-starter-security已由 Starter 传递) -->
    <!-- 移除 spring-boot-starter-data-redis已由 Starter 传递) -->
</dependencies>

6.11.7 枚举下沉

# 将 SocialPlatformEnum 从 youlai-system 移到 youlai-common
$src = "youlai-system\src\main\java\com\youlai\boot\system\enums\SocialPlatformEnum.java"
$dst = "youlai-common\src\main\java\com\youlai\boot\common\enums\SocialPlatformEnum.java"

# 确保目标目录存在
mkdir "youlai-common\src\main\java\com\youlai\boot\common\enums" -Force
Move-Item -Path $src -Destination $dst -Force

然后用 IDE 全局替换包名:

  • com.youlai.boot.system.enums.SocialPlatformEnumcom.youlai.boot.common.enums.SocialPlatformEnum

6.11.8 验证编译

cd d:\Project\youlai-admin\youlai-boot-multi

# 编译全部模块
mvn clean compile -pl youlai-security-spring-boot-starter,youlai-common,youlai-system,youlai-framework -am

# 确认 Starter 模块零 system 依赖(搜索结果应为空)
Select-String -Path "youlai-security-spring-boot-starter\src\**\*.java" `
              -Pattern "import com\.youlai\.boot\.system\." -SimpleMatch

如果最后一条命令有输出,说明 Starter 仍存在 system 依赖,需排查并消除。

6.11.9 验证自动装配

# 启动应用,观察日志中是否出现自动配置加载信息
mvn spring-boot:run -pl youlai-application

# 或在测试中验证
mvn test -pl youlai-application -Dtest=SecurityAutoConfigurationTest

6.12 发布到 Maven Central

以下流程基于 Sonatype Central Publisher Portal。发布凭据使用 Portal User Token不使用旧发布流程。

6.12.1 前置准备

准备项 说明 获取方式
Sonatype 账号 发布构件的账号 注册 https://central.sonatype.com
GPG 密钥 签名构件 gpg --gen-key 生成
Namespace 所有权验证 证明你拥有该 groupId 在 Central Portal 添加 namespace 并按提示完成 DNS 或 GitHub 验证
Maven settings.xml 配置 Central Portal User Token ~/.m2/settings.xml

6.12.2 生成并上传 GPG 密钥

# 生成 GPG 密钥
gpg --gen-key
# 按提示输入姓名、邮箱、密码

# 查看密钥 ID
gpg --list-keys
# 输出示例rsa3072/ABCDEF1234567890

# 上传公钥到密钥服务器Maven Central 要求)
gpg --keyserver keyserver.ubuntu.com --send-keys ABCDEF1234567890
# 备用服务器
gpg --keyserver keys.openpgp.org --send-keys ABCDEF1234567890
gpg --keyserver pgp.mit.edu --send-keys ABCDEF1234567890

6.12.3 配置 settings.xml

<!-- ~/.m2/settings.xml -->
<settings>
  <servers>
    <server>
      <id>central</id>
      <username>你的 Central Portal token username</username>
      <password>你的 Central Portal token password</password>
    </server>
  </servers>

  <profiles>
    <profile>
      <id>central</id>
      <properties>
        <gpg.executable>gpg</gpg.executable>
        <gpg.passphrase>你的GPG密码</gpg.passphrase>
      </properties>
    </profile>
  </profiles>

  <activeProfiles>
    <activeProfile>central</activeProfile>
  </activeProfiles>
</settings>

6.12.4 配置 Starter 的 pom.xml发布相关

youlai-security-spring-boot-starter/pom.xml<project> 下追加:

<!-- 发布信息 -->
<name>youlai Security Spring Boot Starter</name>
<description>认证鉴权 Spring Boot Starter支持 JWT / Redis Token 双模式</description>
<url>https://github.com/youlaitech/youlai-starter</url>

<licenses>
  <license>
    <name>The Apache Software License, Version 2.0</name>
    <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
  </license>
</licenses>

<developers>
  <developer>
    <id>youlai</id>
    <name>youlai</name>
    <email>youlai@example.com</email>
    <url>https://github.com/youlaitech</url>
  </developer>
</developers>

<scm>
  <url>https://github.com/youlaitech/youlai-starter</url>
  <connection>scm:git:git://github.com/youlaitech/youlai-starter.git</connection>
  <developerConnection>scm:git:ssh://github.com/youlaitech/youlai-starter.git</developerConnection>
</scm>

<!-- 发布插件 -->
<build>
  <plugins>
    <!-- 源码包 -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-source-plugin</artifactId>
      <version>3.3.1</version>
      <executions>
        <execution>
          <id>attach-sources</id>
          <goals><goal>jar-no-fork</goal></goals>
        </execution>
      </executions>
    </plugin>

    <!-- Javadoc 包 -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-javadoc-plugin</artifactId>
      <version>3.6.3</version>
      <executions>
        <execution>
          <id>attach-javadocs</id>
          <goals><goal>jar</goal></goals>
        </execution>
      </executions>
    </plugin>

    <!-- GPG 签名 -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-gpg-plugin</artifactId>
      <version>3.1.0</version>
      <executions>
        <execution>
          <id>sign-artifacts</id>
          <phase>verify</phase>
          <goals><goal>sign</goal></goals>
        </execution>
      </executions>
    </plugin>

    <!-- 发布到 Sonatype Central Portal -->
    <plugin>
      <groupId>org.sonatype.central</groupId>
      <artifactId>central-publishing-maven-plugin</artifactId>
      <version>0.11.0</version>
      <extensions>true</extensions>
      <configuration>
        <publishingServerId>central</publishingServerId>
        <autoPublish>true</autoPublish>
      </configuration>
    </plugin>
  </plugins>
</build>

注意Central Portal 要求发布包包含 sources、javadocs、GPG 签名和完整 POM 元信息。central-publishing-maven-plugin 负责上传与发布,不负责自动补齐这些元信息。

6.12.5 执行发布

cd d:/Project/youlai-admin/youlai-boot-multi/youlai-security-spring-boot-starter

# 1. 编译 + 测试 + 签名 + 发布(一条命令)
mvn clean deploy -P central

# 2. 发布后登录 https://central.sonatype.com 检查状态
#    状态变为 "Published" 后,约 15-30 分钟同步到 Maven Central
# 验证已发布(等待同步后)
# 浏览器访问:
# https://repo1.maven.org/maven2/com/youlai/youlai-security-spring-boot-starter/

6.12.6 快照版本SNAPSHOT发布

开发阶段可发布 SNAPSHOT 版本供其他项目测试:

<!-- pom.xml 版本号带 -SNAPSHOT -->
<version>4.3.2-SNAPSHOT</version>
# SNAPSHOT 发布到 Central Portal Snapshots
mvn clean deploy -P central

# 使用方添加快照仓库
<!-- 使用方 pom.xml -->
<repositories>
  <repository>
    <id>central-snapshots</id>
    <url>https://central.sonatype.com/repository/maven-snapshots/</url>
    <snapshots><enabled>true</enabled></snapshots>
  </repository>
</repositories>

6.13 使用方式(其他项目接入)

6.13.1 引入依赖

<!-- 使用方 pom.xml -->
<dependencies>
  <!-- 引入 youlai Security Starter -->
  <dependency>
    <groupId>com.youlai</groupId>
    <artifactId>youlai-security-spring-boot-starter</artifactId>
    <version>1.0.0</version>
  </dependency>
</dependencies>

6.13.2 配置 application.yml

security:
  enabled: true
  session:
    type: jwt                  # jwt | redis-token
    access-token-time-to-live: 7200
    refresh-token-time-to-live: 604800
    jwt:
      secret-key: ${JWT_SECRET:your-strong-secret-key-here}
    redis-token:
      allow-multi-login: true
  ignore-urls:
    - /api/v1/auth/login/**
    - /api/v1/auth/refresh-token
  unsecured-urls:
    - /doc.html
    - /swagger-ui/**
    - /v3/api-docs/**

6.13.3 实现适配器(必须)

// 使用方必须提供两个适配器 Bean否则启动失败
@Component
public class MyUserAuthenticationAdapter implements UserAuthenticationPort {

    @Autowired
    private MyUserService myUserService;  // 使用方自己的用户服务

    @Override
    public SecurityUser getAuthInfoByUsername(String username) {
        // 调用使用方自己的用户查询逻辑
        MyUserEntity user = myUserService.findByUsername(username);
        if (user == null) return null;

        SecurityUser securityUser = new SecurityUser();
        securityUser.setUserId(user.getId());
        securityUser.setUsername(user.getUsername());
        securityUser.setPassword(user.getPassword());
        securityUser.setStatus(user.getStatus());
        securityUser.setDeptId(user.getDeptId());
        securityUser.setRoles(user.getRoleCodes());
        securityUser.setDataScopes(user.getDataScopes());
        return securityUser;
    }

    @Override
    public SecurityUser getAuthInfoByMobile(String mobile) {
        // ... 实现手机号查询
    }

    @Override
    public SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid) {
        // ... 实现第三方登录查询(如不支持可 return null
    }
}

@Component
public class MyPermissionAdapter implements PermissionPort {

    @Autowired
    private MyRoleMenuService roleMenuService;

    @Override
    public Set<String> getRolePerms(Set<String> roleCodes) {
        return roleMenuService.findPermsByRoleCodes(roleCodes);
    }
}

6.13.4 编写 SecurityConfig使用方自定义安全规则

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private TokenAuthenticationFilter tokenAuthenticationFilter;  // Starter 提供

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;    // 使用方提供

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;              // 使用方提供

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/login").permitAll()
                .requestMatchers("/api/v1/auth/captcha").permitAll()
                .anyRequest().authenticated()
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler)
            )
            // 必须位于 ExceptionTranslationFilter 之后、AuthorizationFilter 之前,
            // 这样 TokenAuthenticationFilter 抛出的 AuthenticationException 会由
            // AuthenticationEntryPoint 统一处理。
            .addFilterBefore(tokenAuthenticationFilter, AuthorizationFilter.class);
        return http.build();
    }
}

使用方需要提供 AuthenticationEntryPointAccessDeniedHandler Bean。youlai 项目可以在这两个 Bean 内继续使用自己的 ResponseWriter 输出统一 Result;其他项目可以返回空 401/403、Problem Details 或自有 JSON。Starter 不关心响应格式。

6.13.5 验证接入成功

// 启动应用,日志应显示自动配置已加载:
// ... SecurityAutoConfiguration matched
// ... JwtTokenAutoConfiguration matched或 RedisTokenAutoConfiguration

// 调用登录接口测试
// POST /api/v1/auth/login → 返回 token
// GET /api/v1/sys/users带 token + @PreAuthorize("@ss.hasPerm('sys:user:list')"))→ 200 或 403

6.14 迁移路径总览

阶段 1P1-P3 端口/适配器解耦(第四节/第五节)
    │
    ├── UserAuthInfo 重命名为 SecurityUser
    ├── 枚举下沉 SocialPlatformEnum → youlai-common
    └── 删除空占位文件、UserSession
    │
    ↓
阶段 2创建 Starter 模块6.11 实操步骤)
    │
    ├── 6.11.1  重命名 UserAuthInfo → SecurityUser
    ├── 6.11.2  创建 Starter 模块 + pom.xml
    ├── 6.11.3  根 POM 注册模块
    ├── 6.11.4  迁移代码到 Starter
    ├── 6.11.5  创建自动配置类 + SPI 文件
    ├── 6.11.6  youlai-framework pom.xml 改为引用 Starter
    ├── 6.11.7  枚举下沉
    ├── 6.11.8  验证编译 + 零 system 依赖
    └── 6.11.9  验证自动装配
    │
    ↓
阶段 3发布到 Maven Central6.12
    │
    ├── 6.12.1  Sonatype 账号 + GPG 密钥
    ├── 6.12.2  上传 GPG 公钥
    ├── 6.12.3  配置 settings.xml
    ├── 6.12.4  配置 pom.xml 发布插件
    ├── 6.12.5  mvn clean deploy
    └── 6.12.6  验证 Maven Central
    │
    ↓
阶段 4其他项目接入6.13
    │
    ├── 6.13.1  引入依赖
    ├── 6.13.2  配置 application.yml
    ├── 6.13.3  实现适配器
    ├── 6.13.4  编写 SecurityConfig
    └── 6.13.5  验证接入

七、测试策略

7.1 单元测试

测试目标 Mock 对象 验证点
SecurityUserDetailsService UserAuthenticationPort 返回正确 SecurityUserDetails;用户不存在抛 UsernameNotFoundException
PermissionService PermissionPort 有权限返回 true无权限返回 false超管放行
UserAuthenticationAdapter UserServiceUserSocialService 委托调用正确;返回值转换正确
PermissionAdapter RoleMenuService 委托调用正确
RedisTokenManager RedisTemplateJsonMapper 存取 SecurityUserDetails 正确;password 为 null
JwtTokenManager JWT 签发/解析 roles 正确;getAuthorities()ROLE_ 前缀

7.2 集成测试

  • 登录流程:用户名密码 → 生成 token → 解析 token → 权限校验
  • Redis-Token 模式:登录 → Redis 存储 → 解析还原 → 踢人
  • JWT 模式:登录 → JWT 签发 → 解析 → tokenVersion 失效
  • SpEL 权限:@PreAuthorize("@ss.hasPerm('sys:user:create')") 通过/拒绝

7.3 回归测试用例

  • 用户认证:正确用户名/密码登录成功;错误用户名或密码登录失败
  • 用户禁用:用户状态为禁用时,登录应被拒绝(isEnabled() 返回 false
  • 角色加载:登录成功后,SecurityUserDetails.getAuthorities() 获得正确的角色列表(前缀 ROLE_
  • 权限校验@PreAuthorizess.hasPerm 表达式,有权限放行、无权限拒绝
  • 微信登录:通过 UserAuthenticationPort.getAuthInfoByOpenid 测试社交登录
  • 边界情况:角色集合为空、权限集合为空、端口实现抛异常
  • 事务和并发:高并发登录、权限检查,确保端口实现线程安全
  • 登录态失效:发布后旧 JWT / Redis Token 均返回 401前端跳转登录页

八、实施阶段

阶段 内容 风险 可独立发布
P0 多模块 parent 版本统一;确认 SocialPlatformEnum 下沉;确认旧 Token 发布失效策略 低(前置整理)
P1 SecurityUserDetailsroles + 删 UserSession + RedisTokenManager 简化 + SecurityUtils 简化 + 删除 4 个空占位文件 低(内部改造,接口不变)
P2 JwtTokenManager 适配 roles 中(需回归 JWT 登录流程)
P3 端口/适配器解耦 + 枚举下沉 中(影响面大,需全量回归)
P4 Security Starter 抽离6.11 实操) 高(模块拆分,需完整回归)
P5 发布到 Maven Central6.12 中(发布流程,不影响代码)

P0 必须先完成。P1、P2 可合并发布模型改造一起做P3 单独发布P4 在 P3 稳定后执行P5 在 P4 验证通过后执行。


九、AI 协作指南

本节为 AI 修改 security 代码的操作规范。任何 AI 在修改前必须阅读。

9.1 修改前必读检查

□ 已阅读第〇节「文档使用指南」
□ 已阅读第二节「设计决策」,确认不违背 D1-D8
□ 已确认当前代码处于哪个实施阶段P0/P1/P2/P3/P4/P5
□ 已确认是单模块还是多模块项目

9.2 代码修改检查清单

□ framework/security 下的代码不含 import com.youlai.boot.system.*
□ 端口接口在 framework.security.port 包,无 system 依赖
□ 适配器在 system.security.adapter 包,标注 @Component
□ 端口返回模型使用 SecurityUser由 UserAuthInfo 重命名)
□ SecurityUserDetails.roles 为 Set<String>getAuthorities() 实时计算
□ UserSession.java 已删除,无残留引用
□ RedisTokenManager 存取 SecurityUserDetailspassword 置 null
□ JwtTokenManager parseToken 设 rolesJWT claims 格式不变
□ SecurityUtils.getRoles() 直接取 roles 字段,无 ROLE_ strip
□ 所有新增接口和类有 Javadoc 注释
□ 端口接口 @see 注释引用适配器类(方便定位实现)
□ 多模块youlai-framework/pom.xml 不依赖 youlai-system

9.3 新增端口/适配器决策树

需要安全模块调用 system 模块的新功能?
│
├─ 是 → 是否能通过现有端口完成?
│       │
│       ├─ 能 → 在现有端口接口添加方法,适配器实现
│       └─ 不能 → 需要新端口
│               │
│               ├─ 功能是否属于认证鉴权最小内核?
│               │   ├─ 是 → 新增核心端口framework.security.port
│               │   └─ 否 → 不加入 Security Starter由业务模块自行实现
│               │
│               └─ 命名:<领域意图>Port如 AuditLogPort
│
└─ 否 → 不需要端口,直接在 framework 内实现

9.4 常见问题

Q: 为什么不用 @Autowired 而用构造器注入? A: 构造器注入(@RequiredArgsConstructor + final)保证依赖不可变、非空,符合 Spring 官方推荐。

Q: 端口接口需要标注 @Component 吗? A: 不需要。端口是接口,由适配器实现类标注 @ComponentSpring 自动按类型注入。

Q: SocialPlatformEnum 为什么必须下沉到 common A: 端口接口 UserAuthenticationPort 引用了它。如果端口在 Starter 模块而枚举在 system 模块Starter 编译期就会依赖 system——违背解耦目标。

Q: 可以在端口接口返回 system 实体类吗? A: 禁止。端口返回类型必须为 SecurityUser(纯 POJO或 JDK 基础类型(Set<String> 等)。适配器负责实体 → POJO 转换。

Q: 为什么 UserAuthInfo 要重命名为 SecurityUser A: 两点原因:① 语义——UserAuthInfo 偏向"认证信息",但该模型实际承载用户安全数据(角色、权限范围、部门等),SecurityUser 更准确;② 作为 Starter 公开 APISecurityUserUserAuthInfo 更中性、更规范,其他项目接入时更直观。重命名而非新建,改动最小。

Q: SecurityUserSecurityUserDetails 有什么区别? A: SecurityUser 是端口返回的安全数据 POJO含 status、nickname 等原始字段);SecurityUserDetails 是 Spring Security UserDetails 实现(含 enabled、roles 等认证字段)。SecurityUserDetailsSecurityUser 构造,数据流:Adapter 查 DB → SecurityUser → SecurityUserDetails

Q: 新增了一个安全过滤器,放在哪? A: 放在 framework/security/filter/(单模块)或 Starter 的 filter/ 包。过滤器属于框架基础设施,不依赖 system。

Q: SecurityConfig 应该放在哪? A: 放在使用方system 模块或 application 模块)。不同项目安全规则不同,不应由 Starter 强制装配。Starter 仅提供 SecurityProperties

Q: 如何确认重构后 framework 层零 system 依赖? A: 在 framework/security 目录执行全局搜索 import com.youlai.boot.system,结果应为空(SocialPlatformEnum 下沉后)。

9.5 文档修改规范

  1. 修改本文档时:更新顶部「最后更新」日期,并在附录 B 变更日志追加记录
  2. 新增设计决策时:在第〇节 0.3 关键决策表追加 D7D8... 编号,不得修改已有决策编号
  3. 新增端口时:更新第二节 2.2 端口定义表、第六节 6.10 边界界定表
  4. 完成实施阶段时:在第八节对应阶段标注「 已完成」及日期
  5. 代码示例更新时:保持与实际代码同步,注释标注 // 改动点: 说明差异

9.6 AI 提示词模板

当需要让 AI 执行 security 重构时,可使用以下提示词:

请按照 docs/security-refactor-plan.md 执行 security 模块重构。

当前阶段P3端口/适配器解耦)
项目类型单模块youlai-boot

要求:
1. 先阅读第〇节「文档使用指南」和第二节「设计决策」
2. 按第四节「单模块重构」执行
3. 完成后对照第十节「代码审查清单」自查
4. 不违背 D1-D8 任何决策
5. 删除 4 个空占位文件(见 0.4 节)

十、代码审查清单

10.1 模型改造P1/P2

  • SecurityUserDetailsroles 字段为 Set<String>getAuthorities() 实时计算
  • UserSession.java 已删除,无残留引用
  • RedisTokenManager 存取 SecurityUserDetailspassword 置 null
  • JwtTokenManagerparseTokenrolesJWT claims 格式不变
  • SecurityUtils.getRoles() 直接取 roles 字段,无 ROLE_ strip 逻辑
  • 4 个空占位文件已删除(UserAuthQueryServiceRolePermissionServiceWxMaUserAuthQueryServiceWxMaBindInfo

10.2 端口/适配器解耦P3

  • SecurityUserDetailsService 不含 import com.youlai.boot.system.*
  • PermissionService 不含 import com.youlai.boot.system.*
  • 端口接口在 framework.security.port 包下,无 system 依赖
  • 适配器在 system.security.adapter 包下,标注 @Component
  • 端口接口有 @see 注释引用适配器类
  • SocialPlatformEnum 已下沉到 youlai-commonP3/Starter 阶段)
  • SmsAuthenticationProviderWxMaAuthenticationProvider 留在使用方,未迁入 Starter

10.3 Starter 抽离P4

  • UserAuthInfo 已重命名为 SecurityUser,所有引用已更新
  • youlai-security-spring-boot-starter 模块已创建
  • pom.xml 固定包含 Security、Web、Redis、Hutool JWT 依赖
  • AutoConfiguration.imports 文件已创建,注册 3 个自动配置类
  • JwtTokenAutoConfiguration 使用 @ConditionalOnClass + @ConditionalOnProperty
  • RedisTokenAutoConfiguration 使用 @ConditionalOnClass + @ConditionalOnProperty
  • SecurityConfig 留在使用方,不在 Starter 中强制装配
  • youlai-framework 移除 security 子包pom.xml 改为引用 Starter
  • SocialPlatformEnum 已下沉到 youlai-common
  • Starter 模块零 import com.youlai.boot.system.*(已验证)

10.4 Starter 发布

  • Sonatype Central Portal 账号已注册namespace/GroupId 已验证
  • GPG 密钥已生成并上传到 3 个密钥服务器
  • settings.xml 已配置 central server 凭据
  • pom.xml 已配置 maven-source-pluginmaven-javadoc-pluginmaven-gpg-plugin
  • pom.xml 已配置 central-publishing-maven-plugin(新方式)
  • pom.xml 已配置 <licenses><developers><scm> 元信息
  • mvn clean deploy -P central 执行成功
  • Maven Central 可搜索到构件(https://central.sonatype.com

10.5 通用规范

  • 所有新增接口和类有 Javadoc 注释(中文)
  • 端口方法签名包含 @param@return@throws 注释
  • 使用构造器注入(@RequiredArgsConstructor + final
  • 日志不输出密码、密钥等敏感信息
  • 单元测试覆盖端口调用和适配器委托
  • 多模块:youlai-framework/pom.xml 不依赖 youlai-system
  • 多模块parent 版本与根 POM 一致4.3.1

附录 A术语表

术语 含义
Port端口 安全模块定义的抽象接口,描述需要外部提供的功能。放在 framework.security.port 包。
Adapter适配器 system 模块中对 Port 接口的具体实现,委托 system Service 完成实际操作。放在 system.security.adapter 包。
Ports & Adapters 六边形架构模式,核心业务通过端口与外部通信,外部实现通过适配器连入。
SecurityUser 安全模块的用户安全数据 POJO纯 JDK 类型,无 system 依赖。端口接口的返回类型。由原 UserAuthInfo 重命名而来D2 决策)。
SecurityUserDetails Spring Security UserDetails 实现,封装认证后的用户信息。roles 字段为 Set<String>
UserSession 待删除Redis 存储的会话中间模型,字段是 SecurityUserDetails 的子集。
TokenManager Token 管理器接口,有 JwtTokenManagerRedisTokenManager 两个实现。
SecurityProperties 安全配置属性类,@ConfigurationProperties(prefix = "security")
Starter Spring Boot 自动装配模块,引入依赖即自动配置。本项目命名为 youlai-security-spring-boot-starter
AutoConfiguration.imports Spring Boot 3.x+ 的自动配置注册文件,位于 META-INF/spring/ 目录。
D1-D8 本文档第〇节定义的 8 条关键设计决策,不可违背。

附录 B变更日志

日期 版本 变更内容
2026-06-29 v1.0 初版 security-refactor-plan.md模型消除 + 单/多模块教程)
2026-06-29 v1.0 deep-research-report.md 归档Ports & Adapters 研究)
2026-07-04 v2.0 合并两文档:消除重复逻辑(统一模型为 SecurityUser、端口收敛、重复的模式介绍新增第六节 Security Starter 抽离方案;新增第九节 AI 协作指南补充空占位文件说明0.4 节补充枚举下沉方案6.7 节补充多模块版本不一致提醒5.2 节)
2026-07-04 v2.1 SecurityUser 重命名 + Starter 实操发布指南D2 决策更新(UserAuthInfoSecurityUser 重命名,非新建);全文 UserAuthInfo 替换为 SecurityUser;新增第六节 6.11 实操步骤(命令行);新增 Starter 发布与使用方式
2026-07-04 v2.2 方案收敛为唯一施工路线:删除 OnlineUserPort 与额外仓库分支;明确不接受旧 TokenJWT claims 统一为 rolesStarter 不依赖 ResponseWriter,异常响应交给使用方 AuthenticationEntryPoint / AccessDeniedHandler;配置项统一使用 security.session.*

参考资料:本方案基于 youlai-boot 项目 framework/securitysystem 模块的实际代码分析,结合 Ports & Adapters六边形架构模式和 Spring Boot Starter 自动装配规范设计。所有代码示例中的包名、类名、方法签名均与实际代码库对齐。