CC 咖啡猫的工作空间 Coding Space

服务安全之系统鉴权

认证(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 安全最佳实践

  1. 密码加密:BCrypt,不自研加密算法
  2. Token 存储:Redis 可主动销毁,JWT 无法撤销
  3. 敏感接口:加验证码、防爬、限流
  4. 登录日志:记录 IP、时间、设备
  5. 权限最小化:先拒绝再授权
  6. 防止 CSRF:Spring Security 默认开启,Sa-Token 需手动处理