常见安全架构中 Shiro 的认证确认机制解析

1. 引言

在 Java 生态中,Apache Shiro 是一款轻量级的安全框架,广泛应用于身份认证、授权、会话管理和加密等场景。很多开发者会好奇:Shiro 是如何确认一个用户身份的?本文从常见架构视角,梳理 Shiro 的认证确认流程与核心机制。

2. Shiro 认证确认的核心流程

Shiro 的认证确认本质上是一个「收集凭证、校验身份、建立会话」的过程,核心步骤如下:

  1. 收集身份与凭证 :用户提交用户名和密码,Shiro 将其封装为 UsernamePasswordToken
  2. 提交认证请求 :调用 Subject.login(token),将令牌交给 SecurityManager。
  3. 委托给 Authenticator :SecurityManager 将认证任务委托给内部的 Authenticator
  4. 调用 Realm 校验 :Authenticator 根据配置选择一个或多个 Realm,调用其 getAuthenticationInfo 方法。
  5. 返回认证结果 :Realm 从数据源(数据库、LDAP 等)查询用户信息,比对凭证后返回 AuthenticationInfo;校验失败则抛出对应异常。
  6. 建立会话 :认证成功后,Shiro 将身份信息写入 Subject 的 Session,后续请求即可通过 Subject 快速确认登录状态。

下图以时序图形式展示 Subject、SecurityManager、Authenticator、Realm 与 CredentialsMatcher 之间的认证交互顺序:

sequenceDiagram participant U as 用户 participant S as Subject participant SM as SecurityManager participant A as Authenticator participant R as Realm participant CM as CredentialsMatcher U->>S: 提交用户名密码 S->>SM: login(token) SM->>A: authenticate(token) A->>R: getAuthenticationInfo(token) R->>R: 查询用户数据源 R->>CM: 比对凭证 CM-->>R: 校验结果 R-->>A: 返回 AuthenticationInfo A-->>SM: 返回认证结果 SM-->>S: 建立登录状态 S-->>U: 认证成功

图中关键步骤说明:用户先通过 Subject.login(token) 提交凭证,SecurityManager 将任务委托给 Authenticator;Authenticator 调用 Realm 的 getAuthenticationInfo 查询用户数据,随后由 CredentialsMatcher 完成密码比对;校验通过后,认证结果沿调用链逐层返回,最终由 Subject 固化登录状态,完成整个认证确认闭环。

3. Realm:认证确认的关键落点

Realm 是 Shiro 连接应用与数据源的桥梁,也是「确认身份」真正发生的地方。常见架构中,开发者通常自定义一个 Realm,继承 AuthorizingRealm 并重写两个方法:

java 复制代码
public class CustomRealm extends AuthorizingRealm {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        String username = (String) token.getPrincipal();
        // 从数据库查询用户
        User user = userMapper.findByUsername(username);
        if (user == null) {
            return null; // 用户名不存在
        }
        // 返回密码与盐值,Shiro 会与 token 中的密码比对
        return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 授权逻辑,返回角色与权限
    }
}

这里的关键在于:Realm 只负责「取出正确的用户凭证」,真正的密码比对由 Shiro 的 CredentialsMatcher 完成,默认使用 SimpleCredentialsMatcher 做明文比对,生产环境通常换成 HashedCredentialsMatcher 支持 MD5、SHA-256 加盐校验。

在 Spring Boot 环境中,可以通过一个 @Configuration 类将自定义的 CustomRealm 注册到 SecurityManager,并配置 HashedCredentialsMatcher 使用 MD5 加盐校验密码。完整实现如下:

java 复制代码
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {

    // 1. 配置 HashedCredentialsMatcher,使用 MD5 加盐校验
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");
        matcher.setHashIterations(1); // 加密迭代次数,与注册时保持一致
        return matcher;
    }

    // 2. 将自定义 Realm 注册为 Bean,并绑定凭证匹配器
    @Bean
    public CustomRealm customRealm() {
        CustomRealm realm = new CustomRealm();
        realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }

    // 3. 将 Realm 注册到 SecurityManager
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm());
        return securityManager;
    }

    // 4. 配置 Shiro 过滤器,拦截所有请求
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        factoryBean.setLoginUrl("/login");
        factoryBean.setUnauthorizedUrl("/403");
        return factoryBean;
    }

    // 5. 开启 Shiro 注解支持(如 @RequiresPermissions)
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

