shiro
实战教程
一、权限管理
1.1什么是权限管理
基本上涉及到用户参与的系统都需要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源,用户首先经过身份认证通过后,如果该用户有访问这个资源的权限才可以继续访问。
1.2用户身份认证
概念
所谓的身份认真就是判断一个用户是否是合法用户的过程。最常见的就是通过用户输入用户名和密码来校验,看其是否和系统中存储的用户名和密码一致,以此来判断用户的身份是否合法。对于门禁刷卡系统,需要刷卡,对于指纹登录认证,则填充指纹。
身份认证流程
Subject(主体)
访问系统的用户,其实需要认证的都可被称为主体。
Principal(身份信息)
主体的标识,表示具有唯一性,例如你的身份证号码,手机号。一个主体可以有多个角色但是必须有一个主身份。
credentials(凭证信息)
例如密码、证书这些。凭证信息只有主体自己知道。
1.3用户授权
授权又称为访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源是没有权限进行访问的。
授权流程
授权可以理解为主体认证后继续判断是否具有对本资源访问的权限,如果有的话,可以进行访问,如果没有,则拒绝访问
上面提到的有三个关键对象:主体(Subject)、资源(Resource)、权限/许可(Permission);
权限又分为粗粒度和细粒度,粗粒度权限是指对资源类型的权限,细粒度权限是对资源实例的权限。
粗粒度权限管理:用户具有用户管理的权限,具有导出订单明细的权限。对资源实例的控制称为细粒度权限控制,就是控制数据级别的权限。比如:用户只被允许修改本部门的员工信息,用户只允许导出自己创建的订单。
权限分配
对主体分配权限,主体只允许在权限范围内对资源进行操作。例如用户user1分配了对商品信息修改的权限,所以user1只能对商品进行信息修改。
权限分配的数据通常需要持久化,可以将上面的数据模型创建边并将用户具有的权限信息存储到数据库中。
授权方式
-
基于角色的访问控制
-
基于角色的访问控制是以角色为中心进行控制,例如:判断用户A是否具有admin权限
if(userA.hasRole("admin")){ // 有权限 }else{ // 无权限 } // 判断A是否有多个角色 userA.hasAllRoles(Arrays.asList("role1","role2"));
-
-
基于资源的访问控制
-
以资源为中心进行访问
例如用户A对用户管理模块中的所有资源有修改用户信息的权限。
if(A.isPermission("user:update:*")){ // A可以修改user信息,如果换成“uer:update:u1”;就表示只能对u1的用户信息进行修改 }
这里需要知道
权限字符串
上面代码中的"user:update:*"
就是权限字符串,它的格式是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有哪些操作的权限,中间是以:
作为分隔符,*
代表通配符,表示所有。例子:
- 用户创建的权限:
user:create:*
,或者user:create
; - 用户对实例001的所有权限:
user:*:001
- 用户创建的权限:
-
二、shiro框架
什么是shiro?
Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Apache Shiro™是一个功能强大且易于使用的Java安全框架,它执行身份验证,授权,加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序-从最小的移动应用程序到最大的Web和企业应用程序。
http://shiro.apache.org/
Shiro是Apache下的一个开源框架,他将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
核心架构图
-
Subject
主体,外部应用与Subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。subject在Shiro中是一个接口,接口中定义了很多认证授权的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager进行认证授权的。
-
SecurityManager
安全管理器,对全部的
subject
进行安全管理,他是shiro
的核心,也是一个接口,继承了Authenticator
,Authorizer
,SessionManager
这三个接口。 -
Authenticator
对用户身份进行认证,
Authenticator
是一个接口,shiro
提供ModularRealmAuthenticator
实现类,通过ModularRealmAuthenticator
基本上可以满足大多数需求,也可以自定义认证器。 -
Authorizer
用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限.
-
SessionManager
会话管理,
shiro
框架定义了一套会话管理,它不依赖web容器的session
,所以shiro
可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。 -
SessionDAO
SessionDAO
即会话dao
,是对session会话操作的一套接口,比如要将session
存储到数据库,可以通过jdbc
将会话存储到数据库. -
realm
Realm即领域,相当于
datasource
数据源,securityManager
进行安全认证需要通过Realm
获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。 -
CacheManager
即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
-
Cryptography
即密码管理,
shiro
提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能.
认证测试demo
public class TestAuthenticator {
public static void main(String[] args) {
// 创建安全管理对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 设置realm
securityManager.setRealm(new IniRealm("classpath:shiro-authenticator.ini"));
// 安全工具类设置安全管理对象
SecurityUtils.setSecurityManager(securityManager);
// 创建一个主体
Subject subject = SecurityUtils.getSubject();
// 生成Token
UsernamePasswordToken token = new UsernamePasswordToken("xiaohei","123");
try {
// 开始认证
subject.login(token);
System.out.println("认证状态: "+subject.isAuthenticated()); //true
}catch (Exception e){
e.printStackTrace();
}
}
}
认证最终执行用户名比较的是SimpleAccountRealm
类中的 doGetAuthenticationInfo
在这个方法中完成用户校验。
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken)token;
SimpleAccount account = this.getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
2、最终密码校验是在AuthenticatingRealm
类的``assertCredentialsMatch`方法。
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
CredentialsMatcher cm = this.getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) {
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
} else {
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication. If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
}
}
cm.doCredentialsMatch
(token, info)方法校验参数的token和从ini文件或者数据库中取出的认证信息是否一致,这个方法是CredentialsMatcher
接口的唯一方法,用来验证token和认证信息是否匹配。
实现此方法的类有四个,这里debug走的是SimpleCredentialsMatcher
类实现的方法
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = this.getCredentials(token);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenCredentials, accountCredentials);
}
直接强转为Object,然后通过equals方法进行比较。
自定义Realm
自定义Realm,只需要继承AuthorizingRealm
抽象类。实现对应的认证(doGetAuthenticationInfo
)和授权方法(doGetAuthorizationInfo
)。因为AuthorizingRealm已经实现了Authorizer、PermissionResolverAware接口,继承了AuthenticatingRealm抽象类。
图中虚线是接口的实现,蓝色实现代表类的继承。
shiro授权
授权流程
授权方式
编程式
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")){
//有权限
}else{
//无权限
}
注解式
@RequiresRoles("admin")
public void deleteUserById(Integer id){
// 如果有admin的权限才会成功调用这个方法来删除用户
}
标签式
JSP页面中通过以下标签完成授权
<shiro:hasRole name="admin">
<!--有权限-->
</shiro:hasRole>
Thyemeleaf需要额外的配置
授权测试
-
基于角色的访问控制
package club.qy.hu; import club.qy.hu.realm.MD5CustormRealm; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject; import java.util.Arrays; public class TestCustomRealm { public static void main(String[] args) { // 创建安全管理对象 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //创建自定义Realm MD5CustormRealm md5CustormRealm = new MD5CustormRealm(); //创建 HashedCredentialsMatcher对象 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //设置算法名称和散列次数 credentialsMatcher.setHashAlgorithmName("md5"); credentialsMatcher.setHashIterations(1024); md5CustormRealm.setCredentialsMatcher(credentialsMatcher); // 设置成自定义认证 securityManager.setRealm(md5CustormRealm); // 安全工具类设置安全管理对象 SecurityUtils.setSecurityManager(securityManager); // 创建一个主体 Subject subject = SecurityUtils.getSubject(); // 生成Token UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang", "123"); try { // 开始认证 subject.login(token); System.out.println("认证状态: " + subject.isAuthenticated()); } catch (Exception e) { e.printStackTrace(); } if (subject.isAuthenticated()) { //如果认证成功,进行权限判断 System.out.println("----------基于角色的权限认证------------"); if (subject.hasRole("admin")) { System.out.println("该用户具有admin角色的权限"); } if (subject.hasAllRoles(Arrays.asList("admin", "super"))) { System.out.println("该用户具有admin,super 角色的权限"); } } } }
自定义realm
package club.qy.hu.realm; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import java.util.Arrays; public class CustormRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String principal = (String) principalCollection.getPrimaryPrincipal(); System.out.println("认证信息:"+principal); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 添加角色 simpleAuthorizationInfo.addRole("admin"); simpleAuthorizationInfo.addRole("super"); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String principal = (String) authenticationToken.getPrincipal(); // 查询数据库得到 username String username = "xiaozhang"; String salt = "!w46H>,"; if (username.equals(principal)){ // 第一个参数是用户名也叫做认证信息, // 第2个参数代表从数据库中取出的加密的密码, // 第3个参数代表加密所用的盐, // 第4个参数是SimpleAuthenticationInfo的名字 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, "fd6b93fd376e3d9aa012f33eb58641b0", ByteSource.Util.bytes(salt),this.getName()); return authenticationInfo; } return null; } }
输出结果
-
基于资源的访问控制
自定义Realm
public class CustormRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String principal = (String) principalCollection.getPrimaryPrincipal(); System.out.println("认证信息:"+principal); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 添加权限字符串 simpleAuthorizationInfo.addStringPermission("user:create"); simpleAuthorizationInfo.addStringPermissions(Arrays.asList("user:*:01","user:update:*")); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String principal = (String) authenticationToken.getPrincipal(); // 查询数据库得到 username String username = "xiaozhang"; String salt = "!w46H>,"; if (username.equals(principal)){ // 第一个参数是用户名也叫做认证信息, // 第2个参数代表从数据库中取出的加密的密码, // 第3个参数代表加密所用的盐, // 第4个参数是SimpleAuthenticationInfo的名字 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, "fd6b93fd376e3d9aa012f33eb58641b0", ByteSource.Util.bytes(salt),this.getName()); return authenticationInfo; } return null; } }
测试类
public static void main(String[] args) { // 创建安全管理对象 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //创建自定义Realm MD5CustormRealm md5CustormRealm = new MD5CustormRealm(); //创建 HashedCredentialsMatcher对象 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //设置算法名称和散列次数 credentialsMatcher.setHashAlgorithmName("md5"); credentialsMatcher.setHashIterations(1024); md5CustormRealm.setCredentialsMatcher(credentialsMatcher); // 设置成自定义认证 securityManager.setRealm(md5CustormRealm); // 安全工具类设置安全管理对象 SecurityUtils.setSecurityManager(securityManager); // 创建一个主体 Subject subject = SecurityUtils.getSubject(); // 生成Token UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang", "123"); try { // 开始认证 subject.login(token); System.out.println("认证状态: " + subject.isAuthenticated()); } catch (Exception e) { e.printStackTrace(); } if (subject.isAuthenticated()) { //如果认证成功,进行权限判断 System.out.println("----------基于资源的权限认证------------"); System.out.println(subject.isPermitted("user:create")); System.out.println(subject.isPermitted("user:update:01")); //分别具有哪些权限 for (boolean b : subject.isPermitted("user:delete", "user:*:01")) { System.out.println(b); } // 判断是否同时具有指定权限 System.out.println(subject.isPermittedAll("user:delete", "user:*:01")); } }
输出结果