java代码审计 Shiro认证授权部分
前言:
这两天发现自己读shiro权限这块有点忘了,于是再好好学一遍shiro,然后结合实战代码审计记录练下
1.Shiro 核心组件
shiro中的权限定义:用户,角色,权限 ,如图所示
1、UsernamePasswordToken,Shiro 用来封装用户登录信息,使用用户的登录信息来创建令牌 Token。
2、SecurityManager,Shiro 的核心部分,负责安全认证和授权。
3、Suject,Shiro 的一个抽象概念,包含了用户信息。
4、Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。
5、AuthenticationInfo,用户的角色信息集合,认证时使用。(来认证看你是什么角色)
6、AuthorzationInfo,用户的权限信息集合,授权时使用(角色拥有什么权限)
7、DefaultWebSecurityManager,安全管理器,开发者自定义的 Realm 需要注入到此进行管理
8、ShiroFilterFactoryBean,过滤器工厂,Shiro的基本运行机制是开发者定制规则,Shiro去执行具体的执行操作
整个过程:首先用户登录,UsernamePasswordToken封装创建token,然后去找Suject查用户信息,接着又SecurityManager负责认证和授权,他会利用到AuthenticationInfo,AuthorzationInfo进行权限和角色的授权
2.Shiro配置
shiro中Realm是用户自己写的模块用来完成,首先来看到ShiroRealm的编写,需要继承抽象类AuthorizingRealm
两个抽象方法必须实现 doGetAuthenticationInfo
,doGetAuthorizationInfo
,这两个方法这样理解,(如果你是admin登录,doGetAuthenticationInfo就是校验你是不是admin,doGetAuthorizationInfo给你的admin账户授权,你的角色和权限)
先来看doGetAuthenticationInfo(用来校验用户正确性,未整合JWT的写法,也就是用账号密码认证)
整个思路是,用户输入的账号密码会封装到AuthenticationToken
中,我们取了之后使用用户登录的账号去数据库查询出正确的密码。接着返回SimpleAuthenticationInfo(携带正确的凭证,在这里是密码),其带有了正确的密码会使用默认的CredentialsMatcher()去对比是否正确,然后用户是否正确
ShiroConfig的配置
需要将realm注入到securityManager中
再把securityManager注入ShiroFilterFactoryBean
在ShiroFilterFactoryBean中实现对权限的设置
接着回到实现权限认证的另一个类doGetAuthorizationInfo
,这里是通过在数据库中设置对应的perms和roles的值来代表对应的权限和角色。通过Subject拿到用户信息然后设置对应权限角色
3.实际项目
项目地址:https://github.com/jeecgboot/JeecgBoot
shiro整合JWT的写法(也就是用jwttoken认证代替密码认证)
优点:
实现 “无状态认证”,适配分布式与微服务架构
减轻服务端存储与性能压力
...
也是看到doGetAuthenticationInfo。这里和上面不同的是自定义的函数checkUserTokenIsEffect完成了token的校验。SimpleAuthenticationInfo中传入的token自然也是正确的凭证,也就是没有使用shiro自带的校验功能
所以其实这里可以自定义CredentialsMatcher
跳过传统的比对逻辑,但是项目中没写也不碍事
public class JwtCredentialsMatcher implements CredentialsMatcher {@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {// 由于JWT在Realm中已经验证过有效性,这里直接返回true,表示“凭证匹配”return true;}
}
通过实现接口来实现jwt
checkUserTokenIsEffect:如何校验jwt,其实和jwt的原理差不多
主要是这三部,获取用户,然后查询正确的用户信息,然后调用jwtTokenRefresh校验用户是否正确
在getLoginUser()
查询中会存在一个aes解密,因为数据库进行了aes加密。保证数据安全
ShiroConfig的设置
再把securityManager注入ShiroFilterFactoryBean
配置无需登录即可访问的url
这里添加的过滤器主要是对跨域的支持
在securityManager注入Realm,禁用 Shiro 的会话(Session)存储功能,并配置 Redis 作为缓存管理器,适配无状态认证场景(如 JWT 认证)
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(myRealm);/** 关闭shiro自带的session,详情见文档* http://shiro.apache.org/session-management.html#SessionManagement-* StatelessApplications%28Sessionless%29*/DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();defaultSessionStorageEvaluator.setSessionStorageEnabled(false);subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);securityManager.setSubjectDAO(subjectDAO);//自定义缓存实现,使用redissecurityManager.setCacheManager(redisCacheManager());return securityManager;
}
接着把这整个项目的登录认证授权捋一遍
首先在登录接口输入账号密码后,查询是否存在登录的此用户,如果存在则用户开始密码匹配,每个账户也存在一个加密盐,然后将密码通过PBEWITHMD5andDES算法加密与数据库比对,相同则代表登录成功
登录成功后生成token,这里生成jwt使用的算法是HMAC256,secret为该账户的密码,然后传回前端,即前端就有了这个凭证
登录成功后前端会访问/getUserinfo这个接口带上刚刚返回给前端的token。被jwtfilter拦截。判断当前路径是否有@IngoreAuth路径。然后调用executeLogin()
注解@IgnoreAuth的配置
然后会交给realm登入认证
我们可以重点看看这个自定义的类是如何校验的
checkUserTokenIsEffect:如何校验jwt,其实和jwt的原理差不多
首先也是获取正确的用户,不过如果是从redis中获取的话会经过aes解密,也就是redis中的数据是由aes加密过的,逻辑在handlerObject中
调用jwtTokenRefresh
也就是主要的token判断是否合理
这段代码首先从redis中取出token(经过认证后存入的token),发现如果存在token,有效性有问题也刷新。算一个小漏洞,不过危害程度很低,如果攻击者能获取到某用户(如admin用户)的token即使过期,仍然可以利用普通用户的账号密码登录加此admin用户的token伪造高权限。权限维持。因为传入的账号密码是正确的账号密。漏洞利用的条件:某用户token(在redis中)即可
private static boolean jwtTokenRefresh(String token, String userName, String passWord, RedisUtil redisUtil) {String cacheToken = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));if (oConvertUtils.isNotEmpty(cacheToken)) {// 校验token有效性if (!JwtUtil.verify(cacheToken, userName, passWord)) {String newAuthorization = JwtUtil.sign(userName, passWord);// 设置Toekn缓存有效时间redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);}return true;}return false;}
当访问需要鉴权的接口时会调用doGetAuthorizationInfo
授权,也是从数据库中取对应的角色权限赋值来。
如访问某个接口需要什么权限如下即可
@PostMapping(value = "/add")@RequiresPermissions("airag:knowledge:add")public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);airagKnowledgeService.save(airagKnowledge);return Result.OK("添加成功!");}