从一次开发漏洞看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身份认证过程分析
- 调用
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. 错误使用模式的扩展分析
参考文章中同样存在问题的实现:
@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;
}
这种实现会导致:
- 用户user2(guest角色)无法访问admin接口
- 尝试登录user1(admin角色,不知道密码,登录失败)
- 但
USER_SESSION已被更新为user1信息 - 权限校验时获取到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. 关键安全原则
- 认证与授权分离:认证阶段只验证身份,授权阶段才检查权限
- 避免过早存储会话数据:在认证成功前不应修改会话状态
- 使用框架提供的标准方式:
getPrincipal()是Shiro设计用来获取认证主体的标准方法 - 状态一致性:认证状态(
isAuthenticated)与用户信息必须同步更新
8. 漏洞修复方案
- 移除
doGetAuthenticationInfo()中对session的直接操作 - 所有获取当前用户信息的地方改为使用
SecurityUtils.getSubject().getPrincipal() - 确保权限校验基于认证主体而非自定义session属性
9. 总结
通过这个案例,我们学习到:
- Shiro框架的错误使用会导致严重的安全漏洞
- 理解框架内部工作流程对正确使用至关重要
- 认证过程中过早操作session是危险的
- 遵循框架设计模式而非自定义非标准实现
- 安全组件的实现需要严格遵循最小权限和职责分离原则
正确使用Shiro框架的关键在于理解其设计理念,严格遵循其提供的标准API,避免引入不安全的自定义实现。