JavaWeb中的权限控制——Apache Shiro框架

  • A+

Apache Shiro概述

   Apache Shiro是Java的一个安全框架,主要用于处理身份认证、授权、企业会话管理和加密等。与Spring Security一样都是一个权限安全框架,但是与Spring Security相比,在于其比较简洁易懂的认证和授权方式。

  常用功能:

  • 身份认证/登陆
  • 权限验证,验证某个已登陆认证的用户是否拥有某个权限
  • 会话管理
  • 加密,保护数据安全性
  • Caching缓存,例如用户登陆后其相关的角色/权限不必每次都去查询
  • ......

工作方式

  Apache Shiro主要的架构如下:

图片.png

subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
securityManager:安全管理器,主体进行认证和授权都是通过securityManager进行。
authenticator:认证器,主体进行认证最终通过authenticator进行的。
authorizer:授权器,主体进行授权最终通过authorizer进行的。
sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
SessionDao:通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
realm [relm] :域,领域,相当于数据源,通过realm存取认证、授权相关数据。
cryptography:密码管理,提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

  最主要的三大核心组件就是Subject,SecurityManager,realms,其一系列的认证以及权限校验操作主要是通过filter实现的:

图片.png
  常见的认证及权限校验流程如下,Subject是主体,表示当前用户,其所有交互都会委托给安全管理器SecurityManager,其负责与其他组件进行交互,要验证用户身份,还有一个安全数据源Realm,安全管理器SecurityManager会从Realm获取相应的用户进行比较以确定用户身份是否合法,或者从Realm得到用户相应的角色/权限进行验证用户是否能进行操作:

图片.png

相关依赖

  使用shiro需要引入相关依赖:

图片.png

用户认证与授权

  Realm是相关的安全数据源,通过自定义一个Realm类,继承AuthorizingRealm抽象类,重写获取用户信息以及权限校验的方法来实现认证授权操作。
  主要是实现Realm的两个方法:doGetAuthentication(用户身份认证信息)以及doGetAuthorizationInfo(用于权限校验信息)。
  首先是认证流程,先根据传入的用户名结合service方法获取User信息,如果user非空那么生成 AuthenticationInfo信息,然后提供给AuthenticatingRealm内部使用的CredentialsMatcher 进行账号密码等凭证的验证,shiro会直接将密码和前面生成token中的密码进行匹配,如果匹配成功则登陆成功,不成功则报错,当然也可以自己实现相关的凭证验证流程:

java
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException{
//获取用户输入的账号
String username = (String)token.getPrincipal();
UserInfo userInfo = userInfoService.findByUserName(username);
if(userInfo == null){
return null;
}
SimpleAuthenticationInfo authenticaationInfo = new SimpleAuthenticationInfo(
userInfo,
userInfo.getPassword(),
ByteSource.Util.bytes(userInfo.getCredentialsSalt()),
getName()
);
return authenticationInfo;
}

  Controller层的登陆接口的调用如下,UsernamePasswordToken是用来存储用户和密码的,Shiro在Subject.login()的时候调用了我们重写的doGetAuthenticationInfo(AuthenticationToken token)方法。完成对应的身份验证以及授权过程:

java
@RequestMapping("/Auth")
@ResponseBody
public void LoginAuth(String username, String password, RedirectAttributes model,HttpServletResponse response) throws IOException {
Subject sub = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
sub.login(token);
} catch (UnknownAccountException e) {
System.out.println("对用户[{}]进行登录验证,验证未通过,用户不存在" + username);
token.clear();
msg= "UnknownAccountException";
} catch (LockedAccountException lae) {
System.out.println("对用户[{}]进行登录验证,验证未通过,账户已锁定" + username);
token.clear();
msg= "LockedAccountException";
} catch (ExcessiveAttemptsException e) {
System.out.println("对用户[{}]进行登录验证,验证未通过,错误次数过多" + username);
token.clear();
msg= "ExcessiveAttemptsException";
} catch (AuthenticationException e) {
System.out.println("对用户[{}]进行登录验证,验证未通过,堆栈轨迹如下" + username);
token.clear();
msg= "AuthenticationException";
}
return "main"
}

  然后是授权操作,若认证成功后直接调用doGetAuthorizationInfo方法,通过getPrimaryPrincipal得到之前传入的用户名,然后根据用户名调用UserService 接口获取封装角色及权限信息,返回authorizationInfo:

java
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserInfo userInfo = (UserInfo)principals.getPrimaryPrincipal();
for(SysRole role:userInfo.getRoleList()){
authorizationInfo.addRole(role.getRole());
for(SysPermission p:role.getPermissions()){
authorizationInfo.addStringPermission(p.getPermission());
}
}
return authorizationInfo;
}

