服务安全之系统鉴权
认证(Authentication)和授权(Authorization)是系统安全的两个核心环节。认证解决"你是谁"的问题,授权解决"你能做什么"的问题。本文档整合 Sa-Token、Spring Security、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 的请求处理流程:
HTTP 请求
↓
Filter Chain(过滤器链)
↓
① SecurityContextPersistenceFilter → 从 Session 加载 SecurityContext
② HeaderWriterFilter → 添加安全响应头
③ CsrfFilter → CSRF 防护
④ LogoutFilter → 处理登出
⑤ UsernamePasswordAuthenticationFilter → 用户名密码认证
⑥ BasicAuthenticationFilter → HTTP Basic 认证
⑦ AnonymousAuthenticationFilter → 未认证用户设为匿名
⑧ SessionManagementFilter → Session 管理
⑨ ExceptionTranslationFilter → 包装异常为 HTTP 响应
⑩ FilterSecurityInterceptor → 最后一关:权限校验
↓
通过 → 业务代码
不通过 → 403 Forbidden / 302 重定向到登录页
2.2 请求鉴权的完整流程
请求 GET /admin/users
↓
FilterSecurityInterceptor 检查:需要 ADMIN 角色
↓
SecurityContext 中没有 Authentication
↓
AnonymousAuthenticationFilter 塞入匿名 Authentication
↓
AccessDecisionManager 投票:匿名用户没有 ADMIN 角色
↓
投票结果:拒绝访问
↓
ExceptionTranslationFilter 捕获
↓
判断是否已认证:
未认证 → 重定向到 /login 页面
已认证但权限不足 → 403 Forbidden
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
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) { ... }
}
权限投票机制:
// AffirmativeBased:多数票胜出,有票同意就放行
public class AffirmativeBased extends AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
for (AccessDecisionVoter voter : decisionVoters) {
int result = voter.vote(authentication, object, attributes);
if (result == ACCESS_GRANTED) return;
}
throw new AccessDeniedException("无权限访问");
}
}
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 功能维度对比
| 功能 | Sa-Token | Spring Security | Shiro |
|---|---|---|---|
| 登录认证 | ✅ | ✅ | ✅ |
| 角色校验 | ✅ | ✅ | ✅ |
| 权限码校验 | ✅ | ✅ | ✅ |
| 注解鉴权 | ✅ | ✅ | ✅ |
| 踢人下线 | ✅ | ✅ | ✅ |
| Token / Session | ✅ | ✅ | ✅ |
| Redis 共享 | ✅ | ✅ | ✅ |
| SSO 单点登录 | ✅(内置) | ❌(需 Spring Security OAuth2) | ❌(需扩展) |
| OAuth2 第三方登录 | ❌ | ✅ | ❌ |
| CSRF 防护 | ❌ | ✅ | ❌ |
| 细粒度权限(数据权限) | ❌ | ❌ | ❌(需插件) |
| 与 Spring 耦合度 | 低(独立使用) | 高(强关联) | 中 |
5.2 学习曲线对比
| 维度 | Sa-Token | Spring Security | Shiro |
|---|---|---|---|
| 文档友好度 | ⭐⭐⭐⭐⭐(文档简洁,示例丰富) | ⭐⭐⭐(官方文档晦涩) | ⭐⭐⭐⭐ |
| 上手难度 | ⭐⭐(5 分钟跑通) | ⭐⭐⭐⭐⭐(学习曲线陡峭) | ⭐⭐⭐ |
| 配置复杂度 | ⭐⭐(几行配置) | ⭐⭐⭐⭐⭐(配置项极多) | ⭐⭐⭐ |
5.3 性能与轻量对比
| 维度 | Sa-Token | Spring Security | Shiro |
|---|---|---|---|
| 依赖包大小 | ~200KB(极轻量) | ~5MB(庞大) | ~1MB |
| 启动时间影响 | 几乎无 | 显著 | 较小 |
| 内存占用 | 低 | 高 | 中 |
| Redis 操作 | 每次验证 1 次 | 可配置 | 可配置 |
5.4 适用场景对比
| 场景 | 推荐 | 原因 |
|---|---|---|
| 快速中小型项目 | Sa-Token | 配置少,上手快 |
| Spring 全家桶 + 复杂权限 | Spring Security | 生态完整,集成度高 |
| 非 Spring 项目 | Shiro | 不强依赖 Spring |
| 需要 SSO | Sa-Token(简单 SSO) | 内置 SSO,配置简单 |
| 需要 OAuth2 第三方登录 | Spring Security | 原生支持 OAuth2 Client |
| 需要微服务认证 | Sa-Token(JWT)或 Spring Security | 都支持 JWT |
| 需要细粒度数据权限 | 目前都不满足 | 需自研或用 MyBatis-Plus 插件 |
5.5 选型决策树
需要 OAuth2 / 第三方登录(微信、GitHub 等)?
└── 是 → Spring Security
需要极简配置 / 快速上线 / 小型项目?
└── 是 → Sa-Token
非 Spring 项目(Spring Boot / Spring Cloud 以外)?
└── 是 → Shiro
需要 SSO 单点登录(跨系统免登录)?
└── 是 → Sa-Token(简单场景)/ Spring Security OAuth2(复杂企业场景)
Spring 全家桶 + 复杂安全场景(多租户、审计、SAML)?
└── 是 → Spring Security(即使配置复杂,但收益高)
一般业务系统,认证授权不复杂?
└── Sa-Token(性价比最高)
需要微服务网关鉴权 / 内部服务互调?
└── 是 → Sa-Token(JWT)或 Spring Security(OAuth2 资源服务器)
需要移动端 APP 鉴权?
└── 是 → Sa-Token(轻量)或 Spring Security(OAuth2)
团队对安全框架不熟悉,想要快速落地?
└── 是 → Sa-Token(5 分钟跑通)
已在使用 Spring Cloud 全家桶?
└── 是 → Spring Security(生态一致)
5.6 各框架推荐版本
| 框架 | 推荐版本 | 说明 |
|---|---|---|
| Spring Security | 6.x(Spring Boot 3.x) | 最新响应式支持 |
| Sa-Token | 1.37.x | 稳定,文档完善 |
| Shiro | 1.12.x | 最后一个主流版本 |
六、常见问题
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 = 客户端持有凭证(无状态)
7.2 三框架选型口诀
小项目快上线 → Sa-Token
Spring 全家桶 → Spring Security
不用 Spring → Shiro
要 OAuth2 → Spring Security
要 SSO → Sa-Token
7.3 密码安全
- 生产环境必须用 BCrypt(不可逆、带盐)
- 禁止使用 MD5/SHA-1/SHA-256(可被彩虹表破解)
- 盐值单独存储,不要混在密码里
7.4 权限校验三时机
请求入口(Filter) → URL 级权限
↓
方法调用(@PreAuthorize) → 功能级权限
↓
数据访问(WHERE 条件) → 行级数据权限(需自研)
7.5 安全最佳实践
- 密码加密:BCrypt,不自研加密算法
- Token 存储:Redis 可主动销毁,JWT 无法撤销
- 敏感接口:加验证码、防爬、限流
- 登录日志:记录 IP、时间、设备
- 权限最小化:先拒绝再授权
- 防止 CSRF:Spring Security 默认开启,Sa-Token 需手动处理