Springboot

RBAC 权限系统 + AOP 审计日志实战:从策略模式到密码脱敏

2026-07-07 #java#Springboot#Redis#RBAC

大部分 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 ...
}

问题

  1. 每新增一个角色,所有相关方法都要加 if-else 分支
  2. 分支逻辑散落在各个 Controller 中,难以维护
  3. 违反开闭原则——修改角色逻辑要改已有代码
  4. 测试困难——每个分支都要单独覆盖

第二章:角色枚举设计

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;
}

/**
* 强校验:无效角色码直接抛异常
* ★ 不要返回null,直接抛异常——快速失败
*/
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<>();

// ★ Spring 构造器注入:自动收集所有 RoleStrategy 实现
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;
}

/**
* Token 刷新(策略路由)
*/
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();

// 根据角色找到对应的策略,查用户,生成新Token
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) {
// ★ 策略模式:一行代码搞定,不需要 if-else
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`)
);

-- 预置4个角色
INSERT INTO `roles` VALUES
(1, 'SUPER_ADMIN', '超级管理员', '拥有全部管理权限'),
(2, 'DEPT_ADMIN', '部门管理员', '管理部门内部事务'),
(3, 'CLUB_LEADER', '社团负责人', '管理社团相关事务'),
(4, 'USER', '普通用户', '仅可访问首页');

-- 用户表(统一 users 表,所有角色共用)
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
<!-- 查询管理员(3种管理员角色) -->
SELECT u.*, GROUP_CONCAT(r.code) AS role
FROM users u
INNER JOIN user_roles ur ON u.id = ur.user_id
INNER JOIN roles r ON ur.role_id = r.id
WHERE r.code IN ('SUPER_ADMIN', 'DEPT_ADMIN', 'CLUB_LEADER')
GROUP BY u.id

<!-- 查询普通用户(只查USER角色) -->
SELECT u.*, GROUP_CONCAT(r.code) AS role
FROM users u
INNER JOIN user_roles ur ON u.id = ur.user_id
INNER JOIN roles r ON ur.role_id = r.id
WHERE r.code = 'USER'
GROUP BY u.id

统一 users 表 vs 分表:统一表设计简单(不需要跨表 JOIN),分表隔离性好(不同角色字段不同)。本项目用统一表 + 角色码区分。


第五章:方法级权限注解

5.1 启用注解权限

1
2
3
4
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // ★ 启用 @PreAuthorize / @PostAuthorize
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_ADMINGrantedAuthority。但很多项目的 JWT 过滤器在设置 Authentication 时传了 Collections.emptyList()(空权限列表),导致 @PostAuthorize 永远拒绝访问。

正确做法

1
2
3
4
5
6
7
8
// JWT过滤器中设置权限
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; // 操作来源IP
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)")
* 匹配所有标注了 @AuditLogRecord 注解的方法
* auditLogRecord 参数名与注解变量名一致,Spring自动绑定
*/
@Around("@annotation(auditLogRecord)")
public Object handleAudit(ProceedingJoinPoint joinPoint,
AuditLogRecord auditLogRecord) throws Throwable {

// 1. 获取当前用户
String username = getCurrentUsername();

// 2. 获取客户端IP
String ip = getClientIp();

long startTime = System.currentTimeMillis();

try {
// 3. 执行目标方法
Object result = joinPoint.proceed();

// 4. 记录成功日志(脱敏处理)
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) {
// 5. 记录失败日志(脱敏 + 异常信息)
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 + "]";
}

/**
* 脱敏单个参数
* 检测 password/pwd 字段并替换为 ***
*/
private String sanitizeArg(Object arg) {
if (arg == null) return "null";
String argStr = arg.toString();

// 正则替换:password=xxx, newPassword=xxx 等
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
// 方案1:Jackson自定义序列化器(精确脱敏)
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;

// 方案2:自定义脱敏注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
String strategy() default "PASSWORD"; // PASSWORD, PHONE, EMAIL, ID_CARD
}

第九章:获取当前用户和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"; // 登录接口可能还没有Authentication
}

为什么登录接口会是 anonymous@AuditLogRecord(action = "登录") 标注在登录方法上,但登录成功前 SecurityContext 中还没有 Authentication,所以用户名为 anonymous。解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方案1:从方法参数中提取用户名
@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();
}
// 多级代理时取第一个非unknown的IP
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 {

// 加锁:SETNX + 过期时间
public boolean tryLock(String lockKey, String requestId, int expireTime) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent("lock:" + lockKey, requestId, expireTime, TimeUnit.SECONDS);
return result != null && result;
}

// 释放锁:Lua脚本保证原子性(先比较再删除)
// ★ 必须用Lua脚本,否则get和delete之间可能有其他线程介入
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("用户名已存在");
}
// BCrypt加密密码
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
// 方案1:@Async异步记录
@Async
public void saveLog(AuditLog log) {
auditLogMapper.insert(log);
}

// 方案2:消息队列
@Around("@annotation(auditLogRecord)")
public Object handleAudit(ProceedingJoinPoint joinPoint, AuditLogRecord record) throws Throwable {
// ... 收集日志信息
rabbitTemplate.convertAndSend("audit.log.queue", auditLog); // 发到MQ
// ...
}

// 方案3:批量写入(Buffer + 定时刷盘)
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
// 1. 枚举加一项
public enum RoleEnum {
// ...
TEACHER("TEACHER", "教师");
}

// 2. 写策略实现
@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) { ... }
}

// 3. 数据库插入角色
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 保持干净。一行注解,背后是代理模式 + 环绕通知 + 反射 + 正则脱敏的完整体系。

评论
分享