权限控制方式

  通过自定义一个Realm类,继承AuthorizingRealm抽象类,重写获取用户信息以及权限校验的方法。完成用户认证与授权过程,封装好当前用户的角色及权限信息后,可以结合以下方式完成相关的权限控制:

通过filterChainDefinitions

  ShiroFilter 是整个 Shiro 的入口点,用于拦截需要安全控制的请求进行处理。使用filterChainDefinitions(filterChainDefinitions是Shiro连接约束配置,通过对应的参数定义权限相关的filter Chain,验证顺序是自上而下,从左往右)声明url和filter对应的权限控制关系。
  例如下面的例子,查看shiro的配置文件,每个URL配置,表示配置该URL的请求将由对应的过滤器进行验证,例如authc对应的org.apache.shiro.web.filter.authc.FromAuthenticationFilter,该过滤器表示需要认证通过后才可以访问:

图片.png

  常见的filterChainDefinitions参数如下:

| 参数 | 对应的Filter | 作用 |
| ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 身份验证相关 | | |
| authc | org.apache.shiro.web.filter. authc.FormAuthentication Filter | 表示需要认证通过后才可以访问 |
| logout | org.apache.shiro.web.filter. authc.LogoutFilter | 表示退出成功后重定向的地址 |
| anon | org.apache.shiro.web.filter. authc.AnonymousFilter | 表示可以非登陆状态下匿名使用,一般静态资源和登陆接口会用到比较多 |
| 授权相关 | | |
| roles | org.apache.shiro.web.filter. authz.RolesAuthorizationF ilter | 表示必须拥有相关角色才可以访问 |
| perms | org.apache.shiro.web.filter. authz.PermissionsAuthoriz ationFilter | 表示必须拥有相关权限 |

  除了通过配置文件的方式,在SpringBoot进行整合时,也可以使用注解的方式,通过Shiro的Filter工厂,设置对应的过滤条件和跳转条件:

java
@Configuration
public class shiroConfig {
......
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
......
}

通过相关标签

  在实际业务场景中,对于已登陆的用户角色,需要根据不同的权限对view层进行保护,显示/隐藏Web应用程序的JSP/View。例如查看所有用户信息的页面仅仅只有管理员角色才可以查看。对于普通用户来说需要隐藏。

  shiro提供了JSTL标签用于在JSP/GSP页面进行权限控制,如根据登录用户显示相应的页面按钮。可以通过在jsp页面中引入标签库,通过对应的标签来实现:

图片.png

  常见的标签如下:

| 标签 | 功能 |
| ------------------------------------------- | ------------------------------------------------------ |
| <shiro:authenticated> | 完成登陆认证后即可显示内容 |
| <shiro:notAuthenticated> | 不在登陆状态即可显示内容 |
| <shiro:hasPermission name=”update”> | 拥有update权限资源的用户即可显示内容 |
| <shiro:lacksPermission name=”update”> | 没有update权限资源的用户即可显示内容 |
| <shiro:principal> | 显示用户身份名称 |
| <shiro:principal property=”username”> | 显示用户身份中的属性值 |
| <shiro:lacksRole name=”guest”> | 当前用户没有guest角色时即可显示内容 |
| <shiro:hasRole name=”guest”> | 当前用户拥有guest或test角色时即可显示内容 |
| <shiro:hasAnyRoles name=”guest,test”> | 如果当前用户有任意一个角色(或的关系)将显示 body 体内容 |
| <shiro:guest> | 用户没有身份验证时显示相应信息,即游客访问信息 |

  例如下面的例子,只有当前用户角色为admin时才可以访问查询用户信息接口:

图片.png

通过权限注解

  shiro 提供了相应的注解用于权限控制,将权限控制的细粒度增加到url上,可以防止用户跳过前端直接访问后端接口需要在相关配置文件中添加对应的支持:

图片.png

  常用的一些注解如下:

| 注解 | 功能 |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| @RequiresAuthentication | 表示当前 Subject 已经通过 login 进行了身份验证(用户已登陆) |
| @RequiresUser | 表示当前 Subject 已经身份验证或者通过RememberMe登录的。 |
| @RequiresGuest | 表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。 |
| @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND) | 表示当前 Subject 需要角色 admin 和 user。 |
| @RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR) | 表示当前 Subject 需要权限 user:a 或 user:b。 |

  例如下面的例子,当前用户必须要有保存用户和administrator权限才可以使用该接口:

java
@RequiresPermissions(value={"user_saveUser","administrator"},logical=Logical.OR)
@RequestMapping(value="/user",method=RequestMethod.POST)
public Message&lt;String&gt; saveUser(@RequestBody UserSaveVM user){
userService.saveUser(user);
return Message.success(Constants.SUCCESS);
}

