Shiro学习01:使用Shiro实现身份管理和权限验证
Shiro的基本概念
-
Shiro的基本功能:
Authentication
: 身份验证,即登录Authorization
: 权限验证,即授权Cryptography
: 加密,将密码以密文形式存储进数据库
-
Shiro的三个基本概念:
Subject
,SecurityManager
,Realms
Subject
表示当前操作的用户,可以通过Subject currentUser = SecurityUtils.getSubject()
获得当前Subject
.SecurityManager
安全控制器,是实现Shiro功能的核心,与其他组件进行交互,实现Subject
委托的各种功能,类似于Spring MVC中的DispatcherServlet
.Realms
安全数据源,Shiro从Realm
获取安全数据(如用户,角色,权限).
-
Shiro结构:
Subject
: 当前登录的用户SecurityManager
: 安全控制器,Shiro的核心,它管理着所有Subject
,且负责进行认证和授权,及会话,缓存的管理Authenticator
: 认证器,负责用户登录Authorizer
: 授权器,控制用户能访问哪些功能Realm
: 安全数据源,用于获取安全实体,一般在应用中我们需要自己实现Realm
.
Shiro入门实例1: 通过ini配置文件实现身份验证
项目准备
新建MAVEN项目,在pom.xml
中导入如下依赖:
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
使用默认Realm组件
在不进行配置的情况下,Shiro会使用从ini文件中读取的数据创建数据源.
-
在
main/resources
目录下创建shiro.ini
文件如下,用来构建Realm
数据源[users] # 模拟用户数据库列表: 账号=密码 user1 = password1 user2 = password2
-
编写代码测试登录功能
@Test public void TestLogin() { // 通过ini配置文件创建SecurityManager工厂对象 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); // 通过工厂对象生产SecurityManager对象 SecurityManager securityManager = factory.getInstance(); // 将SecurityManager绑定到当前运行环境中 SecurityUtils.setSecurityManager(securityManager); // 创建当前的登陆主体 Subject currentUser = SecurityUtils.getSubject(); // 模拟收集当前登录主体的身份凭证 UsernamePasswordToken token = new UsernamePasswordToken("user1", "password1"); // 尝试登陆账号 try { currentUser.login(token); // 可以通过当前Subject对象的isAuthenticated()方法判断当前登录状态 System.out.println("当前登录状态" + currentUser.isAuthenticated()); } catch (UnknownAccountException uae) { System.out.println("username wasn't in the system"); } catch (IncorrectCredentialsException ice) { System.out.println("password didn't match"); } catch (LockedAccountException lae) { System.out.println("account for that username is locked"); } // 退出账号 currentUser.logout(); System.out.println("当前登录状态" + currentUser.isAuthenticated()); }
输出:
当前登录状态true 当前登录状态false
若认证操作失败,系统可能抛出如下三种常见的异常:
UnknownAccountException
: 账号不存在IncorrectCredentialsException
: 账号存在但密码错误LockedAccountException
: 账户被锁定
观察源代码,发现程序的执行过程如下
使用自定义Realm
-
实现我们自定义的
Realm
类,继承自AuthorizingRealm
.[外链图片转存失败(img-TZxVWmQI-1565533563049)(1565340862010.png)]
package cn.maoritian.realm; public class MyRealm extends AuthorizingRealm { // 使用该方法返回值区分不同Realm public String getRealm() { return "MyRealm"; } // doGetAuthorizationInfo()进行授权操作 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } // doGetAuthenticationInfo()进行认证操作 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 从token中获取用户名 String username = (String) token.getPrincipal(); // 模拟根据用户名在数据库中查询密码 String password; if (!"user1".equals(username)) { return null; } else { password = "password1"; } // 返回登录比对信息 AuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName()); return info; } }
-
编写
shiro.ini
配置文件,指定使用我们自定义的Realm
[main] # 自定义realm myRealm =cn.maoritian.realm.MyRealm # 指定SecurityManager的realms实现 securityManager.realms=$myRealm
-
编写代码测试登录功能,测试代码与上个测试代码完全相同
@Test public void TestLoginMyRealm() { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("user1", "password1"); try { currentUser.login(token); System.out.println("当前登录状态" + currentUser.isAuthenticated()); } catch (UnknownAccountException uae) { System.out.println("username wasn't in the system"); } catch (IncorrectCredentialsException ice) { System.out.println("password didn't match"); } catch (LockedAccountException lae) { System.out.println("account for that username is locked"); } currentUser.logout(); System.out.println("当前登录状态" + currentUser.isAuthenticated()); }
使用密文存储密码
在实际项目中,我们都是将密码以加盐且多次迭代的方式存储在数据库中,使用shiro内置的方法,我们可以完成对明文的加密.
String password = "password"; // 明文
Md5Hash md5Hash = null; // 密文
// 直接对明文进行1次md5加密
md5Hash = new Md5Hash(password);
System.out.println(md5Hash); // 5f4dcc3b5aa765d61d8327deb882cf99
// 对明文加盐进行1次md5加密
md5Hash = new Md5Hash(password, "username");
System.out.println(md5Hash); // d51c9a7e9353746a6020f9602d452929
// 对明文加盐进行3次md5加密
md5Hash = new Md5Hash(password, "username", 3);
System.out.println(md5Hash); // c69d335dc6b83db4ca13ee576672ddf7
下面我们测试在自定义的Realm中使用密文进行登陆验证
-
编写
shiro.ini
配置文件,配置匹配凭证器[main] # 定义凭证匹配器 credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher # 定义散列算法 credentialsMatcher.hashAlgorithmName=md5 # 定义散列迭代次数 credentialsMatcher.hashIterations=3 # 自定义realm myRealm=cn.maoritian.realm.MyRealm # 指定SecurityManager的realms实现 securityManager.realms=$myRealm
-
配置自定义Realm,模拟从数据库中查询密文密码,注意此时的密文密码应为原密码不加盐迭代三次的结果
package cn.maoritian.realm; public class MyRealm extends AuthorizingRealm { // 使用该方法返回值区分不同Realm public String getRealm() { return "MyRealm"; } // doGetAuthorizationInfo()进行授权操作 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } // doGetAuthenticationInfo()进行认证操作 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 从token中获取用户名 String username = (String) token.getPrincipal(); // 模拟根据用户名在数据库中查询密码 String password; if (!"user1".equals(username)) { return null; } else { // 此时返回的密文密码应为明文密码"password"不加盐迭代三次的结果 password = "918f43923202882e53f05b3f02cb68f1"; } // 返回登录比对信息 AuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName()); return info; } }
-
编写代码测试登录功能,测试代码与上面测试代码完全相同
@Test public void TestLoginWithCredential() { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("user1", "password1"); try { currentUser.login(token); System.out.println("当前登录状态" + currentUser.isAuthenticated()); } catch (UnknownAccountException uae) { System.out.println("username wasn't in the system"); } catch (IncorrectCredentialsException ice) { System.out.println("password didn't match"); } catch (LockedAccountException lae) { System.out.println("account for that username is locked"); } currentUser.logout(); System.out.println("当前登录状态" + currentUser.isAuthenticated()); }
Shiro入门实例2: 通过ini配置文件实现权限管理
BRAC权限管理
BRAC为基于角色的权限管理,有如下三个概念:
user
: 用户,指当前操作用户role
: 角色,是权限的集合permission
: 权限,对资源的操作许可
角色和权限的对应关系可以使用权限表达式来表示,其形式为资源:操作:实例
,示例如下:
权限表达式 | 意义 |
---|---|
user:create 或user:create:* | 对用户资源的创建权限 |
user:update:001 | 对用户实例001的修改权限 |
user:*:001 | 对用户实例001的所有权限 |
使用ini配置文件为用户授权
-
编写
shiro.ini
文件,为用户配置权限[users] # 模拟用户数据库列表: 账号=密码,权限 user1=password1,role1,role2 user2=password2,role3 [roles] # 角色role1对资源user具有所有权限 role1=user:* # 角色role2对资源user具有create,delete权限 role2=user:create,user:delete # 角色role3对资源user具有create权限 role3=user:create
-
编写测试代码如下:
@Test public void TestRoles() { // 使用user1登录系统 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("user1", "password1"); currentUser.login(token); // 判断当前用户是否拥有某角色 System.out.println(currentUser.hasRole("role1")); // 判断当前用户是否拥有列表中所有角色 System.out.println(currentUser.hasAllRoles(Arrays.asList("role1", "role2"))); // 以数组形式返回当前用户是否拥有某角色,此方法可以减少hasRole()方法的调用次数 System.out.println(Arrays.toString(currentUser.hasRoles(Arrays.asList("role1", "role2", "role3")))); // 若当前用户没有某角色则报UnauthorizedException异常 currentUser.checkRole("role1"); // 若当前用户没有所有角色则报UnauthorizedException异常 currentUser.checkRoles("role1", "role2", "role3"); }
@Test public void TestPermissions() { // 使用user1登录系统 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("user1", "password1"); currentUser.login(token); // 判断当前用户是否拥有某权限 System.out.println(currentUser.isPermitted("user:delete")); // 判断当前用户是否拥有列表中所有权限 System.out.println(currentUser.isPermittedAll("user:delete", "user:list")); // 以数组形式返回当前用户是否拥有某权限,此方法可以减少isPermitted()方法的调用次数 System.out.println(Arrays.toString(currentUser.isPermitted("user:delete", "user:list"))); }
使用自定义Realm为用户授权
-
实现自定义
Realm
,重写其doGetAuthorizationInfo()
方法public class MyRealm extends AuthorizingRealm { public String getRealm() { return "MyRealm"; } // doGetAuthorizationInfo()进行授权操作 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 获取用户名,用于查询角色权限信息 String username = (String) principals.getPrimaryPrincipal(); // 模拟根据用户名到数据库中查询角色和权限信息 List<String> roles = new ArrayList<String>(); //角色集合 List<String> permissions = new ArrayList<String>(); //权限集合 roles.add("role1"); permissions.add("user:*"); // 将角色和权限信息封装进infoAuthorizationInfo对象 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRoles(roles); info.addStringPermissions(permissions); return info; } // doGetAuthenticationInfo()进行认证操作 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 从数据库中查询所有用户名 } }
-
测试代码与上面完全相同,略
Shiro权限管理的执行流程
当我们调用Subject
对象的isPermitted()
或hasRole()
方法,会被委托给SecurityManager
,进而被委托给Authorizer
.Authorizer
在进行授权之前,会调用PermissionResolver
将字符串转化为相应的Permission
实例.若有多个Realm,会委托给ModularRealmAuthorizer
进行循环判断.