这段配置的核心在于:HashedCredentialsMatcher 指定了 MD5 算法与迭代次数,CustomRealm 通过 setCredentialsMatcher 绑定该匹配器,最终由 DefaultWebSecurityManager 统一接管认证与授权流程。这样,Realm 中返回的密码与盐值就会按照 MD5 加盐规则与用户提交的凭证进行比对。

4. 常见架构中的确认方式对比

架构场景 Shiro 确认方式 说明
单体应用 Session 内确认 登录成功后身份存入 Session,每次请求通过 Subject 判断是否已认证。
前后端分离 Token 确认 登录后签发自定义 Token,前端每次请求携带,Shiro 通过过滤器解析并确认身份。
分布式 / 微服务 Redis 共享 Session 或 JWT Shiro 的 SessionDAO 可对接 Redis,实现多节点共享登录态;也可结合 JWT 做无状态认证。
第三方登录(OAuth2) 自定义 Realm 适配 在 Realm 中调用第三方接口换取用户信息,再封装为 AuthenticationInfo 完成确认。

上面的表格比较抽象,下面用几个生活化的例子,帮大家把每种「确认方式」理解得更透彻。

4.0.1 单体应用:Session 内确认

打个比方:就像去一家老式澡堂,进门时前台给你一把带号码的储物柜钥匙。你每次去拿东西,只要出示这把钥匙,前台就知道你是哪个柜子的主人,不用再重新登记。

对应到 Shiro:用户登录成功后,Shiro 把身份信息写进 Session(相当于那把钥匙)。之后每次请求,Shiro 只要看到 Session 里已经有登录标记,就认定「这个人已经认证过了」,直接放行,不用再输一遍密码。

适用场景:只有一个应用服务,所有请求都打到同一台机器上,Session 存在本机内存里就够了,简单直接。

4.0.2 前后端分离:Token 确认

打个比方:就像去游乐场买了一张手环门票,戴在手上。每个项目入口的检票员不查你的身份证,只看你手上的手环是否有效,有效就放你进去。

对应到 Shiro:登录成功后,服务端签发一个 Token(相当于手环)给前端。前端每次请求都把这个 Token 带上,Shiro 的过滤器解析 Token、验证签名,确认有效就认定身份,不再依赖服务端保存的 Session。

适用场景:前端是独立的网页或 App,和后端通过接口通信,后端不关心「上一次请求是谁」,只看这次请求带的 Token 是否可信。

4.0.3 分布式 / 微服务:Redis 共享 Session 或 JWT

打个比方(Redis 共享 Session):就像连锁酒店集团,你在北京店办了会员卡,入住信息统一存在集团总部的系统里。你到上海店入住时,前台一查总部系统,就知道你是会员,不用重新办卡。

对应到 Shiro:多个应用节点把 Session 统一存到 Redis 里。用户在 A 节点登录后,请求被转发到 B 节点,B 节点去 Redis 一查,发现 Session 还在,就认定已登录,登录态不会因为换了节点而丢失。

打个比方(JWT):就像一张盖了公章、防伪的纸质通行证。通行证本身印着你的身份信息,任何检查点只要验证公章(签名)是真的,就认这张证,不需要打电话回总部核实。

对应到 Shiro:服务端不保存会话,登录后签发一个 JWT 给客户端。客户端每次请求带上 JWT,任何节点只要用密钥验证签名通过,就认定身份,无需访问共享存储。

适用场景:系统拆成多个服务、部署在多台机器上。如果业务需要服务端能随时踢人下线,选 Redis 共享 Session;如果追求高并发、无状态、易扩展,选 JWT。

4.0.4 第三方登录(OAuth2):自定义 Realm 适配

打个比方:就像你用微信扫码登录某个网站。网站自己不保存你的账号密码,而是让微信确认「这个人是谁」,微信确认后告诉网站「这是张三」,网站就放你进来。

对应到 Shiro :在自定义 Realm 里调用第三方接口(如微信、GitHub),换取用户信息,再封装成 Shiro 能识别的 AuthenticationInfo。Shiro 不关心用户密码是什么,只关心第三方确认的结果。

适用场景:应用本身不维护账号体系,直接信任微信、GitHub 等第三方平台的登录结果。

4.1 分布式场景下的 Shiro 认证实践

在分布式 / 微服务架构中,Shiro 的 Session 默认存储在应用内存中,多节点部署时会出现登录态不一致的问题。解决思路之一是将 Session 持久化到 Redis,通过自定义 SessionDAO 实现多节点共享登录态。下面给出一个基于 Spring Boot 的配置示例:

java 复制代码
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class ShiroRedisSessionConfig {

    // 1. 配置 RedisTemplate,指定序列化方式
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // Key 使用 String 序列化,Value 使用 JSON 序列化
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    // 2. 自定义 SessionDAO,将 Session 写入 Redis
    @Bean
    public SessionDAO sessionDAO(RedisTemplate<String, Object> redisTemplate) {
        return new RedisSessionDAO(redisTemplate);
    }

    // 3. 配置 SessionManager,绑定 SessionDAO 与 Cookie
    @Bean
    public DefaultWebSessionManager sessionManager(SessionDAO sessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(sessionDAO);
        // 设置 Session 全局过期时间(毫秒),默认 30 分钟
        sessionManager.setGlobalSessionTimeout(1800000);
        // 配置 Cookie 名称与路径
        SimpleCookie cookie = new SimpleCookie("SHIRO_SESSION_ID");
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        sessionManager.setSessionIdCookie(cookie);
        return sessionManager;
    }
}

上述配置中,RedisSessionDAO 需要自行实现,核心逻辑是重写 EnterpriseCacheSessionDAOdoCreatedoReaddoUpdatedoDelete 四个方法,将 Session 对象序列化后写入 Redis。序列化方式建议使用 JSON(如 GenericJackson2JsonRedisSerializer),便于跨语言、跨节点反序列化;如果对性能要求更高,也可以使用 JDK 原生序列化,但可读性和兼容性较差。

与 Redis 共享 Session 相比,JWT 方案属于无状态认证:服务端不保存会话,登录后签发 Token,客户端每次请求携带,服务端通过签名校验身份。两者的取舍如下:

  • Redis 共享 Session:适合需要服务端主动控制会话生命周期(如强制下线、踢人)的场景,实现直观、与单体 Session 语义一致,但需要维护 Redis 的可用性与序列化兼容性。
  • JWT 无状态认证:适合高并发、水平扩展友好的场景,服务端无需存储会话,天然支持跨域与多端;但 Token 一旦签发难以主动失效,需要配合黑名单或短过期时间处理登出与权限变更。

实际选型时,如果业务强依赖「服务端可随时吊销会话」,优先选择 Redis 共享 Session;如果追求无状态、易扩展且能接受 Token 过期策略的复杂度,则 JWT 更合适。两者也可以结合:用 JWT 做接口鉴权,用 Redis 维护黑名单或刷新令牌,兼顾无状态与可控性。

4.1.2 JWT 与 Redis 共享 Session 的选型对比

在分布式场景下,JWT 无状态认证与 Redis 共享 Session 是两种主流的登录态方案,二者在会话控制、性能、安全性和适用场景上各有侧重。下面从四个维度进行对比:

对比维度 JWT 无状态认证 Redis 共享 Session
会话控制 服务端不保存会话,Token 签发后难以主动失效,强制下线、踢人等操作需要借助黑名单或短过期时间实现。 会话集中存储在 Redis,服务端可随时删除或修改 Session,天然支持强制下线、踢人、会话续期等主动控制。
性能 请求无需访问共享存储,服务端仅做签名校验,水平扩展友好,高并发下性能更优。 每次请求需从 Redis 读取 Session,存在一次网络往返,高并发下对 Redis 的读写压力较大。
安全性 依赖签名算法与密钥管理,Token 一旦泄露在有效期内可被冒用,需配合 HTTPS、短过期与刷新机制降低风险。 会话数据集中在服务端,客户端仅持有 Session ID,安全性更可控,但需防范 Session 固定攻击并做好 Redis 访问控制。
适用场景 适合高并发、水平扩展、跨域多端、前后端分离的微服务架构,以及第三方开放平台等无状态接口场景。 适合强依赖服务端会话控制、需要实时吊销登录态的业务,如后台管理系统、金融交易、需要审计追踪的场景。

选型建议:如果业务强依赖「服务端可随时吊销会话」,例如需要强制下线、踢人或严格的权限变更即时生效,优先选择 Redis 共享 Session;如果追求无状态、易扩展、高并发友好,且能接受 Token 过期策略与刷新机制的复杂度,则 JWT 更合适。实际项目中两者也可以结合:用 JWT 做接口鉴权,用 Redis 维护黑名单或刷新令牌,兼顾无状态与可控性。

5. 认证确认的异常与边界

Shiro 通过异常类型区分确认失败的原因,常见的有:

  • UnknownAccountException:用户名不存在。
  • IncorrectCredentialsException:密码错误。
  • LockedAccountException:账号被锁定。
  • ExcessiveAttemptsException:尝试次数过多。

在架构设计上,建议在 Realm 中统一捕获查询异常并转换为 Shiro 标准异常,避免把数据库细节暴露给上层。

下面给出一个 Java 示例,演示如何捕获 Shiro 的常见认证异常并转换为友好的提示信息:

java 复制代码
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;

public class LoginService {

    public String login(String username, String password) {
        try {
            // 封装令牌并提交认证
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            SecurityUtils.getSubject().login(token);
            return "登录成功";
        } catch (UnknownAccountException e) {
            return "用户名不存在";
        } catch (IncorrectCredentialsException e) {
            return "密码错误";
        } catch (LockedAccountException e) {
            return "账号已被锁定";
        } catch (ExcessiveAttemptsException e) {
            return "尝试次数过多,请稍后再试";
        } catch (AuthenticationException e) {
            return "认证失败:" + e.getMessage();
        }
    }
}

在 Realm 中,建议统一捕获底层查询异常并转换为 Shiro 标准异常,避免把数据库细节暴露给上层。示例如下:

java 复制代码
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
    String username = (String) token.getPrincipal();
    try {
        User user = userMapper.findByUsername(username);
        if (user == null) {
            throw new UnknownAccountException("用户名不存在");
        }
        if (user.getStatus() == 0) {
            throw new LockedAccountException("账号已被锁定");
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()), getName());
    } catch (DataAccessException e) {
        // 将数据库访问异常统一转换为 Shiro 标准异常
        throw new AuthenticationException("用户数据查询失败", e);
    }
}

通过这种分层处理,上层业务只需捕获 AuthenticationException 及其子类即可完成友好的错误提示,同时避免把 SQL 异常等底层细节直接暴露给调用方。

6. 小结

Shiro 的认证确认可以概括为:Token 收集凭证、Authenticator 调度、Realm 查询数据、CredentialsMatcher 比对密码、Session 固化结果。理解这条链路,就能在常见架构中灵活配置 Shiro,无论是单体 Session 还是分布式 Token 方案,都能找到对应的确认落点。

相关推荐
Julien20042 小时前
自动挂载网络附加存储
linux·服务器·网络
TDengine (老段)3 小时前
TDengine 常见问题 TOP7
大数据·数据库·时序数据库·常见问题·tdengine·涛思数据
+VX:Fegn08953 小时前
计算机毕业设计|基于java+ vue共享单车信息系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
^Rocky3 小时前
IT 疑难杂症诊疗室:复杂技术问题解决实录
运维·bug
Ivanqhz3 小时前
Slope One 算法详解:与矩阵分解、共现矩阵的异同
java·服务器·网络·人工智能·深度学习
m4Rk_3 小时前
【论文阅读】Agent 记忆机制(75):TiMem——用时间记忆树实现长期记忆的层级巩固
论文阅读·人工智能·学习·开源·github
mysqloffice3 小时前
数智时代,核心系统数据库架构往哪走
数据库·人工智能
挖掘狂人3 小时前
当生产环境"变慢",我用这套 perf + strace 30 分钟定位瓶颈
linux·运维·性能优化
l1t3 小时前
DeepSeek总结的 pgColumnar 1.0-alpha4 发布说明
数据库·postgresql