Shiro实战教程
1. 权限管理
1.1 什么是权限管理
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
1.2 什么是身份认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
1.3 什么是授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的
2. 什么是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.
Shiro是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的 API,您可以快速轻松地保护任何应用程序一从最小的移动应用程序到最大的wb和企业应用程序。
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
3. Shiro的核心架构
- Subject(主体):
- 主体代表了当前与应用程序交互的用户或系统。它可以是一个用户、一个服务、一个应用程序等。
- 主体通常包含了身份(identity)和相关的安全属性,如角色(roles)和权限(permissions)。
- 主体通常是在应用程序中执行操作和访问资源的实体。
- SecurityManager(安全管理器):
- 安全管理器是Shiro的核心组件,负责协调各种安全操作,包括身份验证和授权。
- 安全管理器协调主体(Subject)和相关的安全操作,确保它们遵循应用程序定义的策略和规则。
- Realm(领域):
- Realm 是用于验证和授权的数据源,它与用户身份、角色和权限相关的信息。
- 一个应用程序可以包含一个或多个Realm,每个Realm可以连接到不同的数据源,如数据库、LDAP、Active Directory等。
- Realm 实现了一组接口,包括验证(authentication)和授权(authorization)。
- Authentication(身份验证):
- 身份验证是验证用户提供的身份信息的过程,以确定其是否是合法用户。
- Shiro支持多种身份验证机制,包括用户名/密码验证、OAuth、JWT、LDAP等。
- Realm实现了身份验证过程,验证用户提供的凭证(credential)是否与存储在Realm中的凭证匹配。
- Authorization(授权):
- 授权是确定用户是否有权限执行某个操作或访问某个资源的过程。
- Shiro的授权基于角色和权限的概念。用户可以分配一个或多个角色,每个角色包含一组权限。
- Shiro提供了简单的编程式授权和基于注解的授权。
- Session Management(会话管理):
- Shiro提供了强大的会话管理功能,用于管理用户会话状态。
- 这包括会话的创建、销毁、过期、监听等。会话可以存储在内存、数据库或分布式缓存中,具体取决于应用程序的需求。
- **Cryptography (加密和哈希)**:
- Shiro包含密码服务,用于密码的安全存储和验证。它可以帮助应用程序开发者安全地处理密码,包括哈希、盐值、加密等。
- 这有助于防止密码泄露和保护用户的安全。
- SessionDAO(会话数据访问对象):
SessionDAO
是与数据存储相关的组件,负责将会话数据持久化到外部数据存储(例如数据库)以及从外部数据存储中检索会话数据。SessionManager
与SessionDAO
合作,通过SessionDAO
来实现会话数据的读取和写入。- Shiro提供了默认的
SessionDAO
实现,可以与多种后端数据存储系统(如数据库、缓存等)集成,同时也允许开发者自定义SessionDAO
以适应特定的需求。
- CacheManager(缓存管理器):
CacheManager
是 Shiro 中的组件,用于管理缓存,以提高访问控制、授权和其他安全操作的性能。- Shiro经常会执行频繁的权限检查,如果这些操作每次都要访问数据库或其他数据存储,会导致性能下降。通过缓存敏感数据,可以大大提高性能。
CacheManager
负责创建、管理和配置缓存实例。Shiro支持多种缓存提供程序,包括内存缓存、Ehcache、Redis等。- Shiro允许你在不同的部分使用不同的缓存,以适应应用程序的需求。例如,你可以将权限数据缓存在一个地方,会话数据缓存在另一个地方。
- 缓存管理器可以用于存储认证信息、授权信息、会话状态以及其他需要频繁访问和更新的数据。
4. Shiro中的认证
4.1 认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
4.2 Shiro中认证的关键对象
Shiro中认证的关键对象主要包括以下几个:
Subject(主体):
- Subject 代表当前与应用程序交互的用户或系统。它是进行认证和授权操作的主要对象。
- Subject 包含了用户的身份信息、角色和权限等关键属性。
其实就是访问系统的用户,主体可以是用户、程序等,进行认证的都称为主题。
Principal(主体的标识):
- Principal 是 Subject 的标识,通常是用户的标识信息,比如用户名、用户ID等。
- Principal 用于唯一标识主体,以便进行身份验证和授权。
Principal也可以称为身份信息,是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
Credential(凭证):
- Credential 是主体提供的身份验证信息,通常是密码或令牌。
- Credential 用于验证主体的身份。
是只有主体自己知道的安全信息,如密码、证书等。
Realm(领域):
- Realm 是用于验证和授权的数据源,包含用户的身份信息、角色和权限。
- Realm 实现了 Shiro 的 Realm 接口,用于与应用程序的数据存储进行交互。
这些对象一起协作以执行身份验证过程。Subject 提供了主体的身份信息和凭证,Realm 用于验证这些信息。Shiro允许应用程序使用多个 Realm 来支持不同的数据源或身份验证机制。
4.3 认证流程
通常的认证流程包括:
- Subject 提供主体的身份信息和凭证。
- Subject 将凭证传递给相应的 Realm 进行验证。也可以在传递之前,先打包为一个token,之后传递token。
- Realm 验证凭证是否正确,如果正确则返回主体的信息。
- 主体信息被返回给 Subject,Subject 成功通过认证。
Shiro的身份验证机制是高度可定制的,可以根据应用程序的需求和安全策略来配置不同的 Realm,以及使用不同的凭证和主体信息。这使得Shiro非常灵活,适用于各种身份验证场景。
4.4 认证的开发
创建项目并引入依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency>
可以直接在Maven仓库中搜索。
引入Shiro配置文件并加入如下配置
[users] #不可改变,必须为users #下面的数据格式为:账号=密码 zhangsan=123 lisi=456
细心的你可能会发现这里的配置文件并不是我们常用的yml、properties;而是ini,这样设计的初衷是Shiro想让你的学习成本变得更低,将数据库中 的数据都写死在ini文件中。
简单实现
@Test void contextLoads() { //1. 获取SecurityManager(安全管理器对象)具体的实现类 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //2. 给安全管理器设置realm securityManager.setRealm(new IniRealm("classpath:Shiro.ini")); //3.SecurityUtils 给全局工具类设置安全管理器 SecurityUtils.setSecurityManager(securityManager); //4. 使用SecurityUtils可以来获取Subject主体 Subject subject = SecurityUtils.getSubject(); //5. 创建令牌(如果这里的值不是ini中的,那么就会出现异常) UsernamePasswordToken token = new UsernamePasswordToken("lisi","456"); //用户认证 try { System.out.println("认证状态:"+subject.isAuthenticated()); subject.login(token); System.out.println("认证状态:"+subject.isAuthenticated()); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("认证失败,用户名不存在"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("认证失败,密码错误"); } }
执行结果:
认证状态:false 2023-10-16 15:19:55.925 INFO 21444 --- [ main] a.s.s.m.AbstractValidatingSessionManager : Enabling session validation scheduler... 认证状态:true
如果我现在验证一个不存在的账号信息。
修改:
UsernamePasswordToken token = new UsernamePasswordToken("hhh","456");
4.5 使用自定义Realm
在你查看了我上面写的认证开发的代码之后,你一定会有这样的疑问:我们上面的数据都是死数据,那么我后期如果想要从数据库中取出来做认证,该怎么使用呢?
所以,为了能够和数据库进行交互,我们需要自定义Reaml来从数据库中获取Principal
:
package com.nxz.gateway.realm;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @author 念心卓
* @version 1.0
* @description: 自定义Realm
* @date 2023/10/16 16:07
*/
public class CustomRealm extends AuthorizingRealm {
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.1 获取用户输入的用户名(账号) 获取方式一
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
System.out.println("获取到的用户名:" + username);
//1.2 获取方式二
String principal = (String) authenticationToken.getPrincipal();
System.out.println("获取到的principal:" + principal);
//2. 根据用户名查询数据库
//3. 将查询到的用户封装为认证信息(这里我使用死数据模拟)
if (!"lisi".equals(username)) {
throw new UnknownAccountException("账户不存在");
}
return new SimpleAuthenticationInfo(principal, "456", this.getName());
}
}
其实这里也可以继承
AuthenticatingRealm
,只不过它没有doGetAuthorizationInfo
方法,做不了授权了,所以,一般我们同时要做认证和授权的时候,都是继承AuthorizingRealm
,它里面既可以做认证,也可以做授权。并且细心的你可以发现,获取用户的账号有两种方式来获取,二者获取到的数据都是一样的。
由于还没有配置数据库,所以第二步中根据用户名查询数据库中我就没做,一般有数据库了我们使用下面的方式:
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 1.获取用户输入的用户名 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); // 2.根据用户名查询用户 QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username",username); Users users = usersMapper.selectOne(wrapper); // 3.将查询到的用户封装为认证信息 if (users == null) { throw new UnknownAccountException("账户不存在"); } /** * 参数1:用户 * 参数2:密码 * 参数3:Realm名 */ return new SimpleAuthenticationInfo(users,users.getPassword(),"myRealm"); }
最后的返回你也可以发现,我们一般都是返回SimpleAuthenticationInfo,并且里面的参数:
- 获取到的用户名
- 填入该用户名对应数据库中的密码
- 这个自定义的Realm的名称,你可以使用
this.getName()
的方式来直接获取
测试改造:
@Test
void contextLoads() {
//1. 获取SecurityManager(安全管理器对象)具体的实现类
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2. 给安全管理器设置realm
securityManager.setRealm(new CustomRealm());//这里就使用自定义的Realm即可
//3.SecurityUtils 给全局工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//4. 使用SecurityUtils可以来获取Subject主体
Subject subject = SecurityUtils.getSubject();
//5. 创建令牌(如果这里的值不是ini中的,那么就会出现异常)
UsernamePasswordToken token = new UsernamePasswordToken("lisi","456");
//用户认证
try {
System.out.println("认证状态:"+subject.isAuthenticated());
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("认证失败,用户名不存在");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败,密码错误");
}
}
执行结果:
认证状态:false
获取到的用户名:lisi
获取到的principal:lisi
2023-10-16 16:22:24.512 INFO 22440 --- [ main] a.s.s.m.AbstractValidatingSessionManager : Enabling session validation scheduler...
认证状态:true
简单修改一下:
UsernamePasswordToken token = new UsernamePasswordToken("wangwu","456");
执行结果:
4.6 Shiro配合MD5+Salt
MD5(Message Digest Algorithm 5)是一种广泛用于加密和数据完整性校验的散列函数(hash function)。它是Ron Rivest于1991年设计的,常用于生成数据的摘要或校验和,以便验证数据在传输过程中是否发生了改变。下面是关于MD5的详细信息:
算法原理:
MD5算法通过将输入数据(消息)转换成一个固定长度的散列值(通常是128位,即16字节)来工作。这个散列值是一个在理论上唯一的标识符,代表了原始数据的内容。应用领域:
- 数据完整性校验:MD5常用于验证数据在传输过程中是否被篡改。发送方在传输数据前,计算数据的MD5散列值并发送给接收方,接收方接收数据后重新计算散列值,如果两者匹配,则数据完整性得到验证。
- 加密:虽然MD5主要用于生成散列值,但它也可用于简单的数据加密,如密码存储。然而,由于MD5易受到碰撞攻击,不再被视为安全的加密方法。
- 数据校验和:MD5可用于校验文件完整性。计算文件的MD5散列值并与预先存储的值进行比较,以检查文件是否被修改。
特点:
- 快速计算:MD5算法执行速度较快,适合用于大量数据的散列计算。
- 固定长度输出:MD5的输出长度是固定的,无论输入数据大小如何。
- 输入不敏感:MD5会对输入数据进行散列处理,即使输入数据仅有微小的改变,散列值也会完全不同。
安全性问题:
- MD5算法存在安全性问题。已经发现许多碰撞(不同输入数据生成相同的MD5散列值)漏洞,因此不再建议将MD5用于加密密码或其他敏感数据。
- 由于MD5的散列空间有限(128位),暴力破解MD5散列值变得相对容易,特别是当使用弱密码时。
替代方案:
由于MD5的安全性问题,推荐使用更强大的散列算法,如SHA-256、SHA-3、或bcrypt等,以提高密码安全性和数据完整性验证的安全性。
这里我就采用MD5+Salt的方式来加强安全性,注意:由于MD5本身的不安全,就算你采用了加盐的方式,它也是不安全的,只是相对安全点。
虽然MD5存在安全性问题,但如果你仍然希望使用MD5,加盐可以提高安全性,尤其是对于密码存储来说。加盐是一种常见的方法,可防止相同的密码生成相同的MD5散列值,从而增加密码的安全性。以下是关于如何使用MD5和盐来提高安全性的注意事项:
加盐(Salting):
- 加盐是在原始密码之前或之后添加一个随机生成的字符串,然后再对整个字符串进行MD5哈希。
- 这个随机的盐值是与用户相关联的,通常会存储在数据库中。每个用户的盐值都应该是唯一的。
- 盐的引入使得即使两个用户使用相同的密码,由于盐的不同,它们的MD5散列值也将不同。
唯一盐:
- 盐值必须是唯一的,每个用户应该拥有不同的盐值。
- 盐值的唯一性可以通过随机生成的盐值或使用用户特定的信息(如用户名或邮箱地址)与密码混合来实现。
存储盐值:
- 盐值通常存储在数据库中,与用户相关联。这样,当验证密码时,你可以检索用户的盐值并使用它来重新计算密码的MD5散列值以进行比较。
- 盐值的存储应当与密码散列值分开,以提高安全性。
复杂密码策略:
- 使用复杂的密码策略,以确保用户使用强密码。
- 强密码策略可能包括密码长度要求、包含大写字母、小写字母、数字和特殊字符等。
尽管加盐可以提高密码的安全性,但需要注意的是,MD5本身仍然存在一些不可逆的缺陷,例如其速度很快,容易受到暴力破解和彩虹表攻击。因此,推荐使用更安全的散列算法,如SHA-256或bcrypt,特别是对于安全性要求高的应用程序。
总之,如果选择使用MD5,确保使用加盐的方式,并采取适当的措施来提高密码的安全性。然而,从长远来看,使用更强大的密码哈希算法是更好的选择。
将密码进行MD5加密:
@Test
void md5AndSalt(){
Md5Hash securityPwd1 = new Md5Hash("456");
System.out.println(securityPwd1);
}
执行结果:
250cf8b51c773f3f8dc8b4be867a9a02
导入的包是Shiro的:
import org.apache.shiro.crypto.hash.Md5Hash;
注意:
使用
Shiro
的MD5
加密,只能通过构造器的方式才能够加密,如上代码。
将密码进行MD5+Salt加密:
@Test
void md5AndSalt(){
Md5Hash securityPwd1 = new Md5Hash("456","Xsodj*#$sada");
System.out.println(securityPwd1);
}
构造器第二个参数就是随机盐
执行结果:
a77163b85ce901eca04a00eefda51cd1
将密码进行MD5+Salt+散列次数加密:
一般我们在使用了Shiro的MD5+Salt之后,还会加上一个1024的散列次数,让散列更加平均,更加安全:
@Test
void md5AndSalt(){
Md5Hash securityPwd1 = new Md5Hash("456","Xsodj*#$sada",1024);
System.out.println(securityPwd1);
}
执行结果:
4c515588eaebd33042445c5d5a887d6d
现在我们了解了使用Shiro来加密的Md5Hash,那么我们来实际看看:
仅仅使用MD5加密:
public class CustomMD5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取用户名
String principal = (String) authenticationToken.getPrincipal();
//根据用户名查数据库,这里写死数据
if ("lisi".equals(principal)){
return new SimpleAuthenticationInfo(principal,"250cf8b51c773f3f8dc8b4be867a9a02",this.getName());
}
return null;
}
}
这里的密码使用的是456加密过后的密码
@Test
void contextLoads() {
//1. 获取SecurityManager(安全管理器对象)具体的实现类
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2. 使用hash凭证匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//3. 设置算法为md5算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//4. 注入自定义realm
CustomMD5Realm realm = new CustomMD5Realm();
//5. 设置realm使用hash凭证匹配器
realm.setCredentialsMatcher(hashedCredentialsMatcher);
//6. 给安全管理器设置realm
securityManager.setRealm(realm);
//7.SecurityUtils 给全局工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//8. 使用SecurityUtils可以来获取Subject主体
Subject subject = SecurityUtils.getSubject();
//9. 创建令牌(如果这里的值不是ini中的,那么就会出现异常)
UsernamePasswordToken token = new UsernamePasswordToken("lisi","456");
//用户认证
try {
System.out.println("认证状态:"+subject.isAuthenticated());
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("认证失败,用户名不存在");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败,密码错误");
}
}
执行结果:
认证状态:false
2023-10-16 18:04:04.741 INFO 6420 --- [ main] a.s.s.m.AbstractValidatingSessionManager : Enabling session validation scheduler...
认证状态:true
可见认证成功。
使用MD5+Salt加密:
修改部分:
if ("lisi".equals(principal)){
return new SimpleAuthenticationInfo(
principal,
"a77163b85ce901eca04a00eefda51cd1",
ByteSource.Util.bytes("Xsodj*#$sada"), //这是使用
this.getName());
}
执行结果通过。
使用MD5+Salt+随机散列加密:
hashedCredentialsMatcher.setHashIterations(1024);//设置散列次数
执行结果通过。
5. Shiro中的授权
5.1 授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
5.2 关键对象
授权可简单理解为who对what(which)进行How操作:
Who,即主体(Subject)
,主体需要访问系统中的资源。
What,即资源(Resource)
,如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型
和资源实例
,比如商品信息为资源类型
,类型为t01的商品为资源实例
,编号为001的商品信息也属于资源实例。
How,权限/许可(Permission)
,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
5.3 授权流程
必须是认证之后,才能够授权,否则授权就没有意义。
5.4 授权方式
基于角色的访问控制
RBAC
基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制if(subject.hasRole("admin")){ //操作什么资源 }
基于资源的访问控制
RBAC
基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制if(subject.isPermission("user:update:01")){ //资源实例 //对user下面的01资源实例有更新权限 } if(subject.isPermission("user:update:*")){ //资源类型 //对user下面的所有资源有更新权限 }
5.5 权限字符串
权限字符串的规则是:资源标识符:操作:资源实例标识等,意思是对哪个资源的哪个实例具有什么操作,:
是资源/操作/实例的分割符,权限字符串也可以使用*
通配符。
例子:
- 用户创建权限:
user:create
或user:create:*
- 用户修改实例001的权限:
user:update:001
- 用户实例001的所有权限:
user:*:001
5.6 Shiro中授权编程实现方式
编程式
Subject subject = SecurityUtils.getSubject(); if(subject.hasRole("admin")){ //有权限 }else{ //无权限 }
注解式
@RequireRoles("admin") public void hello(){ //有权限 }
标签式
<!--JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:--> <shiro:hasRole name="admin"> <!--有权限--> </shiro:hasRole> <!--注意Thymeleaf中使用shiro需要额外集成-->
现在基本上不用
JSP
了
5.7 授权的开发
基于角色的访问控制:
基于认证中的代码,添加如下代码:
//认证过后,就开始进行授权
System.out.println("==================授权==================");
if (subject.isAuthenticated()){
System.out.println("该主体是否具有admin权限:"+subject.hasRole("admin"));
System.out.println("该主体是否具有其中一个权限:"+ Arrays.toString(subject.hasRoles(Arrays.asList("admin", "user"))));
System.out.println("该主体是否同时具有admin和user权限:"+subject.hasAllRoles(Arrays.asList("admin", "user")));
}
自定义Realm:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取身份信息 用户名
//这里是获取的主要的身份信息,因为一个主体可以有多个身份
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
System.out.println("获取到的主要身份信息为:"+primaryPrincipal);
//根据获取到的身份信息查询数据库,例如 ;身份信息:lisi; 权限:admin
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//为当前用户添加admin权限
simpleAuthorizationInfo.addRole("admin"); //addRoles是设置多个权限
return simpleAuthorizationInfo;
}
执行结果:
登陆成功
==================授权==================
获取到的主要身份信息为:lisi
该主体是否具有admin权限:true
获取到的主要身份信息为:lisi
获取到的主要身份信息为:lisi
该主体是否具有其中一个权限:[true, false]
获取到的主要身份信息为:lisi
获取到的主要身份信息为:lisi
该主体是否同时具有admin和user权限:false
基于资源的访问控制:
基于认证中的代码,添加如下代码:
//认证过后,就开始进行授权
System.out.println("==================授权==================");
if (subject.isAuthenticated()){
System.out.println("是否对admin下面的所有资源有update权限:"+subject.isPermitted("admin:update"));
System.out.println("是否对user下面的所有资源有update和create权限(同时具有为true):"+subject.isPermittedAll("user:update","user:create"));
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取身份信息 用户名
//这里是获取的主要的身份信息,因为一个主体可以有多个身份
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
System.out.println("获取到的主要身份信息为:"+primaryPrincipal);
//根据获取到的身份信息查询数据库,例如 ;身份信息:lisi; 权限:admin
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//为当前用户添加能够访问admin下面所有资源的创建权限
simpleAuthorizationInfo.addStringPermission("admin:create");
//为当前用户添加能够访问user下面所有资源的update和create权限
simpleAuthorizationInfo.addStringPermissions(Arrays.asList("user:update","user:create"));
return simpleAuthorizationInfo;
}
执行结果:
登陆成功
==================授权==================
获取到的主要身份信息为:lisi
是否对admin下面的所有资源有update权限:false
获取到的主要身份信息为:lisi
获取到的主要身份信息为:lisi
是否对admin下面的所有资源有update权限:true
注意:当进行权限判断的时候,如果一个用户有多个权限,那么
doGetAuthorizationInfo
方法就会执行多次,你查看上面的执行结果也能够看出。
6. 整合SpringBoot项目实战
6.1 整合思路
6.2 引入依赖
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
注意:一定要有web依赖,如果没有web的依赖,那么自定义的shiroFilter就无法加载到tomca中,从而报错:
Caused by: java.lang.NoClassDefFoundError: javax/servlet/Filter
。
6.3 自定义Realm
package com.nxz.gateway.realm;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @author 念心卓
* @version 1.0
* @description: 集成boot的自定义Realm
* @date 2023/10/18 22:30
*/
public class IntegrationBootCustomRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
System.out.println("获取到的principal:" + principal);
//2. 根据用户名查询数据库
//3. 将查询到的用户封装为认证信息(这里我使用死数据模拟)
if (!"lisi".equals(principal)) {
throw new UnknownAccountException("账户不存在");
}
return new SimpleAuthenticationInfo(principal, "456", this.getName());
}
}
6.4 编写Shiro配置类
package com.nxz.gateway.config;
import com.nxz.gateway.realm.IntegrationBootCustomRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
/**
* @author 念心卓
* @version 1.0
* @description: Shiro配置类
* @date 2023/10/18 21:34
*/
@Configuration
public class ShiroConfig {
/**
* 第一步,创建自定义Realm
* @return
*/
@Bean
public IntegrationBootCustomRealm integrationBootCustomRealm(){
return new IntegrationBootCustomRealm();
}
/**
* 第二步,创建安全管理器,这是Shiro的核心部分,这里必须是使用默认的安全管理器
* @param integrationBootCustomRealm 通过依赖注入IntegrationBootCustomRealm
* @return
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(IntegrationBootCustomRealm integrationBootCustomRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//给安全管理器设置自定义Realm
manager.setRealm(integrationBootCustomRealm);
return manager;
}
/**
* 第三步,创建Shiro过滤器工场Bean,要拦截所有请求,必须要用过滤器
* @param defaultWebSecurityManager 通过依赖注入DefaultWebSecurityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilter.setSecurityManager(defaultWebSecurityManager);
HashMap<String, String> hashMap = new HashMap<>();
//认证设置,默认情况下未认证会跳到登陆页面
hashMap.put("/index/*","authc");//表示index下面的所有资源均需要认证
hashMap.put("/user/login","anon");//表示/user/login资源无需认证
//授权设置,默认情况下未授权会跳到未授权页面
hashMap.put("/user/create","perms[user:create]");//表示/user/create资源必须要有user:create权限才能够访问
shiroFilter.setFilterChainDefinitionMap(hashMap);
//设置未认证,则跳转到登陆页面
shiroFilter.setLoginUrl("/user/login");
//设置未授权,则跳转至未授权页面
shiroFilter.setUnauthorizedUrl("/user/authorized");
return shiroFilter;
}
}
认证过滤器
- anon:无需认证。
- authc:必须认证。
- authcBasic:需要通过HTTPBasic认证。
user
:不一定通过认证,只要曾经被Shiro记录即可,比如:记住我。授权过滤器
- perms:必须拥有某个权限才能访问。
- role:必须拥有某个角色才能访问。
- port:请求的端口必须是指定值才可以。
- rest:请求必须基于RESTful,POST,PUT,GET,DELETE。
- ssl:必须是安全的URL请求,协议HTTP。
控制器:
@RestController
@RequestMapping("/index")
public class IndexController {
@GetMapping("/hello")
public String hello(){
return "欢迎访问首页";
}
}
@RestController
@RequestMapping("/user/")
public class UserController {
@GetMapping("login")
public String login(){
return "请登录";
}
@PostMapping("create")
public String createUser(){
return "用户创建成功";
}
@GetMapping("authorized")
public String authorized(){
return "请授予用户权限";
}
@GetMapping("hello")
public String hello(){
return "user hello";
}
}
6.5 测试
当我执行/index/hello的时候,由于没有认证,所有需要登陆
由于我没有给user/hello配置认证或者授权,所以可以访问
当我访问未授权的资源时,由于我没有认证,所以直接要求你先认证。
6.6 常见过滤器
Shiro中提供了多个默认的过滤器,我们可以使用这些过滤器来配置控制指定url的权限:
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定URL可以匿名访问,即无需认证也可以访问的资源 |
authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取 username 、 password ,rememberMe 等参数并尝试登录,如果登录不了就会跳转到loginUrl 配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定URL需要Basic登陆,需要通过HTTPBasic 认证。 |
logout | LogoutFilter | 登出过滤器,配置指定URL就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定授权才能够访问 |
port | PortFilter | 需要指定端口才能够访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要HTTPS请求才能够访问 |
user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
6.7 认证和退出
6.7.1 认证
在之前的代码中,我们如果没有认证就会被要求认证之后才能够访问资源,所以,我们开始来做认证:
@PostMapping("login")
public String login(String username,String password){
//因为你在Config配置了SecurityManager,所以这里就无需再
// SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
//开启授权
try{
subject.login(new UsernamePasswordToken(username,password));
return "认证成功";
}catch (UnknownAccountException | IncorrectCredentialsException e){
log.info("账号或密码错误");
e.printStackTrace();
}
return "认证失败";
}
自定义的Realm:
/**
* @author 念心卓
* @version 1.0
* @description: 集成boot的自定义Realm
* @date 2023/10/18 22:30
*/
@Slf4j
public class IntegrationBootCustomRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("开始认证");
String principal = (String) authenticationToken.getPrincipal();
System.out.println("获取到的principal:" + principal);
//2. 根据用户名查询数据库
//3. 将查询到的用户封装为认证信息(这里我使用死数据模拟)
if (!"lisi".equals(principal)) {
throw new UnknownAccountException("账户不存在");
}
return new SimpleAuthenticationInfo(principal, "456", this.getName());
}
}
可见我的认证的数据是写死了的,只有账号为:lisi;密码为:456;才能够认证成功
Shiro配置类不变
请求发送:
由于我认证的信息写死了,只能够认证账号为:lisi;密码为:456的才能够认证成功。
由于之前我们发现,当我们没有认证的时候访问资源,系统首先会要求你认证,现在我们认证成功了,再去访问资源看看:
注意,必须再同一个session中才有效,如果你是用Postman来认证的,然后使用浏览器来访问受限资源,依然会要求你认证之后才能够访问,并且,如果你的服务器重启之后,那么你也是需要重新认证的。
现在需要认证的资源能够访问,我们再来访问需要授权的资源试试:
之前没有认证的时候,访问授权的资源,它要求你必须先认证之后才能够访问,现在我们认证之后在访问,它发现你虽然认证了,但是没有授权,所以它会要求你授权
6.7.2 退出
你可以认证,那么你就可以退出,当你退出之后,下次访问资源的时候又会要求你重新认证。
修改代码:
@PostMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "退出成功~";
}
退出成功之后,我们看看是否能够访问资源:
所以,当你退出之后,就无法再访问资源了,必须要重新认证才能够访问。
6.8 基于MD5+Salt的注册功能
首先创建一张用户表:
引入依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
控制器:
@PostMapping("register")
public String register(@RequestBody User user) {
return userService.register(user) ? "注册成功" : "注册失败";
}
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Override
public boolean register(User user) {
String password = user.getPassword();
//创建随机盐
String salt = IdUtil.simpleUUID();
log.info("随即盐:{}", salt);
//加密密码:基于MD5+Salt
Md5Hash md5Hash = new Md5Hash(password, salt, 1024);
log.info("加密过后密码:{}", md5Hash);
user.setSalt(salt);
user.setPassword(md5Hash.toString());
return this.save(user);
}
}
执行结果:
6.9 基于MD5+Salt的认证功能
修改自定义的Realm,将数据从数据库中加载出来:
@Slf4j
public class IntegrationBootCustomRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("开始认证");
String principal = (String) authenticationToken.getPrincipal();
log.info("获取到的principal:{}" + principal);
//2. 根据用户名查询数据库
User user = userService.getUserByName(principal);
if (user == null){
throw new UnknownAccountException("用户不存在");
}
return new SimpleAuthenticationInfo(
principal,
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), //随机盐
this.getName());
}
}
@Override
public User getUserByName(String principal) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",principal);
User user = getOne(wrapper);
if (ObjectUtil.isEmpty(user)){
return null;
}
return user;
}
修改配置类:
@Bean
public IntegrationBootCustomRealm integrationBootCustomRealm(){
//设置凭证匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher("md5");
//设置散列次数(注册时散列了多少次,这里就要散列多少次)
hashedCredentialsMatcher.setHashIterations(1024);
IntegrationBootCustomRealm integrationBootCustomRealm = new IntegrationBootCustomRealm();
integrationBootCustomRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return integrationBootCustomRealm;
}
之前的账号:念心卓;密码:123456
认证结果:
访问受限资源:
6.10 授权
6.10.1 基于角色的访问控制
修改自定义Realm:
@Slf4j
public class IntegrationBootCustomRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取主体
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
//根据主体来查询数据库
User user = userService.getUserByName(primaryPrincipal);
if (ObjectUtil.isEmpty(user)){
return null;
}
//获取到用户权限
String roles = user.getRoles();
//数据库中权限字段是字符串,每个权限使用:','分割
String[] roleArray = roles.split(",");
//有什么权限就向shiro中添加权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<String> roleCollection = Arrays.stream(roleArray).collect(Collectors.toList());
//授予权限
simpleAuthorizationInfo.addRoles(roleCollection);
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("开始认证");
String principal = (String) authenticationToken.getPrincipal();
log.info("获取到的principal:{}" + principal);
//2. 根据用户名查询数据库
User user = userService.getUserByName(principal);
if (user == null){
throw new UnknownAccountException("用户不存在");
}
return new SimpleAuthenticationInfo(
principal,
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), //随机盐
this.getName());
}
}
控制器:
@GetMapping("test1")
@RequiresRoles("admin")
public String test1(){
return "具有admin权限的可以访问:成功!";
}
我使用
@RequiresRoles
注解来控制角色权限,只有具有这个权限的用户才能够访问这个方法。
我这里依旧是使用的:账号:念心卓;密码:123456,具有admin权限。
简单修改一下方法的访问权限:
@GetMapping("test2")
@RequiresRoles(value = {"admin","user"})
public String test2(){
return "同时具有admin和user权限的可以访问:成功!";
}
解释:
因为你的念心卓用户只有
admin
权限,没有user
角色权限,而@RequiresRoles
注解,只要里面的内容为数组,那么就代表要拥有value的所有权限,就比如这里你要拥有admin
和user
权限才能够访问。
6.10.2 基于资源的访问控制
修改自定义Realm中的授权规则:
//基于资源的授权
simpleAuthorizationInfo.addStringPermission("user:*:*");
控制器:
@GetMapping("test3")
@RequiresPermissions("user:update:*")
public String test3(){
return "具有user下的所有资源的更新的权限的可以访问:成功!";
}
@GetMapping("test4")
@RequiresPermissions("admin:update:*")
public String test4(){
return "具有admin下的所有资源的更新的权限的可以访问:成功!";
}
6.10.3 总结
其实在一般的时间库表的时候,我们一般会这样设计:
一个用户对应多个角色,一个角色也可以对应多个用户——用户和角色属多对多
一个角色对应多个权限,一个权限也可以属于多个角色——角色和权限属多对多
6.11 Shiro整合Redis
首先引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.1.0</version>
</dependency>
修改Shiro配置类:
//开启缓存管理
integrationBootCustomRealm.setCacheManager(new CustomRedisCacheManager(redisTemplate));
// 开启全局缓存
integrationBootCustomRealm.setCachingEnabled(true);
// 开启认证缓存,并命名(真实的认证缓存名为cacheName)
integrationBootCustomRealm.setAuthenticationCachingEnabled(true);
integrationBootCustomRealm.setAuthenticationCacheName("authenticationCache");
// 开启授权缓存,并命名(真实的授权缓存名为完整包名+cacheName)
integrationBootCustomRealm.setAuthorizationCachingEnabled(true);
integrationBootCustomRealm.setAuthorizationCacheName("authorizationCache");
Redis配置类:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//设置String类型的key设置序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置Hash类型的key设置序列化器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//设置redis链接Lettuce工厂
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
return redisTemplate;
}
}
必须要配置
LettuceConnectionFactory
,否则启动报错。这里为啥使用RedisTemplate而不使用StringRedisTemplate,因为我们不能让value被序列化为String,否则后面Shiro查找缓存的时候会导致类转换异常。
CustomRedisCacheManager:
public class CustomRedisCacheManager implements CacheManager{
private RedisTemplate redisTemplate;
public CustomRedisCacheManager(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 每次执行缓存时,都会调用该方法,自动注入s
* @param s 缓存的名称
* @return
* @param <K>
* @param <V>
* @throws CacheException
*/
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
// 自动去RedisCache中找具体实现
return new RedisCache<K, V>(s,redisTemplate);
}
}
RedisCache:
@Slf4j
public class RedisCache<K,V> implements Cache<K,V> {
private String cacheName;
private RedisTemplate redisTemplate;
private ObjectMapper objectMapper = new ObjectMapper();
public RedisCache(String cacheName,RedisTemplate redisTemplate) {
this.cacheName = cacheName;
this.redisTemplate = redisTemplate;
}
public RedisCache() {
}
@Override
public V get(K k) throws CacheException {
V v = (V) redisTemplate.opsForHash().get(this.cacheName, k.toString());
log.info("获取到的Value为{}",v);
return v;
}
@Override
public V put(K k, V v) throws CacheException {
log.info("需要存放的Key为{},Value为{}",k,v);
redisTemplate.opsForHash().put(this.cacheName,k.toString(),v);
return null;
}
//退出认证的时候执行
@Override
public V remove(K k) throws CacheException {
return (V) redisTemplate.opsForHash().delete(this.cacheName,k.toString());
}
//当集合中没有元素的时候就会执行
@Override
public void clear() throws CacheException {
redisTemplate.delete(this.cacheName);
}
@Override
public int size() {
return redisTemplate.opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<K> keys() {
return (Set<K>) redisTemplate.opsForHash().keys(this.cacheName);
}
@Override
public Collection<V> values() {
return (Collection<V>) redisTemplate.opsForHash().values(this.cacheName);
}
}
注意:上诉的Key我都转为了String,方便后面查看
这里还有一个序列化错误,就是我们之前自定义Realm的时候,使用的序列化为:
可见里面的Util静态内部类并没有序列化,会导致我们的Salt无法序列化,所以我们自己要自定义一个类来序列化:
MyByteSource:
public class MyByteSource extends SimpleByteSource implements Serializable {
public MyByteSource(String s) {
super(s);
}
}
MyByteSource既集成了SimpleByteSource的功能,又能够序列化。
测试:
可见,做授权的时候是查询了数据库的,所以我们现在就是要测试,当前用户访问test1资源,第一次需要去数据库查询权限,后面应该就不该在走数据库了。
现在我再访问一次:
由此,Redis整合Shiro完毕。