直接在接口上操作

  例如平行越权的安全防护,主要是通过权限细粒度直接覆盖接口来完成的。例如下面的例子,以查询用户信息业务为例,通过Shiro的SecurityUtils.getSubject().getPrincipal()方法获取当前用户,在查询之前首先对用户身份进行校验:

java
@ResponseBody
@RequestMapping("queryUserInfo")
public UserVo queryUserInfo(String userId){
Subject subject = SecurityUtils.getSubject();
String username = (String)subject.getPrincipals().getPrimaryPrincipal();
UserVo currentUser = userService.selectUserByUserName(username);
if(!currentUser.getUserId().equals(userId)){
return new UserVO();
}
UserVo user = UserService.queryUserById(userId);
return user;
}

  或者在service层调用isPermitted()方法判断用户有没有相关权限:

java
SecurityUtils.getSubject().isPermitted("")

审计要点

  shiro一般用户处理处理身份认证、授权等业务场景,那么主要的审计内容还是围绕业务逻辑展开:

Relam设计

  shiro的认证授权主要是通过Relam实现的,所以有必要检查其具体实现。如果存在相关的设计缺陷,那么可能导致一系列的权限安全问题。看一个相关的例子:

  shiro身份校验时候subject.login的部分调度相关代码如下:

java
Subject subject = securityManager.login(this,token);
PrincipalCollection principals;
String host = null;
if(subject instanceof DelegatingSubject){
DelegatingSubject delegating = (DelegatingSubject) subject;
principals = delegating.principals;
host = delegating.host;
}else{
principals = subject.getPrincipals();
}
if(principals ==null|| principals.isEmpty()){
String msg ="Principals returned from securityManager.login(token) returned a null or empty value. This value must be non null populated wiht one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;

  在身份认证中有一个关键的值authenticated,代表着当前身份验证保留的状态。当其值为true时,说明用户验证成功了(登录成功且未抛出相关异常)。这里有一个比较有意思的点,登录成功,刷新authenticated的值没有任何的疑问,但是通过整个调用链下来,发现在重复登录的时候,如果用户登录失败,用户的authenticated是不会进行改变的,但是Subject只有在用户成功登录之后才进行更新。(当A用户使用自己的账号登录成功以后,此时authenticated为true,此时使用同一个会话,使用账号B,错误的密码登陆失败,authenticated仍为true。)

  开发是可以在Relam自己实现相关的凭证验证流程的,一般常见的登陆流程为,首先验证用户名密码,若验证成功,则将当前User对象保存在session会话中,用于后续权限校验以及用户业务流程.例如如下的自定义的Realm:

java
public class UserRealm extends AuthorizingRealm{
@Autowired
private IUserService userService;
public IUserService getUserService() {
return userService;
}
public void setUserService(IUserService userService) {
this.userService = userService;
}
//权限校验方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
// TODO Auto-generated method stub
// 获取当前shiro进行权限校验
Session session = SecurityUtils.getSubject().getSession();
// 从session中取出当前的用户
User user= (User)session.getAttribute("user");
Set&lt;String&gt; roles = new HashSet&lt;&gt;();
// 从services获取当前用户的权限状态并结合shiro进行封装
roles.add(user.getRole());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
return info;
}
//登录校验方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// TODO Auto-generated method stub
//获取需要登录的用户
String principal =(String)token.getPrincipal();
if(userService==null) {
userService= SpringBeanFactoryUtils.getBean(IUserService.class);
}
//从service层获取对应的用户信息:
User user = userService.search(principal);
//核心的登录校验,进行相应的客户端传入的账号密码以及数据库内的进行比对,成功允许登录,失败抛出异常
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal,user.getPassword(), this.getName());
//用户登录成功后,进行设置shiro session的操作,因为后续连带性的业务需要,很多场景都需要从session中去维护我们的身份信息。
Session session = SecurityUtils.getSubject().getSession();
//将当前用户对象存入shiro的session中
session.setAttribute("user", user);
return authenticationInfo;
}

  上述过程就是先验证当前的用户主体Subject是否已成功认证了,若是,那么就将当前用户对象存入shiro的session中,然后再调用doGetAuthorizationInfo方法,将当前session中的用户权限进行赋予。

  结合前面的点,可以发现有一个缺陷是,当发现当用户重复登录的时候,会改变session中user属性的值,但是无论成功与否都并不会影响anthenticated的状态,那么也就是说,只要知道数据库中其中一个用户的账号密码,即可实现任意用户登陆的安全问题。

  大致利用过程如下:

  当A用户使用自己的账号登录成功以后,此时antuenticated状态为true,说明用户认证成功,此时尝试登录用户B的账号,即使登录失败,但是session中的user属性已经封装了用户B的信息(因为这里是直接传入用户指定的user信息的),并且antuenticated状态并没有更新为false(用户仍处于认证成功的状态),那么这样就绕过了身份验证,达到了任意用户登录的效果了。下面是具体实例:

  主要有两个角色admin和test,其中tkswifty登录后存在查询管理manage功能:

