大部分 RBAC 教程只讲”用户-角色-权限”三张表的设计,但真正的痛点是:不同角色的登录逻辑、密码修改、注册流程都不一样,用 if-else 写完就是一场灾难。本文基于 IntensifiedMyCode 和 springboot-core-arch 两个项目,把 RBAC 权限系统和 AOP 审计日志的完整实现从头到尾讲透。
第一章:if-else 地狱长什么样 假设有4种角色:超级管理员、部门管理员、社团负责人、普通用户。登录接口要区分角色走不同逻辑:
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 @PostMapping("/login") public R login (@RequestBody Account account) { String role = account.getRole(); if ("SUPER_ADMIN" .equals(role)) { return R.success(adminService.login(account)); } else if ("DEPT_ADMIN" .equals(role)) { return R.success(deptAdminService.login(account)); } else if ("CLUB_LEADER" .equals(role)) { return R.success(clubLeaderService.login(account)); } else if ("USER" .equals(role)) { return R.success(userService.login(account)); } else { return R.error("未知角色" ); } } @PostMapping("/register") public R register (@RequestBody Account account) { String role = account.getRole(); if ("SUPER_ADMIN" .equals(role)) { return R.error("管理员请联系超级管理员添加" ); } else if ("USER" .equals(role)) { userService.register(account); return R.ok(); } else if ... } @PostMapping("/updatePassword") public R updatePassword (@RequestBody Account account) { String role = account.getRole(); if ("SUPER_ADMIN" .equals(role)) { adminService.updatePassword(account); } else if ... }
问题 :
每新增一个角色,所有相关方法都要加 if-else 分支
分支逻辑散落在各个 Controller 中,难以维护
违反开闭原则——修改角色逻辑要改已有代码
测试困难——每个分支都要单独覆盖
第二章:角色枚举设计 2.1 RoleEnum 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 @Getter public enum RoleEnum { SUPER_ADMIN("SUPER_ADMIN" , "超级管理员" ), DEPT_ADMIN("DEPT_ADMIN" , "部门管理员" ), CLUB_LEADER("CLUB_LEADER" , "社团负责人" ), USER("USER" , "普通用户" ); private final String code; private final String label; RoleEnum(String code, String label) { this .code = code; this .label = label; } public static RoleEnum fromCode (String code) { for (RoleEnum value : values()) { if (value.getCode().equalsIgnoreCase(code)) { return value; } } throw new CustomerException ("无效角色标识: " + code); } public static boolean isAdminRole (String code) { return SUPER_ADMIN.code.equals(code) || DEPT_ADMIN.code.equals(code) || CLUB_LEADER.code.equals(code); } public static boolean isUserRole (String code) { return USER.code.equals(code); } }
枚举的好处 :角色码集中管理,不会出现拼写错误。fromCode 做强校验——传一个不存在的角色码会直接抛异常,而不是静默走错分支。
第三章:策略模式实现 3.1 策略接口 1 2 3 4 5 6 7 public interface RoleStrategy { String getRole () ; Account login (Account account) ; void updatePassword (Account account) ; void register (Account account) ; Account selectById (String userId) ; }
3.2 策略实现 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 @Component public class AdminStrategy implements RoleStrategy { @Autowired private AdminService adminService; @Override public String getRole () { return RoleEnum.SUPER_ADMIN.getCode(); } @Override public Account login (Account account) { return adminService.login(account); } @Override public void updatePassword (Account account) { adminService.updatePassword(account); } @Override public void register (Account account) { throw new UnsupportedOperationException ("管理员账号不支持自助注册" ); } @Override public Account selectById (String userId) { return adminService.selectById(userId); } } @Component public class UserStrategy implements RoleStrategy { @Autowired private UserService userService; @Override public String getRole () { return RoleEnum.USER.getCode(); } @Override public Account login (Account account) { return userService.login(account); } @Override public void updatePassword (Account account) { userService.updatePassword(account); } @Override public void register (Account account) { userService.register(account); } @Override public Account selectById (String userId) { return userService.selectById(userId); } }
3.3 策略上下文:Spring 自动装配的魔法 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 @Component public class RoleStrategyContext { private final Map<String, RoleStrategy> strategyMap = new HashMap <>(); public RoleStrategyContext (List<RoleStrategy> strategies) { for (RoleStrategy strategy : strategies) { strategyMap.put(strategy.getRole().toUpperCase(), strategy); } } public RoleStrategy getStrategy (String roleCode) { RoleEnum.fromCode(roleCode); RoleStrategy strategy = strategyMap.get(roleCode.toUpperCase()); if (strategy == null ) { throw new CustomerException ("暂未支持的角色类型: " + roleCode); } return strategy; } public String refreshToken (String oldToken) { DecodedJWT decodedJWT = JwtUtil.verifyToken(oldToken); String userId = decodedJWT.getSubject(); List<String> roles = decodedJWT.getClaim("roles" ).asList(String.class); String role = roles.get(0 ).toUpperCase(); RoleStrategy strategy = getStrategy(role); Account account = strategy.selectById(userId); return JwtUtil.generateAccessToken( String.valueOf(account.getId()), List.of(account.getRole()) ); } }
3.4 自动装配原理 Spring 在初始化 RoleStrategyContext 时,发现构造器参数是 List<RoleStrategy>,会自动从容器中收集所有 RoleStrategy 类型的 Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Spring 容器扫描 @Component │ ├── AdminStrategy (getRole() = "SUPER_ADMIN") └── UserStrategy (getRole() = "USER") │ ▼ 注入到 List<RoleStrategy> │ RoleStrategyContext 构造 │ ▼ 遍历 List,按 getRole() 建 Map │ strategyMap = { "SUPER_ADMIN" → AdminStrategy, "USER" → UserStrategy }
好处 :新增角色时,只需写一个新的 @Component 策略类,不需要修改 RoleStrategyContext 和任何已有代码——完全符合开闭原则。
3.5 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 26 27 28 @RestController public class WebController { @Resource private RoleStrategyContext roleStrategyContext; @PostMapping("/login") public R login (@RequestBody Account account) { LoginResult result = roleStrategyContext .getStrategy(account.getRole()) .login(account); return R.success(result); } @PostMapping("/updatePassword") public R updatePassword (@RequestBody Account account) { roleStrategyContext.getStrategy(account.getRole()).updatePassword(account); return R.ok(); } @PostMapping("/register") public R register (@RequestBody Account account) { account.setRole(RoleEnum.USER.getCode()); roleStrategyContext.getStrategy(account.getRole()).register(account); return R.ok(); } }
对比 if-else 版本 :Controller 代码量减少 80%,且新增角色时无需改动 Controller。
第四章:RBAC 数据库设计 4.1 经典三表结构 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 CREATE TABLE `roles` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `code` varchar (30 ) NOT NULL COMMENT '角色编码' , `name` varchar (50 ) NOT NULL , `description` varchar (255 ) DEFAULT NULL , PRIMARY KEY (`id`), UNIQUE INDEX `role_code_unique`(`code`) ); INSERT INTO `roles` VALUES (1 , 'SUPER_ADMIN' , '超级管理员' , '拥有全部管理权限' ), (2 , 'DEPT_ADMIN' , '部门管理员' , '管理部门内部事务' ), (3 , 'CLUB_LEADER' , '社团负责人' , '管理社团相关事务' ), (4 , 'USER' , '普通用户' , '仅可访问首页' ); CREATE TABLE `users` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (50 ) NOT NULL , `password` varchar (100 ) NOT NULL COMMENT '密码(BCrypt)' , `name` varchar (20 ) NOT NULL , PRIMARY KEY (`id`), UNIQUE INDEX `username_index`(`username`) ); CREATE TABLE `user_roles` ( `user_id` int (11 ) NOT NULL , `role_id` int (11 ) NOT NULL , PRIMARY KEY (`user_id`, `role_id`) );
4.2 SQL 层的角色过滤 管理员和普通用户共用 users 表,通过 SQL JOIN 区分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 < ! SELECT u.* , GROUP_CONCAT(r.code) AS roleFROM users uINNER JOIN user_roles ur ON u.id = ur.user_idINNER JOIN roles r ON ur.role_id = r.idWHERE r.code IN ('SUPER_ADMIN' , 'DEPT_ADMIN' , 'CLUB_LEADER' )GROUP BY u.id< ! SELECT u.* , GROUP_CONCAT(r.code) AS roleFROM users uINNER JOIN user_roles ur ON u.id = ur.user_idINNER JOIN roles r ON ur.role_id = r.idWHERE r.code = 'USER' GROUP BY u.id
统一 users 表 vs 分表 :统一表设计简单(不需要跨表 JOIN),分表隔离性好(不同角色字段不同)。本项目用统一表 + 角色码区分。
第五章:方法级权限注解 5.1 启用注解权限 1 2 3 4 @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { ... }
5.2 使用示例 1 2 3 @PostAuthorize("hasRole('SUPER_ADMIN')") @GetMapping("/getAll") public R getAllMenu () { ... }
5.3 ⚠️ 常见陷阱 hasRole('SUPER_ADMIN') 需要 SecurityContext 中有 ROLE_SUPER_ADMIN 的 GrantedAuthority。但很多项目的 JWT 过滤器在设置 Authentication 时传了 Collections.emptyList()(空权限列表),导致 @PostAuthorize 永远拒绝访问。
正确做法 :
1 2 3 4 5 6 7 8 List<GrantedAuthority> authorities = Arrays.asList( new SimpleGrantedAuthority ("ROLE_" + role.toUpperCase()) ); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken ( account.getUsername(), null , authorities); SecurityContextHolder.getContext().setAuthentication(authToken);
第六章:声明式审计日志 6.1 目标效果 1 2 3 4 5 6 7 8 9 10 11 @AuditLogRecord(action = "登录", resource = "用户") @PostMapping("/login") public R login (@RequestBody Account account) { ... }@AuditLogRecord(action = "添加管理员", resource = "管理员") @PostMapping("/add") public R add (@RequestBody Admin admin) { ... }@AuditLogRecord(action = "修改密码", resource = "用户") @PostMapping("/updatePassword") public R updatePassword (@RequestBody Account account) { ... }
一个注解搞定——不需要在方法体内写任何日志代码。
6.2 审计日志注解 1 2 3 4 5 6 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AuditLogRecord { String action () ; String resource () default "" ; }
6.3 审计日志实体 1 2 3 4 5 6 7 8 9 10 @Data @Builder @NoArgsConstructor @AllArgsConstructor public class AuditLog { private Long id; private String username; private String action; private String resource; private String ipAddress; private String details; private Timestamp createdAt; }
6.4 审计日志表(Flyway初始化) 1 2 3 4 5 6 7 8 9 10 11 12 CREATE TABLE `audit_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR (50 ) NOT NULL , `action` VARCHAR (100 ) NOT NULL , `resource` VARCHAR (50 ) DEFAULT NULL , `ip_address` VARCHAR (50 ) DEFAULT NULL , `details` TEXT, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , PRIMARY KEY (`id`), INDEX `idx_username` (`username`), INDEX `idx_created_at` (`created_at`) );
第七章:AOP 切面实现 7.1 核心代码 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 @Aspect @Component public class AuditLogAspect { @Resource private AuditLogService auditLogService; @Around("@annotation(auditLogRecord)") public Object handleAudit (ProceedingJoinPoint joinPoint, AuditLogRecord auditLogRecord) throws Throwable { String username = getCurrentUsername(); String ip = getClientIp(); long startTime = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); String safeDetails = sanitizeArgs(joinPoint.getArgs()); AuditLog successLog = AuditLog.builder() .username(username) .action(auditLogRecord.action()) .resource(auditLogRecord.resource()) .ipAddress(ip) .details(safeDetails) .build(); auditLogService.saveLog(successLog); return result; } catch (Throwable e) { String safeDetails = sanitizeArgs(joinPoint.getArgs()); AuditLog errorLog = AuditLog.builder() .username(username) .action(auditLogRecord.action()) .resource(auditLogRecord.resource()) .ipAddress(ip) .details(safeDetails + " | 异常: " + e.getMessage()) .build(); auditLogService.saveLog(errorLog); throw e; } } }
7.2 切点表达式解析 @Around("@annotation(auditLogRecord)") 的含义:
匹配所有标注了 @AuditLogRecord 注解的方法
auditLogRecord 参数名与注解变量名一致,Spring 会自动绑定注解实例
等价写法 (手动获取注解):
1 2 3 4 5 6 7 @Around("@annotation(com.example.annotation.AuditLogRecord)") public Object handleAudit (ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); AuditLogRecord auditLogRecord = signature.getMethod() .getAnnotation(AuditLogRecord.class); }
前者更简洁,后者更灵活(可以在方法体内决定是否记录日志)。
第八章:密码脱敏 8.1 问题 如果直接 Arrays.toString(args) 记录方法参数,密码会以明文出现在审计日志中:
1 details: [Account(username=admin, password=123456, role=SUPER_ADMIN)]
审计日志通常有 DBA、运维等多方查看权限,密码明文是严重的安全隐患。
8.2 正则脱敏实现 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 private String sanitizeArgs (Object[] args) { if (args == null || args.length == 0 ) return "[]" ; String sanitized = Arrays.stream(args) .map(this ::sanitizeArg) .collect(Collectors.joining(", " )); return "[" + sanitized + "]" ; } private String sanitizeArg (Object arg) { if (arg == null ) return "null" ; String argStr = arg.toString(); if (argStr.toLowerCase().contains("password" ) || argStr.toLowerCase().contains("pwd" )) { argStr = argStr.replaceAll( "(?i)(password|pwd|newPassword|oldPassword)\\s*=\\s*[^,}\\]]+" , "$1=***" ); } return argStr; }
8.3 正则解析 1 (?i)(password|pwd|newPassword|oldPassword)\s*=\s*[^,}\]]+
部分
含义
(?i)
大小写不敏感
(password|pwd|...)
匹配密码相关字段名
\s*=\s*
匹配等号(允许空格)
[^,}\]]+
匹配值(到逗号、右花括号或右方括号为止)
替换效果 :
1 2 输入: Account(username=admin, password=123456, role=SUPER_ADMIN) 输出: Account(username=admin, password=***, role=SUPER_ADMIN)
8.4 脱敏的局限性 这个正则方案依赖于对象的 toString() 输出格式为 key=value。对于 Lombok @Data 生成的 toString,格式是 ClassName(field1=value1, field2=value2),能正常工作。
但如果参数是 JSON 字符串({"password":"123456"}),正则无法匹配。更健壮的方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class PasswordSerializer extends JsonSerializer <String> { @Override public void serialize (String value, JsonGenerator gen, SerializerProvider provider) { gen.writeString("***" ); } } @JsonSerialize(using = PasswordSerializer.class) private String password;@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Sensitive { String strategy () default "PASSWORD" ; }
第九章:获取当前用户和IP 9.1 获取当前用户 1 2 3 4 5 6 7 8 9 10 11 private String getCurrentUsername () { try { var authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { return authentication.getName(); } } catch (Exception e) { log.debug("无法获取当前用户: " + e.getMessage()); } return "anonymous" ; }
为什么登录接口会是 anonymous :@AuditLogRecord(action = "登录") 标注在登录方法上,但登录成功前 SecurityContext 中还没有 Authentication,所以用户名为 anonymous。解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Around("@annotation(auditLogRecord)") public Object handleAudit (ProceedingJoinPoint joinPoint, AuditLogRecord record) throws Throwable { String username = getCurrentUsername(); if ("anonymous" .equals(username)) { for (Object arg : joinPoint.getArgs()) { if (arg instanceof Account) { username = ((Account) arg).getUsername(); break ; } } } }
9.2 获取客户端IP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private String getClientIp () { try { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = attrs.getRequest(); String ip = request.getHeader("X-Forwarded-For" ); if (ip == null || ip.isEmpty() || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP" ); } if (ip == null || ip.isEmpty() || "unknown" .equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } if (ip != null && ip.contains("," )) { ip = ip.split("," )[0 ].trim(); } return ip; } catch (Exception e) { return "unknown" ; } }
第十章:分布式锁防重复注册 10.1 分布式锁工具 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Component public class DistributedLockUtils { public boolean tryLock (String lockKey, String requestId, int expireTime) { Boolean result = redisTemplate.opsForValue() .setIfAbsent("lock:" + lockKey, requestId, expireTime, TimeUnit.SECONDS); return result != null && result; } public boolean releaseLock (String lockKey, String requestId) { String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('del', KEYS[1]) " + "else return 0 end" ; DefaultRedisScript<Long> redisScript = new DefaultRedisScript <>(luaScript, Long.class); Long result = redisTemplate.execute(redisScript, Collections.singletonList("lock:" + lockKey), requestId); return result != null && result > 0 ; } }
10.2 使用场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void register (User user) { String lockKey = "user:register:" + user.getUsername(); String requestId = String.valueOf(System.currentTimeMillis()); if (distributedLockUtils.tryLock(lockKey, requestId, 10 )) { try { if (userMapper.findByUsername(user.getUsername()) != null ) { throw new CustomerException ("用户名已存在" ); } user.setPassword(PasswordEncoder.encode(user.getPassword())); userMapper.insert(user); } finally { distributedLockUtils.releaseLock(lockKey, requestId); } } else { throw new CustomerException ("系统繁忙,请稍后再试" ); } }
为什么需要分布式锁 :用户快速点击两次注册按钮,两个请求同时检查用户名(都显示不存在),然后都执行 INSERT——第二个会因唯一约束报错,但第一个已经创建了用户。分布式锁确保同一用户名的注册操作串行执行。
第十一章:审计日志查询 11.1 分页查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping("/auditLog/page") public R page (@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(required = false) String username, @RequestParam(required = false) String action) { Map<String, Object> params = new HashMap <>(); params.put("username" , username); params.put("action" , action); params.put("startIndex" , (pageNum - 1 ) * pageSize); params.put("pageSize" , pageSize); List<AuditLog> list = auditLogService.selectPage(params); int total = auditLogService.selectCount(params); return R.success(new PageResult (list, total, pageNum, pageSize)); }
11.2 MyBatis XML 1 2 3 4 5 6 7 8 9 10 11 12 <select id ="selectPage" resultType ="AuditLog" > SELECT * FROM audit_log WHERE 1=1 <if test ="username != null and username != ''" > AND username LIKE CONCAT('%', #{username}, '%') </if > <if test ="action != null and action != ''" > AND action = #{action} </if > ORDER BY created_at DESC LIMIT #{startIndex}, #{pageSize} </select >
第十二章:性能考虑 12.1 同步 vs 异步记录 本文实现是同步记录(auditLogService.saveLog() 在请求线程中执行)。对于高并发场景,建议改为异步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Async public void saveLog (AuditLog log) { auditLogMapper.insert(log); } @Around("@annotation(auditLogRecord)") public Object handleAudit (ProceedingJoinPoint joinPoint, AuditLogRecord record) throws Throwable { rabbitTemplate.convertAndSend("audit.log.queue" , auditLog); } private final List<AuditLog> buffer = new ArrayList <>();public synchronized void saveLog (AuditLog log) { buffer.add(log); if (buffer.size() >= 100 ) { auditLogMapper.batchInsert(buffer); buffer.clear(); } }
12.2 AOP 性能开销
操作
耗时
AOP 代理创建
应用启动时一次性开销
切点匹配
每次方法调用 ~0.01ms
参数序列化+脱敏
~0.1-1ms(取决于参数复杂度)
数据库写入
~1-5ms
对于后台管理系统,这个开销可接受。对于超高并发的接口,建议只记录关键操作。
第十三章:策略模式扩展指南 新增 TEACHER 角色只需 3 步:
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 public enum RoleEnum { TEACHER("TEACHER" , "教师" ); } @Component public class TeacherStrategy implements RoleStrategy { @Override public String getRole () { return "TEACHER" ; } @Override public Account login (Account account) { ... } @Override public void updatePassword (Account account) { ... } @Override public void register (Account account) { ... } @Override public Account selectById (String userId) { ... } } INSERT INTO roles (code, name) VALUES ('TEACHER' , '教师' );
不需要改 :Controller、RoleStrategyContext、任何已有策略类。这就是策略模式 + Spring 自动装配的威力。
总结
技术点
实现
核心文件
策略模式
RoleStrategy 接口 + RoleStrategyContext + List<RoleStrategy> 自动注入
RoleStrategy.java
角色管理
RoleEnum 枚举 + fromCode 强校验
RoleEnum.java
RBAC 三表
roles + users + user_roles(多对多)
SQL
JWT 双Token
AccessToken(30min) + RefreshToken(7天)
TokenUtils.java
声明式审计
@AuditLogRecord 注解 + @Around AOP 切面
AuditLogAspect.java
密码脱敏
正则 (?i)(password|pwd)\s*=\s*[^,}\]]+ 替换为 ***
AuditLogAspect.java
分布式锁
Redis SETNX + Lua释放
DistributedLockUtils.java
方法级权限
@EnableMethodSecurity + @PostAuthorize
SecurityConfig.java
密码加密
BCrypt + 自动加盐 + cost factor
PasswordEncoder.java
核心思想 :用策略模式把”不同角色的不同行为”抽象成接口,用 Spring 的 List<T> 自动注入收集所有实现,用 Map 路由。用 AOP 把审计日志这个横切关注点从业务代码中剥离出来,让 Controller 保持干净。一行注解,背后是代理模式 + 环绕通知 + 反射 + 正则脱敏的完整体系。