leniu-security-guard
SKILL.md
leniu-security-guard
认证注解
所有注解包路径:net.xnzn.framework.secure.filter.annotation.*
| 注解 | 用途 |
|---|---|
@RequiresAuthentication |
需要登录认证(最常用,类/方法级) |
@RequiresGuest |
允许游客访问(公开接口) |
@RequiresPermissions |
需要指定权限码 |
@RequiresRoles |
需要指定角色 |
@RequiresUser |
需要用户登录 |
@RequiresHeader |
需要指定请求头 |
Controller 认证示例
import net.xnzn.framework.secure.filter.annotation.RequiresAuthentication;
import net.xnzn.framework.secure.filter.annotation.RequiresGuest;
import net.xnzn.framework.secure.filter.annotation.RequiresPermissions;
import net.xnzn.framework.secure.filter.annotation.RequiresRoles;
// 类级别:整个 Controller 需要登录
@RestController
@RequestMapping("/api/v2/web/xxx")
@RequiresAuthentication
public class XxxWebController { }
// 方法级别:允许游客访问
@GetMapping("/public/info")
@RequiresGuest
public LeResponse<String> getPublicInfo() { }
// 需要特定权限
@GetMapping("/admin/users")
@RequiresPermissions("system:user:list")
public LeResponse<List<User>> listUsers() { }
// AND 逻辑(默认):需要所有权限
@RequiresPermissions(value = {"system:user:add", "system:user:edit"}, logical = Logical.AND)
// OR 逻辑:需要任一权限
@RequiresPermissions(value = {"system:user:add", "system:user:edit"}, logical = Logical.OR)
// 需要指定角色
@RequiresRoles("admin")
// 需要所有角色
@RequiresRoles(value = {"admin", "manager"}, logical = Logical.AND)
注意:
@RequiresPermissions不指定value时,自动使用@RequestMapping路径作为权限码。
TokenManager API
import net.xnzn.framework.secure.token.TokenManager;
用户信息
// 登录状态
TokenManager.isLogin();
// 用户 ID
Long userId = TokenManager.getSubjectId().orElse(null);
// 用户名
String userName = TokenManager.getSubjectName().orElse(null);
// 附加数据
Map<String, String> userData = TokenManager.getSubjectData();
String orgId = userData.get("orgId");
权限/角色校验
// 单个权限
TokenManager.hasPermission("system:user:add");
// 多个权限(AND)
TokenManager.hasPermission("system:user:add", "system:user:edit");
// 任一权限(OR)
TokenManager.hasAnyPermission("system:user:add", "system:user:edit");
// 角色
TokenManager.hasRole("admin");
TokenManager.hasAnyRole("admin", "manager");
附加数据管理
// 添加
TokenManager.attachData("orgId", "12345");
// 批量添加
TokenManager.attachData(Map.of("orgId", "12345", "deptId", "67890"));
// 获取
String orgId = TokenManager.getSubjectData().get("orgId");
// 移除
TokenManager.removeData("orgId", "deptId");
登出与缓存
// 登出
TokenManager.logout();
// 强制下线
TokenManager.revokeAuthenticate();
// 撤销旧 Token(保留最近 N 个)
TokenManager.revokeAuthenticate(userId, 3);
// 清除权限/角色缓存
TokenManager.clearPermission(userId);
TokenManager.clearRole(userId);
TokenManager.clearRoleAndPermission(userId);
TokenManager.clearAllRoleAndPermission();
WebContext
import net.xnzn.framework.secure.WebContext;
HttpServletRequest request = WebContext.get().getRequest().orElse(null);
HttpServletResponse response = WebContext.get().getResponse().orElse(null);
Optional<AccessToken> token = WebContext.get().getAccessToken();
// 属性存取
WebContext.get().setAttribute("key", "value");
Object value = WebContext.get().getAttribute("key");
WebContext.reset();
数据权限校验(防越权)
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
Order order = orderMapper.selectById(id);
Assert.notNull(order, () -> new LeException("订单不存在"));
// 校验数据归属
Long currentUserId = TokenManager.getSubjectId()
.orElseThrow(() -> new LeException("用户未登录"));
if (!order.getUserId().equals(currentUserId)) {
throw new LeException("无权操作该订单");
}
orderMapper.deleteById(id);
}
最小权限原则
@RequiresAuthentication
@RequiresPermissions("order:view")
public PageVO<OrderVO> pageList(OrderPageParam param) {
Long userId = TokenManager.getSubjectId()
.orElseThrow(() -> new LeException("用户未登录"));
param.setUserId(userId); // 限制只查自己的数据
return orderService.pageList(param);
}
SQL 注入防护
MyBatis XML 必须用 #{}
<!-- 正确:参数化查询 -->
<select id="selectByName" resultType="User">
SELECT id, name, mobile FROM user WHERE name = #{name}
</select>
<!-- 错误:${} 有注入风险 -->
<select id="selectByName" resultType="User">
SELECT * FROM user WHERE name = '${name}'
</select>
禁止 SELECT *
<!-- 错误 -->
SELECT * FROM user WHERE status = #{status}
<!-- 正确:明确指定字段 -->
SELECT id, name, mobile, status FROM user WHERE status = #{status}
VO 敏感字段处理
@Data
public class UserVO {
@JsonIgnore
private String password; // 密码绝不返回
@JsonSerialize(using = MobileSerializer.class)
private String mobile; // 手机号脱敏
@JsonSerialize(using = IdCardSerializer.class)
private String idCard; // 身份证脱敏
}
日志脱敏
// 错误:记录敏感信息
log.info("用户登录, username:{}, password:{}", username, password);
// 正确:不记录密码
log.info("用户登录, username:{}", username);
输入验证
@Data
public class UserParam {
@NotBlank(message = "用户名不能为空")
@Pattern(regexp = "^[a-zA-Z0-9_]{4,20}$", message = "用户名格式不正确")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20之间")
private String password;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
@Email(message = "邮箱格式不正确")
private String email;
}
限流防护
// Redis 计数器限流
public void checkRateLimit(Long userId, String api, int limit, int seconds) {
String key = String.format("rate:%d:%s", userId, api);
Integer count = RedisUtil.incr(key, (long) seconds);
if (count > limit) {
throw new LeException("请求过于频繁,请稍后重试");
}
}
注意事项
- 权限和角色数据自动缓存到 Redis,通过
clearPermission/clearRole手动清除 TokenManager方法需在已登录上下文中使用@RequiresGuest用于公开接口,不需要登录- MyBatis XML 中必须使用
#{}而非${} - VO 中密码字段必须
@JsonIgnore,手机号等用序列化脱敏 - 限流 key 格式:
rate:{userId}:{api}
Weekly Installs
3
Repository
xu-cell/ai-engi…ing-initGitHub Stars
8
First Seen
7 days ago
Security Audits
Installed on
github-copilot3
codex3
kimi-cli3
gemini-cli3
cursor3
amp3