图片.png
  test账号ceshi登录后只存在Home功能:

图片.png
   首先正常登录ceshi用户,激活shiro中的Authenticated的状态, 此时在同一个浏览器处,新开一个tab(保证会话的一致性),重新访问login页面,此时在新打开的tab标签里尝试用错误的密码登录tkswifty账号。
  此时显示登录失败,重新返回ceshi用户的页面,刷新,发现此时已经切换到tkswifty用户,并且拥有manage模块的操作权限了:

图片.png
  既然使用了框架,那么在开发设计时肯定要考虑到相关的特性,Shiro本身提供了SecurityUtils.getSubject().getPrincipal()方法来获取当前用户,并且仅仅只有在登陆认证成功后才会刷新对应的值,那么就可以解决上述的安全问题了。

平行越权

  shiro的授权在类似查询个人信息防止平行越权这类的业务场景下没法通过ShiroFilter进行通用的配置。常常会有遗漏。例如下面的例子,接口仅仅根据userId返回用户信息,并未校验当前用户是否具有权限查询对应userId的信息:

java
@ResponseBody
@RequestMapping("/queryUserInfo")
public UserVo queryUserInfo(String userId){
UserVo user = userService.queryUesrById(userId);
return user;
}

垂直越权

  主要还是权限细粒度的覆盖问题,一般使用标签库对普通用户与管理员用户的在view层页面上进行了区分,认为只要在界面上没法操作就可以防止垂直越权了。例如下面的例子:
  view层通过shiro标签进行了权限划分,只有拥有管理员角色的用户才能看到删除用户接口的相关页面:

图片.png

  那么可以直接定位/deleteUser/ByUserId接口,结合filterChainDefinitions查看对应的权限细粒度覆盖。

  首先是filterChainDefinitions,可以看到这里仅仅是禁止在未通过认证的情况下非登陆访问接口:

图片.png
  然后就是接口实现,可以看到接口处也没有任何的权限注解,也就是说只要通过登陆认证,以普通用户的角色也可以访问管理员删除用户的接口:

java
@RequestMapping("/deleteUser/ByUserId")
public Boolean deleteUser(String userId){
Boolean result = userservice.deleteById(userId);
return result;
}

  Tips:在进行审计时,可以先在view层搜索对应的shiro标签关键字(例如shiro:hasRole),然后查看对应隐藏/显示的接口,通过检查filterChainDefinitions以及接口具体实现,快速寻找垂直越权缺陷

静态资源

  一般静态资源和登陆接口可以非登陆状态下匿名使用,在filterChainDefinitions中会做如下配置:

xml
/static/** = anno

  但是部分static目录下的文件可能存在未授权访问,例如网站部分业务的模版文件、内部资料,或者是通过相关接口道出的统计Excel表、PDF单据等,都可能存在匿名下载的风险。
  所以可以定位相关的目录,审计目录下的内容以及可能关联的接口。

反序列化

Shiro550:
  shiro≤1.2.4版本,默认使用了CookieRememberMeManager,由于AES使用默认的KEY/常见的KEY/KEY泄露,导致反序列化的cookie可控,从而引发反序列化攻击。

Shiro721:
  rememberMe cookie通过AES-128-CBC模式加密,易受到Padding Oracle攻击。可以通过结合有效的rememberMe cookie作为Padding Oracle攻击的前缀,然后精心制作rememberMe来进行反序列化攻击。

Tips:在1.4.2版本后shiro更换AES-CBCAES-GCM

shiro搭配spring时身份验证绕过(CVE-2020-1957)

  Apache Shiro<1.5.2时,当Spring框架的动态Controller与Shiro结合使用时,尝试在对应的URL接口后加入/可能可以绕过相关的权限控制。

参考资料

Shiro教程_张开涛
https://xz.aliyun.com/t/5287

相关推荐: 业务安全的上岸体历(-)

在甲方做业务SDL的几年,在落地方面也做了不少努力。一是得看着业务高P的脸色,二是得假装硬气的扮猪吃老虎,一路走来可谓是一把辛酸泪。 闲话不多扯,我们团队在SDL方向,针对公司现状进行威胁建模,进行了多个维度的治理。 在本文笔者会根据过往的治理工作,在业务建设…