服务安全之系统鉴权
认证(Authentication)解决“当前主体是谁”,授权(Authorization)判断该主体能否执行某个动作。本文用 Spring Security、Sa-Token 和 Shiro 展示常见实现,并重点说明会话、资源归属和框架版本的边界。
一、核心概念
1.1 认证 vs 授权
| 概念 | 说明 | 问题 |
|---|---|---|
| 认证 | 验证用户身份 | 登录时验证用户名密码 |
| 授权 | 验证访问权限 | 判断用户有没有访问接口的权限(RBAC) |
1.2 通用核心组件
| 组件 | 说明 |
|---|---|
| 认证信息 | 存放用户身份(用户名、密码、角色) |
| 认证管理器 | 校验认证信息 |
| 密码哈希器 | 密码哈希与验证 |
| 权限决策器 | 决定是否能访问资源 |
| 上下文持有者 | 持有当前 Authentication 的上下文 |
1.3 三框架核心定位
| 框架 | 核心机制 | 与 Spring 耦合度 |
|---|---|---|
| Spring Security | 过滤器链(Filter Chain) | 高(强关联) |
| Sa-Token | Token 管理 + 注解鉴权 | 低(独立使用) |
| Shiro | 过滤器链(Filter Chain) | 中(需要集成) |
二、Spring Security 深入原理
2.1 过滤器链原理
Spring Security 通过 SecurityFilterChain 组合安全过滤器。实际过滤器由启用的登录方式、CSRF、Session、资源服务器等配置决定,不应依赖一张固定的全量顺序表。概念流程如下:
HTTP 请求
↓
SecurityFilterChain
├── 加载或建立 SecurityContext
├── 执行当前登录方式对应的认证过滤器
├── 执行 CSRF、响应头、Session 等已启用防护
├── AuthorizationFilter 调用 AuthorizationManager
└── ExceptionTranslationFilter 将认证/授权异常交给入口或拒绝处理器
↓
通过 → 业务代码
不通过 → 403 Forbidden / 302 重定向到登录页
2.2 请求鉴权的完整流程
请求 GET /admin/users
↓
安全过滤器从 SecurityContext 获取当前 Authentication
↓
AuthorizationManager 根据请求规则检查 ADMIN 权限
↓
授权失败,抛出 AccessDeniedException
↓
ExceptionTranslationFilter 捕获
↓
尚未建立有效身份 → AuthenticationEntryPoint 返回 401 或发起登录跳转
已有身份但权限不足 → AccessDeniedHandler 返回 403
实际响应取决于 API、表单登录和 OAuth2 等配置,不能假设所有未登录请求都会返回 302。
2.3 认证流程
UsernamePasswordAuthenticationFilter 处理登录:
登录请求 POST /login,携带 username & password
↓
attemptAuthentication(request, response)
↓
从 request 获取 username 和 password
↓
创建 UsernamePasswordAuthenticationToken(未认证)
↓
交给 AuthenticationManager.authenticate(token)
↓
认证成功:
1. SecurityContextHolder.getContext().setAuthentication(Authentication)
2. 调用 AuthenticationSuccessHandler
认证失败:
1. SecurityContext 清空
2. 调用 AuthenticationFailureHandler
2.4 核心组件详解
AuthenticationManager 认证管理器
// 默认实现是 ProviderManager,内部维护多个 AuthenticationProvider
AuthenticationManager(ProviderManager)
↓
DaoAuthenticationProvider → 查数据库(UserDetailsService)
JwtAuthenticationProvider → JWT 验证
OAuth2AuthenticationProvider → OAuth2 认证
UserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.findByUsername(username);
if (user == null) throw new UsernameNotFoundException("用户不存在");
// 返回 UserDetails 包含用户名、密码、角色列表
return User.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 推荐:带盐,强度自适应
}
2.5 授权流程
基于配置文件的授权:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/login").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
);
return http.build();
}
}
基于注解的方法级授权:
@RestController
public class AdminController {
// 只有 ADMIN 角色才能访问
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/users")
public List<User> getUsers() { ... }
// 表达式:用户名是当前用户本人或者是 ADMIN
@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
@GetMapping("/user/{username}")
public User getUser(@PathVariable String username) { ... }
}
当前授权核心机制:
AuthorizationFilter / 方法安全拦截器
↓
AuthorizationManager 根据 Authentication 与受保护对象作出决定
↓
允许:继续过滤器链或方法调用
拒绝:抛出 AccessDeniedException,并转换为 403 等响应
较早版本常用 AccessDecisionManager 和 voter 描述授权;在新版本中它们属于旧模型。新项目应从 AuthorizationManager、请求级规则和方法级规则理解,迁移旧项目时再对照 voter 机制。@PreAuthorize 还需要显式启用方法安全,不能只添加注解。
2.6 Session 管理
并发控制:
http.sessionManagement(session -> session
.maximumSessions(1) // 同一个用户最多一个会话
.maxSessionsPreventsLogin(false) // false=踢掉前一个用户,true=禁止新登录
);
Redis Session 共享(多节点部署):
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
@Configuration
@EnableRedisHttpSession // Session 存 Redis
public class SessionConfig {}
2.7 获取当前登录用户
// 方式1:通过 SecurityContext
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 方式2:通过 @AuthenticationPrincipal 注解(推荐)
@GetMapping("/me")
public UserDetails me(@AuthenticationPrincipal UserDetails user) {
return user;
}
// 方式3:Controller 方法参数注入
@GetMapping("/me2")
public User me3(Authentication auth) {
return userService.findByUsername(auth.getName());
}
三、Sa-Token 深入原理
轻量级 Java 权限认证框架,核心是登录凭证(Token)管理。开箱即用,API 极简,适用场景:不想引入 Spring Security 的复杂性,又需要一个完整的认证授权方案。
3.1 工作原理
登录流程:
@PostMapping("/login")
public Result<?> doLogin(String username, String password) {
// 1. 业务校验:验证用户名密码
User user = userService.checkPassword(username, password);
if (user == null) return Result.error("用户名或密码错误");
// 2. 登录:创建 Token,存入 Redis
StpUtil.login(user.getId()); // 参数是用户唯一标识
// 默认 Token 名:satoken,值是一串 UUID
// 存入 Redis:satoken:uuid → userId,过期时间 30 天
// 3. 返回 Token 给前端
return Result.ok().put("token", StpUtil.getTokenValue());
}
验证流程:
请求到达
↓
SaTokenFilter(拦截器)读取 satoken
↓
Redis 查询 Token 是否存在且未过期
↓
Token 有效 → 取出 userId → 继续业务代码
↓
Token 无效/过期 → 抛出 NotLoginException → 全局异常处理返回 401
前端携带 Token:
Header: satoken: <token值>
或
Cookie: satoken=<token值>
3.2 StpLogic 核心接口
public interface StpLogic {
void login(Object id); // 登录
void logout(); // 登出
void checkLogin(); // 检查是否已登录
void checkRole(String... roles); // 检查角色
void checkPermission(String code); // 检查权限码
Object getLoginId(); // 获取当前 userId
TokenInfo getTokenInfo(); // 获取 Token 信息
}
3.3 核心功能
登录与验证:
StpUtil.login(userId); // 登录
StpUtil.checkLogin(); // 验证是否已登录(未登录抛异常)
StpUtil.getLoginIdAsLong(); // 获取当前登录用户 ID
StpUtil.logout(); // 登出
角色与权限校验:
StpUtil.checkRole("admin"); // 必须有 admin 角色
StpUtil.checkRole("admin", "user"); // 必须拥有所有指定角色
StpUtil.checkRoleOr("admin", "super_admin"); // 拥有任一角色即可
StpUtil.checkPermission("user:add"); // 必须有 user:add 权限码
StpUtil.checkPermissionOr("user:add", "user:export"); // 拥有任一即可
注解鉴权:
@SaCheckLogin // 必须登录才能访问
@GetMapping("/user/info")
public User getUserInfo() { ... }
@SaCheckRole("admin") // 必须有 admin 角色
@DeleteMapping("/user/{id}")
public void deleteUser(@PathVariable Long id) { ... }
@SaCheckPermission("user:add") // 必须有 user:add 权限码
@PostMapping("/user")
public void addUser(@RequestBody User user) { ... }
踢人下线:
StpUtil.kickout(userId); // 让指定用户的 Token 失效(强制下线)
StpUtil.kickoutByToken(tokenValue); // 让指定 Token 失效
StpUtil.getSession().getLoginCount(); // 查询当前在线用户数
3.4 Token 存储模式
Redis 存储(默认):
sa-token:
token-name: satoken
timeout: 86400 # Token 有效期(秒),默认 30 天
token-style: uuid # Token 生成风格
Key: satoken:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Value: userId
TTL: 30 天
JWT 模式(无状态):
不依赖 Redis,Token 本身包含用户信息,签发后无法主动撤销:
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"loginId": 10086, "loginType": "login", "exp": 1735689600}
Signature: HMAC-SHA256(header + "." + payload, secret)
- JWT 优点:不依赖 Redis,可跨服务验证
- JWT 缺点:Token 一旦签发无法主动撤销,必须等过期
Cookie 模式:
StpUtil.login(userId, "SatokenCookie"); // 指定 Cookie 名写入
StpUtil.getCookie().getValue(); // 获取 Cookie 值
3.5 SSO 单点登录
用户在一个系统登录后,访问另一个系统无需重新登录:
用户登录 app1.com → 获取 Token
↓
访问 app2.com → 自动带上 app1.com 的 Token
↓
app2.com 验证 Token → 发现是跨域 → 向 app1.com 发起校验
↓
app1.com 返回:有效,返回用户信息
SSO 有三种模式:
- OAuth2 模式:标准单点登录,通过 Ticket 票据交换
- Header 模式:前端把 Token 一并带走
- URL 重定向模式:通过 URL 参数传递 Ticket
四、Shiro 深入原理
Apache Shiro 是 Java 领域历史最悠久的权限框架,比 Spring Security 更早出现,核心特点是简单易用、与 Spring 集成灵活。它不依赖 Spring,可以独立使用,也可以无缝集成到 Spring/SpringBoot 项目中。
4.1 核心架构
Shiro 包含三大核心组件:
Subject(主体) → 当前用户(可以是人、进程、爬虫等)
SecurityManager(安全管理器) → 核心,所有安全操作都经过它
Realm(领域) → 连接你的数据源(数据库/LDAP),提供认证和授权数据
工作流程:
Subject.login(token)
↓
SecurityManager.login(subject, token)
↓
获取配置的 Realm 进行认证
↓
Realm 查询 UserStore 返回 AuthenticationInfo
↓
比对密码,认证成功 → 创建 Subject(包含 Principal + Credentials)
4.2 过滤器链原理
Shiro 的请求处理同样基于过滤器链,与 Spring Security 类似:
HTTP 请求
↓
Filter Chain(ShiroFilter)
↓
①OncePerRequestFilter → 确保过滤器只执行一次
②BasicHttpAuthenticationFilter → HTTP Basic 认证
③FormAuthenticationFilter → 表单登录认证
④UsernamePasswordAuthenticationFilter → 用户名密码认证
⑤AnonymousFilter → 匿名访问(不放行)
⑥AuthcBasicFilter → 认证基础过滤
⑦UserFilter → "rememberMe" 用户校验
⑧AccessControlFilter → 权限校验基类
⑨PathMatchingFilter → 路径匹配
↓
通过 → 业务代码
不通过 → 跳转到 loginUrl 或返回 401/403
配置示例(ShiroFilterFactoryBean):
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(
SecurityManager securityManager) {
ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean();
factory.setSecurityManager(securityManager);
factory.setLoginUrl("/login"); // 未登录跳转
factory.setUnauthorizedUrl("/403"); // 无权限跳转
Map<String, Filter> filters = factory.getFilters();
filters.put("authc", new FormAuthenticationFilter());
filters.put("roles", new RolesAuthorizationFilter());
// 拦截规则:key = URL 路径,value = 过滤器链名称
Map<String, String> filterChain = new LinkedHashMap<>();
filterChain.put("/public/**", "anon"); // anon = 匿名访问
filterChain.put("/user/**", "authc"); // authc = 需认证
filterChain.put("/admin/**", "roles[admin]"); // 需 admin 角色
filterChain.put("/**", "anon"); // 默认
factory.setFilterChainDefinitionMap(filterChain);
return factory;
}
4.3 认证流程
UsernamePasswordToken 是认证凭证:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true); // 记住我
subject.login(token);
认证流程:
subject.login(token)
↓
SecurityManager.getSubject().login(token)
↓
DelegatingSubject.login(token)
↓
SecurityManager.login(subject, token)
↓
获取配置的 Realms(可能有多个)
↓
遍历 Realms 执行 doAuthenticate(AuthenticationToken)
↓
MyRealm.doGetAuthenticationInfo(token)
↓
从数据库查询用户信息,返回 AuthenticationInfo
↓
CredentialsMatcher 比对密码(MD5 / BCrypt / 自定义)
↓
认证成功:Subject 绑定 AuthenticationInfo
↓
认证失败:抛出 AuthenticationException
自定义 Realm 示例:
public class MyRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// 从数据库查询用户
User user = userMapper.findByUsername(username);
if (user == null) {
throw new UnknownAccountException("用户不存在");
}
// 返回 AuthenticationInfo(包含密码,会由 CredentialsMatcher 比对)
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), // 盐值
getName() // Realm 名称
);
}
// 授权(获取角色和权限)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
User user = userMapper.findByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(user.getRoles()); // 角色集合
info.setStringPermissions(user.getPermissions()); // 权限码集合
return info;
}
}
4.4 授权流程
权限校验的三种方式:
// 1. 编程方式
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {
// 有 admin 角色
}
if (subject.isPermitted("user:delete")) {
// 有 user:delete 权限
}
// 2. 注解方式(需开启 AOP)
@RequiresRoles("admin")
@GetMapping("/admin/users")
public List<User> getUsers() { ... }
@RequiresPermissions("user:delete")
@DeleteMapping("/user/{id}")
public void deleteUser(@PathVariable Long id) { ... }
@RequiresAuthentication // 需认证
@RequiresGuest // 匿名
@RequiresUser // 已记住或已认证
// 3. JSP/Thymeleaf 标签
<shiro:hasRole name="admin">
<a href="/admin">管理后台</a>
</shiro:hasRole>
角色继承:
// 配置角色继承(admin 继承 user 的权限)
HashMap<String, String> roleHierarchy = new HashMap<>();
roleHierarchy.put("admin", "user guest"); // admin 拥有 user 和 guest 的所有权限
4.5 Session 管理
Shiro 有独立的 Session 管理,不依赖 Servlet 容器:
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
// 常见操作
session.setAttribute("user", currentUser);
session.setAttribute("key", value);
session.setTimeout(30 * 60 * 1000); // 30 分钟超时
Object value = session.getAttribute("key");
session.removeAttribute("key");
Spring Boot 集成 Redis Session:
// 依赖 shiro-redis(需自行实现 SessionDAO)
@Bean
public SessionsSecurityManager securityManager(
Realm realm,
SessionManager sessionManager) {
DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
sm.setRealm(realm);
sm.setSessionManager(sessionManager);
return sm;
}
4.6 与 Spring Security 的核心区别
| 维度 | Shiro | Spring Security |
|---|---|---|
| 架构理念 | 简单灵活,专注认证授权 | 功能全面,与 Spring 生态深度绑定 |
| 配置复杂度 | 相对简单 | 配置项极多,学习曲线陡峭 |
| Session 管理 | 独立 Session 管理(可脱离容器) | 依赖 Servlet Session |
| OAuth2 / SAML | 不支持(需扩展) | 原生支持 |
| 方法级安全 | 注解 + AOP | @PreAuthorize 等更强大 |
| 密码加密 | 支持 MD5/SHA/BCrypt | BCryptPasswordEncoder |
| 适用场景 | 非 Spring 项目、中小型系统 | Spring 全家桶、企业级复杂场景 |
完整集成示例:
// 1. 定义 Realm
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
User user = userMapper.findByUsername(upToken.getUsername());
if (user == null) throw new UnknownAccountException("用户不存在");
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
getName()
);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
User user = userMapper.findByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(user.getRoles());
info.setStringPermissions(user.getPermissions());
return info;
}
}
// 2. 配置 SecurityManager
@Bean
public SecurityManager securityManager(Realm realm, SessionManager sessionManager) {
DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
sm.setRealm(realm);
sm.setSessionManager(sessionManager);
sm.setRememberMeManager(new CookieRememberMeManager());
return sm;
}
// 3. 配置 ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean();
factory.setSecurityManager(securityManager);
factory.setLoginUrl("/login");
factory.setUnauthorizedUrl("/403");
Map<String, Filter> filters = new HashMap<>();
filters.put("authc", new FormAuthenticationFilter());
filters.put("roles", new RolesAuthorizationFilter());
factory.setFilters(filters);
Map<String, String> chain = new LinkedHashMap<>();
chain.put("/public/**", "anon");
chain.put("/login", "anon");
chain.put("/user/**", "authc,roles[user]");
chain.put("/admin/**", "authc,roles[admin]");
chain.put("/**", "anon");
factory.setFilterChainDefinitionMap(chain);
return factory;
}
// 4. 开启 Shiro 注解(需配合 AOP)
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
开启注解支持(在 Spring Boot 主类或配置类上添加):
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxy = new DefaultAdvisorAutoProxyCreator();
proxy.setProxyTargetClass(true);
return proxy;
}
常用注解说明:
| 注解 | 说明 |
|---|---|
@RequiresAuthentication |
需要认证才能访问 |
@RequiresGuest |
匿名访问 |
@RequiresUser |
已记住或已认证的用户 |
@RequiresRoles("admin") |
需要指定角色 |
@RequiresPermissions("user:delete") |
需要指定权限码 |
五、综合对比
5.1 功能维度对比
三者都能完成基础认证和角色/权限判断,真正拉开差异的是协议生态、默认安全策略、扩展方式和团队维护能力。不要用单个勾叉表示“支持 OAuth2、SSO 或 CSRF”:协议中的客户端、授权服务器、资源服务器是不同角色,框架支持范围也会随版本和扩展包变化。
| 评估维度 | 要核对的问题 |
|---|---|
| 身份协议 | 只需站内登录,还是需要 OAuth 2.0/OIDC、SAML、企业身份源? |
| 会话模型 | 服务端 Session、Opaque Token、JWT 分别如何撤销、轮换和过期? |
| 授权粒度 | URL、方法、资源归属和租户隔离分别在哪一层执行? |
| 浏览器防护 | Cookie、CORS、CSRF、CSP 和登录跳转由谁配置? |
| 审计与运维 | 登录失败、权限拒绝、密钥轮换和异常会话如何观察与处置? |
| 生态与升级 | 是否匹配当前 Spring Boot BOM,依赖是否仍维护,安全公告由谁跟进? |
5.2 学习曲线对比
学习成本取决于要实现的协议与安全边界,星级或“几分钟上手”无法复现。比较时应让候选框架实现同一组最小用例:登录、退出、过期、撤销、横向越权防护、并发会话和审计日志,再记录代码量、默认行为和失败结果。
5.3 性能与轻量对比
依赖大小、启动时间、内存和 Redis 次数会随模块、配置、JDK、会话模式及业务过滤器变化。若这些指标影响部署,应在相同 JDK、相同依赖范围和相同认证流程下测量冷启动、稳态内存、P95 延迟及外部存储请求数,不使用脱离版本与场景的固定数字。
5.4 适用场景对比
| 已知条件 | 优先调查方向 | 仍需验证 |
|---|---|---|
| Spring 应用且依赖 OAuth 2.0/OIDC 生态 | Spring Security | 所需角色、授权服务器选型和迁移成本 |
| 非 Spring 应用或已有 Shiro 资产 | Shiro 或现有方案升级 | 当前维护状态、漏洞修复和现代协议扩展 |
| 主要是站内 Token、会话和权限码 | Sa-Token 等轻量方案 | 浏览器防护、协议互通、团队运维能力 |
| 多租户或行级数据权限 | 框架认证 + 业务数据约束 | 资源归属检查不能只靠角色名 |
5.5 选型决策树
先列身份来源、客户端类型和合规要求
→ 确认需要实现的协议角色
→ 设计 Session / Token 的过期、撤销与轮换
→ 设计 URL、方法、资源归属和租户四层授权
→ 用同一组失败用例验证候选方案
→ 再比较接入、升级和运维成本
5.6 各框架推荐版本
不要在长期笔记中写固定“推荐版本”。Spring 项目优先跟随所用 Spring Boot 版本管理的依赖组合;其他框架在实施时查看官方稳定版、运行环境要求、迁移说明和安全公告,并把实际版本写进项目锁文件或构建清单。
六、常见问题
6.1 Token 过期后如何处理
// Spring Security
@ExceptionHandler(AccessDeniedException.class)
public Result<?> handleAccessDenied(AccessDeniedException e) {
return Result.error(403, "无权限访问");
}
// Sa-Token
@ExceptionHandler(NotLoginException.class)
public Result<?> handleNotLogin(NotLoginException e) {
return Result.error(401, "未登录或登录已过期");
}
6.2 前后端分离如何传递 Token
// 前端:放在 Header 中(推荐)
axios.defaults.headers.common['satoken'] = localStorage.getItem('satoken');
// 或放在 Cookie 中(自动携带)
document.cookie = "satoken=" + token;
// Spring Security JWT 方式
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
6.3 @PreAuthorize 不生效(Spring Security)
检查是否开启了全局方法安全注解:
@Configuration
@EnableMethodSecurity // 必须加!否则 @PreAuthorize 不生效
public class SecurityConfig {}
6.4 Sa-Token 无法替代 Spring Security 的场景
- 需要细粒度行级数据权限控制
- 需要 OAuth2 资源服务器认证
- 需要与 Spring Security OAuth2 配合的第三方登录(微信、GitHub)
- 复杂的企业级安全管理(多租户、敏感操作审计)
6.5 Shiro 注解不生效
检查是否正确配置了 AOP 支持:
// 1. 确保添加了这两个 Bean
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxy = new DefaultAdvisorAutoProxyCreator();
proxy.setProxyTargetClass(true);
return proxy;
}
// 2. 确保添加了 Advisor
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
return new AuthorizationAttributeSourceAdvisor(securityManager);
}
6.6 Session 与 Token 选择建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| PC 端 Web(需会话保持) | Session + Cookie | 传统方案,Cookie 自动携带 |
| 移动端 APP | Token(Header) | 无 Cookie,跨平台 |
| 微服务架构 | Token(Redis 存储) | 无状态,易扩展 |
| 前后端分离 | Token(Header) | 跨域,JWT 可自包含 |
| 需要 SSO | Token | 跨域携带方便 |
七、要点速记
7.1 核心口诀
- 认证 = 验证"你是谁"(登录)
- 授权 = 验证"你能做什么"(权限)
- Session = 服务端存储会话状态
- Token = 客户端携带的凭证;可以是自包含 JWT,也可以是服务端可查询的引用令牌
7.2 三框架选型口诀
先定身份协议 → 再选框架
先定撤销与轮换 → 再选 Token 形态
URL 规则管入口 → 方法规则管功能 → 数据条件管资源归属
7.3 密码安全
- 使用专门的自适应密码哈希算法,如 Argon2、scrypt、bcrypt 或 PBKDF2,并按环境校准成本参数。
- 不要直接用 MD5、SHA-1 或单次 SHA-256 保存密码;它们不是密码哈希方案。
- bcrypt 等格式通常把算法参数和盐编码在最终哈希串中,无需另建“盐字段”;另行管理的 pepper 应放在密钥系统中,不与数据库一起保存。
7.4 权限校验三时机
请求入口(Filter) → URL 级权限
↓
方法调用(@PreAuthorize) → 功能级权限
↓
数据访问(WHERE 条件) → 行级数据权限(需自研)
7.5 安全最佳实践
- 密码加密:BCrypt,不自研加密算法
- Token 生命周期:明确过期、轮换、撤销与密钥泄露后的处置;JWT 可通过短有效期、会话版本或撤销列表限制风险
- 敏感接口:加验证码、防爬、限流
- 登录日志:记录 IP、时间、设备
- 权限最小化:先拒绝再授权
- 防止 CSRF:Spring Security 默认开启,Sa-Token 需手动处理
八、验证清单与参考
- 匿名用户访问受保护接口得到 401,已登录但无权限得到 403。
- 普通用户不能通过修改路径参数读取或修改他人的资源。
- 方法级授权已显式启用,并有允许与拒绝两类测试。
- Token 或 Session 过期、退出、密码修改和密钥轮换后的行为符合约定。
- Cookie 场景验证 CSRF,跨域场景验证 CORS;两者不是同一个问题。
- 日志不记录密码、完整 Token、验证码或敏感个人数据。
参考: