从一次开发漏洞看shiro的正确使用
字数 1396 2025-08-26 22:11:51

Shiro安全框架的正确使用与漏洞分析

1. 漏洞案例背景

在一个班级管理平台的开发中,开发者使用了Apache Shiro框架进行权限校验。上线后发现存在严重漏洞:攻击者可以绕过身份验证,实现任意用户登录。

2. 问题代码分析

2.1 错误的Realm实现

问题出现在自定义Realm的doGetAuthenticationInfo方法中:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
    throws AuthenticationException, NumberFormatException {
    if (token.getPrincipal() == null) {
        throw new UnknownAccountException();
    }
    Integer studentId = Integer.valueOf((String) token.getPrincipal());
    User user = Optional.ofNullable(userMapper.selectByStudentId(studentId))
                       .orElseThrow(UnknownAccountException::new);
    SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
        user, user.getPassword(), getName());
    
    // 问题点:在认证过程中就将用户信息存入session
    Session session = SecurityUtils.getSubject().getSession();
    session.setAttribute("USER_SESSION", user);
    return info;
}

2.2 登录逻辑

Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(studentId, password);
try {
    subject.login(token);
    // ...
}

2.3 API身份验证逻辑

if (!SecurityUtils.getSubject().isAuthenticated()) {
    return resultJson.error(401, "未授权");
}
User user = CommonUtil.getUserFromShiroSession();

// CommonUtil.getUserFromShiroSession():
public static User getUserFromShiroSession() {
    return (User) SecurityUtils.getSubject().getSession().getAttribute("USER_SESSION");
}

3. Shiro身份认证过程分析

  1. 调用Subject.login()方法
  2. 委托给SecurityManager.login()
  3. 最终调用到AuthenticatingRealm.getAuthenticationInfo()
  4. 调用自定义的doGetAuthenticationInfo()方法
  5. 执行assertCredentialsMatch()进行凭证匹配

关键问题:即使在登录失败的情况下,doGetAuthenticationInfo()中设置的session属性依然会生效,而authenticated状态不会被更新。

4. 漏洞原理

  1. 用户A使用自己的账号成功登录(isAuthenticated变为true)
  2. 带着这个session尝试登录用户B的账号(不知道密码,登录失败)
  3. 但此时session中的USER_SESSION已被更新为用户B的信息
  4. 由于isAuthenticated状态未被重置,系统认为用户仍然是已认证状态
  5. 后续权限检查从session中获取用户信息,误认为当前用户是用户B

5. 错误使用模式的扩展分析

参考文章中同样存在问题的实现:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
    String principal = (String) token.getPrincipal();
    User user = Optional.ofNullable(DBCache.USERS_CACHE.get(principal))
                       .orElseThrow(UnknownAccountException::new);
    if (!user.isLocked()) {
        throw new LockedAccountException();
    }
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
        principal, user.getPassword(), getName());
    
    // 问题点:在认证过程中就将用户信息存入session
    Session session = SecurityUtils.getSubject().getSession();
    session.setAttribute("USER_SESSION", user);
    return authenticationInfo;
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
    // 从session中获取用户信息进行权限校验
    Session session = SecurityUtils.getSubject().getSession();
    User user = (User) session.getAttribute("USER_SESSION");
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    Set<String> roles = new HashSet<>();
    roles.add(user.getRoleName());
    info.setRoles(roles);
    final Map<String, Collection<String>> permissionsCache = DBCache.PERMISSIONS_CACHE;
    final Collection<String> permissions = permissionsCache.get(user.getRoleName());
    info.addStringPermissions(permissions);
    return info;
}

这种实现会导致:

  1. 用户user2(guest角色)无法访问admin接口
  2. 尝试登录user1(admin角色,不知道密码,登录失败)
  3. USER_SESSION已被更新为user1信息
  4. 权限校验时获取到admin角色,成功访问受限接口

6. Shiro的正确使用方式

6.1 获取当前用户的正确方法

错误方式:从自定义的session属性中获取

正确方式

(User) SecurityUtils.getSubject().getPrincipal()

6.2 正确的Realm实现示例

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
    throws AuthenticationException {
    String username = (String) token.getPrincipal();
    User user = userService.findByUsername(username);
    if (user == null) {
        throw new UnknownAccountException();
    }
    // 仅返回认证信息,不操作session
    return new SimpleAuthenticationInfo(
        user, // 认证成功后可通过getPrincipal()获取
        user.getPassword(),
        getName()
    );
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 从principals中获取用户信息
    User user = (User) principals.getPrimaryPrincipal();
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    // 设置角色和权限
    info.setRoles(user.getRoles());
    info.setStringPermissions(user.getPermissions());
    return info;
}

6.3 正确的API验证逻辑

Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
    return resultJson.error(401, "未授权");
}
// 正确获取当前用户信息
User currentUser = (User) subject.getPrincipal();

7. 关键安全原则

  1. 认证与授权分离:认证阶段只验证身份,授权阶段才检查权限
  2. 避免过早存储会话数据:在认证成功前不应修改会话状态
  3. 使用框架提供的标准方式getPrincipal()是Shiro设计用来获取认证主体的标准方法
  4. 状态一致性:认证状态(isAuthenticated)与用户信息必须同步更新

8. 漏洞修复方案

  1. 移除doGetAuthenticationInfo()中对session的直接操作
  2. 所有获取当前用户信息的地方改为使用SecurityUtils.getSubject().getPrincipal()
  3. 确保权限校验基于认证主体而非自定义session属性

9. 总结

通过这个案例,我们学习到:

  1. Shiro框架的错误使用会导致严重的安全漏洞
  2. 理解框架内部工作流程对正确使用至关重要
  3. 认证过程中过早操作session是危险的
  4. 遵循框架设计模式而非自定义非标准实现
  5. 安全组件的实现需要严格遵循最小权限和职责分离原则

正确使用Shiro框架的关键在于理解其设计理念,严格遵循其提供的标准API,避免引入不安全的自定义实现。

Shiro安全框架的正确使用与漏洞分析 1. 漏洞案例背景 在一个班级管理平台的开发中,开发者使用了Apache Shiro框架进行权限校验。上线后发现存在严重漏洞:攻击者可以绕过身份验证,实现任意用户登录。 2. 问题代码分析 2.1 错误的Realm实现 问题出现在自定义Realm的 doGetAuthenticationInfo 方法中: 2.2 登录逻辑 2.3 API身份验证逻辑 3. Shiro身份认证过程分析 调用 Subject.login() 方法 委托给 SecurityManager.login() 最终调用到 AuthenticatingRealm.getAuthenticationInfo() 调用自定义的 doGetAuthenticationInfo() 方法 执行 assertCredentialsMatch() 进行凭证匹配 关键问题 :即使在登录失败的情况下, doGetAuthenticationInfo() 中设置的session属性依然会生效,而 authenticated 状态不会被更新。 4. 漏洞原理 用户A使用自己的账号成功登录( isAuthenticated 变为true) 带着这个session尝试登录用户B的账号(不知道密码,登录失败) 但此时session中的 USER_SESSION 已被更新为用户B的信息 由于 isAuthenticated 状态未被重置,系统认为用户仍然是已认证状态 后续权限检查从session中获取用户信息,误认为当前用户是用户B 5. 错误使用模式的扩展分析 参考文章中同样存在问题的实现: 这种实现会导致: 用户user2(guest角色)无法访问admin接口 尝试登录user1(admin角色,不知道密码,登录失败) 但 USER_SESSION 已被更新为user1信息 权限校验时获取到admin角色,成功访问受限接口 6. Shiro的正确使用方式 6.1 获取当前用户的正确方法 错误方式 :从自定义的session属性中获取 正确方式 : 6.2 正确的Realm实现示例 6.3 正确的API验证逻辑 7. 关键安全原则 认证与授权分离 :认证阶段只验证身份,授权阶段才检查权限 避免过早存储会话数据 :在认证成功前不应修改会话状态 使用框架提供的标准方式 : getPrincipal() 是Shiro设计用来获取认证主体的标准方法 状态一致性 :认证状态( isAuthenticated )与用户信息必须同步更新 8. 漏洞修复方案 移除 doGetAuthenticationInfo() 中对session的直接操作 所有获取当前用户信息的地方改为使用 SecurityUtils.getSubject().getPrincipal() 确保权限校验基于认证主体而非自定义session属性 9. 总结 通过这个案例,我们学习到: Shiro框架的错误使用会导致严重的安全漏洞 理解框架内部工作流程对正确使用至关重要 认证过程中过早操作session是危险的 遵循框架设计模式而非自定义非标准实现 安全组件的实现需要严格遵循最小权限和职责分离原则 正确使用Shiro框架的关键在于理解其设计理念,严格遵循其提供的标准API,避免引入不安全的自定义实现。