1. 授权
授权,即访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
2. 授权中的关键对象
在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)
-
主体(Subject)
,主体需要访问系统中的资源。 -
资源(Resource)
,在应用中用户可以访问的URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源又或者是系统菜单、页面、按钮、类方法、系统商品信息等,用户只要授权后才能访问。- 资源包括
资源类型
和资源实例
,比如商品信息为资源类型
,类型为t01的商品为资源实例
,编号为001的商品信息也属于资源实例。
- 资源包括
-
权限(Permission)
,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。 -
角色(Role)
,角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。
3 常见的授权形式
-
基于角色的访问控制
RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
Shiro中的实现:
-
编程式:通过写 if/else 授权代码块完成:
Subject subject = SecurityUtils.getSubject(); if(subject.hasRole(“admin”)) { //有权限 } else { //无权限 }
-
注解式:通过在执行的 Java 方法上放置相应的注解完成:
@RequiresRoles("admin") public void hello() { //有权限 }
-
JSP/GSP 标签:在 JSP/GSP 页面通过相应的标签完成:(注意: Thymeleaf 中使用shiro需要额外集成!)
<shiro:hasRole name="admin"> <!— 有权限 —> </shiro:hasRole>
-
-
基于资源的访问控制
RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
if(subject.isPermitted("user:update:01")){ //资源实例 //对01用户进行修改 } if(subject.isPermitted("user:update:*")){ //资源类型 //对01用户进行修改 }
4.字符串通配符权限
规则:“资源标识符:操作:资源实例标识符**” 即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串,“:”表示资源/操作/实例的分割;“,”表示操作的分割;“*”表示任意资源/操作/实例。
4.1 单个资源单个权限
subject().checkPermissions("system:user:update");
用户拥有资源“system:user”的“update”权限。
4.2 单个资源多个权限
role=system:user:update,system:user:delete
然后通过如下代码判断
subject().checkPermissions("system:user:update", "system:user:delete");
用户拥有资源“system:user”的“update”和“delete”权限。如上可以简写成:
ini 配置(表示角色4拥有 system:user 资源的 update 和 delete 权限)
role="system:user:update,delete"
接着可以通过如下代码判断
subject().checkPermissions("system:user:update,delete");
通过“system:user:update,delete”验证“system:user:update, system:user:delete”是没问题的,但是反过来是规则不成立。
4.3单个资源全部权限
ini 配置
role="system:user:create,update,delete:view"
然后通过如下代码判断
subject().checkPermissions("system:user:create,update,delete:view");
用户拥有资源“system:user”的“create”、“update”、“delete”和“view”所有权限。如上可以简写成:
ini 配置文件(表示角色 5 拥有 system:user 的所有权限)
role=system:user:*
也可以简写为(推荐上边的写法):
role=system:user
然后通过如下代码判断
subject().checkPermissions("system:user:*");
subject().checkPermissions("system:user");
通过“system:user:*”验证“system:user:create,delete,update:view”可以,但是反过来是不成立的。
4.4 所有资源单个权限
ini 配置
role61=*:view
然后通过如下代码判断
subject().checkPermissions("user:view");
用户拥有所有资源的“view”所有权限。假设判断的权限是“"system:user:view”,那么需要“role5=::view”这样写才行
4.5 实例级别的权限
- 单个实例单个权限
ini 配置
role=user:view:1
对资源 user 的 1 实例拥有 view 权限。
然后通过如下代码判断
subject().checkPermissions("user:view:1");
- 单个实例多个权限
ini 配置
role="user:update,delete:1"
对资源 user 的 1 实例拥有 update、delete 权限。
然后通过如下代码判断
subject().checkPermissions("user:delete,update:1");
subject().checkPermissions("user:update:1", "user:delete:1");
- 单个实例所有权限
ini 配置
role=user:*:1
对资源 user 的 1 实例拥有所有权限。
然后通过如下代码判断
subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");
- 所有实例单个权限
ini 配置
role=user:auth:*
对资源 user 的 所有实例拥有auth权限。
然后通过如下代码判断
subject().checkPermissions("user:auth:1", "user:auth:2");
- 所有实例所有权限
ini 配置
role=user:*:*
对资源 user 的所有 实例拥有所有权限。
然后通过如下代码判断
subject().checkPermissions("user:view:1", "user:auth:2");
4.6 Shiro 对权限字符串缺失部分的处理
user:view
等价于user:view:*
;而organization
等价于organization:*
或者organization:*:*
。可以这么理解,这种方式实现了前缀匹配。user:*
可以匹配如user:delete
、user:delete
可以匹配如user:delete:1
、user::1
可以匹配如user:view:1
、user
可以匹配user:view
或user:view:1
等。- 即
*
可以匹配所有,不加*
可以进行前缀匹配;但是如*:view
不能匹配system:user:view
,需要使用*:*:view
,即后缀匹配必须指定前缀(多个冒号就需要多个来匹配)。
4.7 性能问题
通配符匹配方式比字符串相等匹配来说是更复杂的,因此需要花费更长时间,但是一般系统的权限不会太多,且可以配合缓存来提供其性能,如果这样性能还达不到要求我们可以实现位操作算法实现性能更好的权限匹配。另外实例级别的权限验证如果数据量太大也不建议使用,可能造成查询权限及匹配变慢。可以考虑比如在sql查询时加上权限字符串之类的方式在查询时就完成了权限匹配。
5. 授权代码实现
代码中包含了认证的代码
public class SaltMD5Realm extends AuthorizingRealm {
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//根据用户名模拟到数据源获取权限以及权限信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("primaryPrincipal = " + primaryPrincipal);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//模拟获取到了admin角色权限
simpleAuthorizationInfo.addRole("admin");
//模拟获取到用户对应资源权限,添加到simpleAuthorizationInfo
simpleAuthorizationInfo.addStringPermission("user:update:*");
simpleAuthorizationInfo.addStringPermission("product:*:*");
return simpleAuthorizationInfo;
}
/**
* 给传入密码做md5加密
* @param password
* @return
*/
public String Md5Util(String password){
Md5Hash md5Hash = new Md5Hash(password);
return md5Hash.toHex();
}
/**
* 给传入密码做md5+salt加密
* @param password
* @param salt
* @return
*/
public String Md5SaltUtil(String password, String salt){
Md5Hash md5Hash = new Md5Hash(password, salt);
return md5Hash.toHex();
}
/**
* 给传入密码做md5+salt+hash散列次数 加密
* @param password
* @param salt
* @param hash
* @return
*/
public String Md5SaltHashUtil(String password, String salt, Integer hash){
Md5Hash md5Hash = new Md5Hash(password, salt, hash);
return md5Hash.toHex();
}
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if ("Gangbb".equals(principal)) {
//只是用md5加密
//String password = this.Md5Util("999");
//return new SimpleAuthenticationInfo(principal, password, this.getName());
//使用md5+salt+hash
String salt = "Gangbb-salt";
Integer hash = 1024;
String password1 = this.Md5SaltUtil("999", salt);
String password2 = this.Md5SaltHashUtil("999", salt, hash);
// 参数1:从数据源获取的用户名 参数2:从数据源获取的Md5+salt密码 参数3:密码处理的盐值 参数4:使用的realm的名字
return new SimpleAuthenticationInfo(principal, password2, ByteSource.Util.bytes(salt), this.getName());
}
return null;
}
}
测试:
public class TestSaltMD5Realm {
public static void main(String[] args) {
//创建安全管理器securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//IniRealm realm = new IniRealm("classpath:shiro.ini");
//设置为自定义realm获取认证数据
SaltMD5Realm saltMD5Realm = new SaltMD5Realm();
//定义密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//给密码匹配器设置md5校验
credentialsMatcher.setHashAlgorithmName("MD5");
//给密码匹配器设置散列次数
credentialsMatcher.setHashIterations(1024);
//把 md5+salt+hash 密码匹配器装入saltMD5Realm
saltMD5Realm.setCredentialsMatcher(credentialsMatcher);
//把saltMD5Realm交给defaultSecurityManager完成校验
defaultSecurityManager.setRealm(saltMD5Realm);
//将安装工具类中设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//创建token令牌
UsernamePasswordToken token = new UsernamePasswordToken("Gangbb", "999");
try {
//用户登录
subject.login(token);
System.out.println("登录成功~~");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!!");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误!!!");
}
//如果认证通过,就进行授权
if(subject.isAuthenticated()){
//基于角色权限管理
//判断单个角色
boolean single = subject.hasRole("admin");
System.out.println(single);
//判断多个角色
boolean many = subject.hasAllRoles(Arrays.asList("role1", "role2"));
System.out.println(many);
//判断多个角色中是否有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("role1", "role2"));
for(Boolean b:booleans){
System.out.println(b);
}
//或者使用check方法,若无权限抛出UnauthorizedException
//subject.checkRole("admin");
//subject.checkRoles(Arrays.asList("role1", "role2"));
// 基于资源授权 单个资源
boolean permitted = subject.isPermitted("product:create:001");
System.out.println(permitted);
//多个资源
boolean permitted2 = subject.isPermittedAll("user:create:1", "user:delete");
System.out.println(permitted2);
//基于资源授权 对应check方法
// subject.checkPermission("sys:user:delete");
// subject.checkPermissions("user:create:1","user:delete");
}
}
}