一、 权限概述
1. 什么是权限
权限管理,一般指根据系统设置的安全策略或者安全规则,用户可以访问而且只能访问自己被授权的资源,不多不少。权限管理几乎出现在任何系统里面,只要有用户和密码的系统。
权限管理在系统中一般分为:
- 访问权限
一般表示你能做什么样的操作,或者能够访问那些资源。例如:给张三赋予“店铺主管”角色,“店铺主管”具有“查询员工”、“添加员工”、“修改员工”和“删除员工”权限。此时张三能够进入系统,则可以进行这些操作。 - 数据权限
一般表示某些数据是否属于你,或者属于你可以操作范围。例如:张三是"店铺主管"角色,他可以看他手下客服人员所有的服务的买家订单信息,他的手下只能看自己负责的订单信息。
2. 认证概念
⑴ 什么是认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和密码,看其是否与系统中存储的该用户的用户名和密码一致,来判断用户身份是否正确。例如:密码登录,手机短信验证、三方授权等。
⑵ 认证流程
⑶ 关键对象
上边的流程图中需要理解以下关键对象:
Subject:主体:访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
Principal:身份信息是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
credential:凭证信息:是只有主体自己知道的安全信息,如密码、证书等。
3. 授权概念
⑴ 什么是授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后,系统会为其分配对应的权限,当访问资源时,会校验其是否有访问此资源的权限。
这里首先理解4个对象。
- 用户对象user:当前操作的用户、程序。
- 资源对象resource:当前被访问的对象
- 角色对象role :一组 “权限操作许可权” 的集合。
- 权限对象permission:权限操作许可权
⑵ 授权流程
⑶ 关键对象
授权可简单理解为who对what进行How操作
Who: 主体(Subject),可以是一个用户、也可以是一个程序
What: 资源(Resource),如系统菜单、页面、按钮、方法、系统商品信息等。
访问类型:商品菜单,订单菜单、分销商菜单
数据类型:我的商品,我的订单,我的评价
How: 权限/许可(Permission)
我的商品(资源)—>访问我的商品(权限许可)
分销商菜单(资源)----> 访问分销商列表(权限许可)
二、 Shiro概述
1. Shiro简介
⑴ 什么是Shiro?
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
⑵ Shiro 的特点
Shiro 是一个强大而灵活的开源安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密。如下是它所具有的特点:
- 易于理解的 Java Security API;
- 简单的身份认证(登录),支持多种数据源(LDAP,JDBC 等);
- 对角色的简单的签权(访问控制),也支持细粒度的鉴权;
- 支持一级缓存,以提升应用程序的性能;
- 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
- 异构客户端会话访问;
- 非常简单的加密 API;
- 不跟任何的框架或者容器捆绑,可以独立运行。
⑶ 核心组件
1) Shiro架构图
2) Subject
Subject主体,外部应用与subject进行交互,subject将用户作为当前操作的主体,这个主体:可以是一个通过浏览器请求的用户,也可能是一个运行的程序。Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权。
3) SecurityManager
SecurityManager权限管理器,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
4) Authenticator
Authenticator即认证器,对用户登录时进行身份认证
5) Authorizer
Authorizer授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
6) Realm(数据库读取+认证功能+授权功能实现)
Realm领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据
比如:
如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
注意:
不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
7) SessionManager
SessionManager会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
8) SessionDAO
SessionDAO即会话dao,是对session会话操作的一套接口
比如:
可以通过jdbc将会话存储到数据库
也可以把session存储到缓存服务器
9) CacheManager
CacheManager缓存管理,将用户权限数据存储在缓存,这样可以提高性能
10) Cryptography
Cryptography密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能
三、 Shiro入门
1. 身份认证
⑴ 基本流程
流程如下:
1、Shiro把用户的数据封装成标识token,token一般封装着用户名,密码等信息
2、使用Subject门面获取到封装着用户的数据的标识token
3、Subject把标识token交给SecurityManager,在SecurityManager安全中心中,SecurityManager把标识token委托给认证器Authenticator进行身份验证。认证器的作用一般是用来指定如何验证,它规定本次认证用到哪些Realm
4、认证器Authenticator将传入的标识token,与数据源Realm对比,验证token是否合法
⑵ 案例演示
1) 需求
使用shiro完成一个用户的登录
2) 实现
① 新建项目
shiro-study-project
② 导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kejizhentan</groupId>
<artifactId>shiro-study-project</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- compiler插件, 设定JDK版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
</plugins>
</build>
</project>
③ 编写shiro.ini
#声明用户账号
[users]
zhangsan=123456
④ 编写HelloShiro
package com.kejizhentan;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
public class HelloShiro {
public static void main(String[] args) {
//导入权限ini文件构建权限工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//工厂构建安全管理器
SecurityManager securityManager = factory.getInstance();
//使用SecurityUtils工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建账号token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("zhangsan", "123456");
//登录操作
subject.login(usernamePasswordToken);
System.out.println("是否登录成功:" + subject.isAuthenticated());
}
}
⑤ 测试
3) 小结
1、权限定义:ini文件
2、加载过程:
导入权限ini文件构建权限工厂 工厂构建安全管理器
使用SecurityUtils工具生效安全管理器 使用SecurityUtils工具获得主体
使构建账号token用SecurityUtils工具获得主体 构建账号token 登录操作
2. Realm
⑴ Realm接口
所以,一般在真实的项目中,我们不会直接实现Realm接口,我们一般的情况就是直接继承AuthorizingRealm,能够继承到认证与授权功能。它需要强制重写两个方法
public class DefinitionRealm extends AuthorizingRealm {
/**
* @Description 认证
* @param authcToken token对象
* @return
*/
public abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
return null;
}
/**
* @Description 鉴权
* @param principals 令牌
* @return
*/
public abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
return null;
}
}
⑵ 自定义Realm
1) 需求
定义Realm,取得密码用于比较
2) 实现
① 新建项目
shiro-study-project
② 定义SecurityService
SecurityService
/**
* @Description:权限服务接口
*/
public interface SecurityService {
/**
* @Description 查找密码按用户登录名
* @param loginName 登录名称
* @return
*/
String findPasswordByLoginName(String loginName);
}
SecurityServiceImpl
public class SecurityServiceImpl implements SecurityService {
@Override
public String findPasswordByLoginName(String loginName) {
return "123456";
}
}
③ 定义DefinitionRealm
package com.kejizhentan.realm;
import com.kejizhentan.service.SecurityService;
import com.kejizhentan.service.impl.SecurityServiceImpl;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class DefinitionRealm extends AuthorizingRealm {
/**
* @Description 认证接口
* @param token 传递登录token
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从AuthenticationToken中获得登录名称
String loginName = (String) token.getPrincipal();
SecurityService securityService = new SecurityServiceImpl();
String password = securityService.findPasswordByLoginName(loginName);
if ("".equals(password)||password==null){
throw new UnknownAccountException("账户不存在");
}
//传递账号和密码
return new SimpleAuthenticationInfo(loginName,password,getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
④ 自定义模拟Controller的类
package com.kejizhentan.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
/**
* @Auther: kejizhentan
* @Date 2022/12/30 23:14
* @Description: 模拟controller
*/
public class ShiroController {
public static void main(String[] args) {
//导入权限ini文件构建权限工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//工厂构建安全管理器
SecurityManager securityManager = factory.getInstance();
//使用SecurityUtils工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建账号token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("zhangsan", "123456");
//登录操作
subject.login(usernamePasswordToken);
System.out.println("是否登录成功:" + subject.isAuthenticated());
}
}
⑤ 编辑shiro.ini
#声明自定义的realm,且为安全管理器指定realms
[main]
definitionRealm=com.kejizhentan.realm.DefinitionRealm
securityManager.realms=$definitionRealm
⑥ 测试
3) 认证源码跟踪
(1)通过debug模式追踪源码subject.login(token) 发现。首先是进入Subject接口的默认实现类。果然,Subject将用户的用户名密码委托给了securityManager去做。
(2)然后,securityManager说:“卧槽,认证器authenticator小弟,听说你的大学学的专业就是认证呀,那么这个认证的任务就交给你咯”。遂将用户的token委托给内部认证组件authenticator去做
(3)事实上,securityManager的内部组件一个比一个懒。内部认证组件authenticator说:“你们传过来的token我需要拿去跟数据源Realm做对比,这样吧,这个光荣的任务就交给Realm你去做吧”。Realm对象:“一群大懒虫!”。
(4)Realm在接到内部认证组件authenticator组件后很伤心,最后对电脑前的你说:“大兄弟,对不住了,你去实现一下呗”。从图中的方法体中可以看到,当前对象是Realm类对象,即将调用的方法是doGetAuthenticationInfo(token)。而这个方法,就是你即将要重写的方法。如果帐号密码通过了,那么返回一个认证成功的info凭证。如果认证失败,抛出一个异常就好了。你说:“什么?最终还是劳资来认证?”没错,就是苦逼的你去实现了,谁叫你是程序猿呢。所以,你不得不查询一下数据库,重写doGetAuthenticationInfo方法,查出来正确的帐号密码,返回一个正确的凭证info
(5)好了,这个时候你自己编写了一个类,继承了AuthorizingRealm,并实现了上述doGetAuthenticationInfo方法。你在doGetAuthenticationInfo中编写了查询数据库的代码,并将数据库中存放的用户名与密码封装成了一个AuthenticationInfo对象返回。可以看到下图中,info这个对象是有值的,说明从数据库中查询出来了正确的帐号密码
(6)那么,接下来就很简单了。把用户输入的帐号密码与刚才你从数据库中查出来的帐号密码对比一下即可。token封装着用户的帐号密码,AuthenticationInfo封装着从数据库中查询出来的帐号密码。再往下追踪一下代码,最终到了下图中的核心区域。如果没有报异常,说明本次登录成功。
3. 编码、散列算法
⑴ 编码与解码
Shiro提供了base64和16进制字符串编码/解码的API支持,方便一些编码解码操作。
Shiro内部的一些数据的【存储/表示】都使用了base64和16进制字符串
1) 需求
理解base64和16进制字符串编码/解码
2) 新建项目
encode-decode-project
3)添加pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kejizhentan</groupId>
<artifactId>encode-decode-project</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- compiler插件, 设定JDK版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
</plugins>
</build>
</project>
4)新建EncodesUtil
package com.kejizhentan.utils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.Hex;
/**
* @Description:封装base64和16进制编码解码工具类
*/
public class EncodesUtil {
/**
* @Description HEX-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeHex(byte[] input){
return Hex.encodeToString(input);
}
/**
* @Description HEX-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeHex(String input){
return Hex.decode(input);
}
/**
* @Description Base64-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeBase64(byte[] input){
return Base64.encodeToString(input);
}
/**
* @Description Base64-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeBase64(String input){
return Base64.decode(input);
}
}
5)新建ClientTest
package com.kejizhentan.test;
import com.kejizhentan.utils.EncodesUtil;
import org.junit.Test;
/**
* @Description:测试
*/
public class ClientTest {
/**
* @Description 测试16进制编码
*/
@Test
public void testHex(){
//自定义字符串
String val = "hello";
//先将字符串转换成字节数组,然后通过工具类将字节数组编码成字符串
String flag = EncodesUtil.encodeHex(val.getBytes());
//再通过工具将编码的字符串解码成字节数据,然后再装换成字符串
String valHandler = new String(EncodesUtil.decodeHex(flag));
//比较经编码和解码的字符串是否和原先的字符串一样
System.out.println("比较结果:"+val.equals(valHandler));
}
/**
* @Description 测试base64编码
*/
@Test
public void testBase64(){
//自定义字符串
String val = "hello";
//先将字符串转换成字节数组,然后通过工具类将字节数组编码成字符串
String flag = EncodesUtil.encodeBase64(val.getBytes());
//再通过工具将编码的字符串解码成字节数据,然后再装换成字符串
String valHandler = new String(EncodesUtil.decodeBase64(flag));
//比较经编码和解码的字符串是否和原先的字符串一样
System.out.println("比较结果:"+val.equals(valHandler));
}
}
6) 小结
1、shiro目前支持的编码与解码:
base64
(HEX)16进制字符串
2、那么shiro的编码与解码什么时候使用呢?又是怎么使用的呢?
⑵ 散列算法
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如salt(即盐);这样散列的对象是“密码+salt”,这样生成的散列值相对来说更难破解。
shiro支持的散列算法:
Md2Hash、Md5Hash、Sha1Hash、Sha256Hash、Sha384Hash、Sha512Hash
⑶ 在原来的encode-decode-project项目上新建HashUtil工具类
package com.kejizhentan.utils;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import java.util.HashMap;
import java.util.Map;
/**
* @Auther: kejizhentan
* @Date 2022/12/31 14:20
* @Description: 散列算法加密工具类
*/
public class HashUtil {
private static final String SHA1 = "SHA-1";
private static final Integer ITERATIONS =512;
/**
* @Description sha1方法
* @param input 需要散列字符串
* @param salt 盐字符串
* @return
*/
public static String sha1(String input, String salt) {
return new SimpleHash(SHA1, input, salt,ITERATIONS).toString();
}
/**
* @Description 随机获得salt字符串
* @return
*/
public static String generateSalt(){
SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
return randomNumberGenerator.nextBytes().toHex();
}
/**
* @Description 生成密码字符密文和salt密文
* @param
* @return
*/
public static Map<String,String> entryptPassword(String passwordPlain) {
Map<String,String> map = new HashMap<>();
String salt = generateSalt();
String password =sha1(passwordPlain,salt);
map.put("salt", salt);
map.put("password", password);
return map;
}
}
⑷ 修改测试类ClientTest
package com.kejizhentan.test;
import com.kejizhentan.utils.EncodesUtil;
import com.kejizhentan.utils.HashUtil;
import org.junit.Test;
import java.util.Map;
/**
* @Description:测试
*/
public class ClientTest {
/**
* @Description 测试16进制编码
*/
@Test
public void testHex(){
//自定义字符串
String val = "hello";
//先将字符串转换成字节数组,然后通过工具类将字节数组编码成字符串
String flag = EncodesUtil.encodeHex(val.getBytes());
//再通过工具将编码的字符串解码成字节数据,然后再装换成字符串
String valHandler = new String(EncodesUtil.decodeHex(flag));
//比较经编码和解码的字符串是否和原先的字符串一样
System.out.println("比较结果:"+val.equals(valHandler));
}
/**
* @Description 测试base64编码
*/
@Test
public void testBase64(){
//自定义字符串
String val = "hello";
//先将字符串转换成字节数组,然后通过工具类将字节数组编码成字符串
String flag = EncodesUtil.encodeBase64(val.getBytes());
//再通过工具将编码的字符串解码成字节数据,然后再装换成字符串
String valHandler = new String(EncodesUtil.decodeBase64(flag));
//比较经编码和解码的字符串是否和原先的字符串一样
System.out.println("比较结果:"+val.equals(valHandler));
}
/**
* @Auther: kejizhentan
* @Date 2022/12/31 14:25
* @Description: 散列算法加密
*/
@Test
public void testDigestsUtil(){
Map<String,String> map = HashUtil.entryptPassword("123456");
System.out.println("获得结果:"+map.toString());
}
}
4. Realm使用散列算法
上面我们了解编码,以及散列算法,那么在realm中怎么使用?在shiro-study-project中我们使用的密码是明文的校验方式,也就是SecurityServiceImpl中findPasswordByLoginName返回的是明文123456的密码
public class SecurityServiceImpl implements SecurityService {
@Override
public String findPasswordByLoginName(String loginName) {
return "123456";
}
}
⑴ 新建项目
⑵ 添加pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kejizhentan</groupId>
<artifactId>shiro-ciphertext-realm-project</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- compiler插件, 设定JDK版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
</plugins>
</build>
</project>
⑶ 添加shiro.ini
#声明自定义的realm,且为安全管理器指定realms
[main]
definitionRealm=com.kejizhentan.realm.DefinitionRealm
securityManager.realms=$definitionRealm
⑷ 添加EncodesUtil.java
package com.kejizhentan.utils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.Hex;
/**
* @Description:封装base64和16进制编码解码工具类
*/
public class EncodesUtil {
/**
* @Description HEX-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeHex(byte[] input){
return Hex.encodeToString(input);
}
/**
* @Description HEX-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeHex(String input){
return Hex.decode(input);
}
/**
* @Description Base64-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeBase64(byte[] input){
return Base64.encodeToString(input);
}
/**
* @Description Base64-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeBase64(String input){
return Base64.decode(input);
}
}
⑸ 添加HashUtil.java
package com.kejizhentan.utils;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import java.util.HashMap;
import java.util.Map;
/**
* @Auther: kejizhentan
* @Date 2022/12/31 14:20
* @Description: 散列算法加密工具类
*/
public class HashUtil {
public static final String SHA1 = "SHA-1";
public static final Integer ITERATIONS =512;
/**
* @Description sha1方法
* @param input 需要散列字符串
* @param salt 盐字符串
* @return
*/
public static String sha1(String input, String salt) {
return new SimpleHash(SHA1, input, salt,ITERATIONS).toString();
}
/**
* @Description 随机获得salt字符串
* @return
*/
public static String generateSalt(){
SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
return randomNumberGenerator.nextBytes().toHex();
}
/**
* @Description 生成密码字符密文和salt密文
* @param
* @return
*/
public static Map<String,String> entryptPassword(String passwordPlain) {
Map<String,String> map = new HashMap<>();
//随机获得salt字符串
String salt = generateSalt();
//通过加盐的方式生成加密后的密码
String password =sha1(passwordPlain,salt);
map.put("salt", salt);
map.put("password", password);
return map;
}
}
⑹ 修改SecurityService
SecurityService修改成返回salt和password的map
package com.kejizhentan.service;
import java.util.Map;
/**
* @Description:权限服务接口
*/
public interface SecurityService {
/**
* @Description 查找密码按用户登录名
* @param loginName 登录名称
* @return
*/
Map<String,String> findPasswordByLoginName(String loginName);
}
package com.kejizhentan.service.impl;
import com.kejizhentan.service.SecurityService;
import com.kejizhentan.utils.HashUtil;
import java.util.Map;
public class SecurityServiceImpl implements SecurityService {
@Override
public Map<String,String> findPasswordByLoginName(String loginName) {
//模拟数据库中存储的密文信息
return HashUtil.entryptPassword("123456");
}
}
⑺ 添加DefinitionRealm.java
package com.kejizhentan.realm;
import com.kejizhentan.service.SecurityService;
import com.kejizhentan.service.impl.SecurityServiceImpl;
import com.kejizhentan.utils.HashUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import java.util.Map;
public class DefinitionRealm extends AuthorizingRealm {
/**
* @Description 构造函数
*/
public DefinitionRealm() {
//指定密码匹配方式为sha1
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(HashUtil.SHA1);
//指定密码迭代次数
matcher.setHashIterations(HashUtil.ITERATIONS);
//使用父亲方法使匹配方式生效
setCredentialsMatcher(matcher);
}
/**
* @Description 认证接口
* @param token 传递登录token
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从AuthenticationToken中获得登录名称
String loginName = (String) token.getPrincipal();
SecurityService securityService = new SecurityServiceImpl();
Map<String, String> map = securityService.findPasswordByLoginName(loginName);
if (map.isEmpty()){
throw new UnknownAccountException("账户不存在");
}
String salt = map.get("salt");
String password = map.get("password");
//传递账号和密码:参数1:缓存对象,参数2:密文密码,参数三:字节salt,参数4:当前DefinitionRealm名称
return new SimpleAuthenticationInfo(loginName,password, ByteSource.Util.bytes(salt),getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
⑻ 添加ShiroController.java
package com.kejizhentan.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
/**
* @Auther: kejizhentan
* @Date 2022/12/30 23:14
* @Description: 模拟controller
*/
public class ShiroController {
public static void main(String[] args) {
//导入权限ini文件构建权限工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//工厂构建安全管理器
SecurityManager securityManager = factory.getInstance();
//使用SecurityUtils工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建账号token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("zhangsan", "123456");
//登录操作
subject.login(usernamePasswordToken);
System.out.println("是否登录成功:" + subject.isAuthenticated());
}
}
⑼ 测试
5. 身份授权
⑴ 基本流程
1、首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager。
2、SecurityManager接着会委托给内部组件Authorizer;
3、Authorizer再将其请求委托给我们的Realm去做;Realm才是真正干活的;
4、Realm将用户请求的参数封装成权限对象。再从我们重写的doGetAuthorizationInfo方法中获取从数据库中查询到的权限集合。
5、Realm将用户传入的权限对象,与从数据库中查出来的权限对象,进行一一对比。如果用户传入的权限对象在从数据库中查出来的权限对象中,则返回true,否则返回false。
进行授权操作的前提:用户必须通过认证。
在真实的项目中,角色与权限都存放在数据库中。为了快速上手,我们先创建一个自定义DefinitionRealm,模拟它已经登录成功。直接返回一个登录验证凭证,告诉Shiro框架,我们从数据库中查询出来的密码是也是就是你输入的密码。所以,不管用户输入什么,本次登录验证都是通过的。
/**
* @Description 认证接口
* @param token 传递登录token
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从AuthenticationToken中获得登录名称
String loginName = (String) token.getPrincipal();
SecurityService securityService = new SecurityServiceImpl();
Map<String, String> map = securityService.findPasswordByLoginName(loginName);
if (map.isEmpty()){
throw new UnknownAccountException("账户不存在");
}
String salt = map.get("salt");
String password = map.get("password");
//传递账号和密码:参数1:用户认证凭证信息,参数2:明文密码,参数三:字节salt,参数4:当前DefinitionRealm名称
return new SimpleAuthenticationInfo(loginName,password, ByteSource.Util.bytes(salt),getName());
}
好了,接下来,我们要重写我们本小节的核心方法了。在DefinitionRealm中找到下列方法:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
此方法的传入的参数PrincipalCollection principals,是一个包装对象,它表示"用户认证凭证信息"。包装的是谁呢?没错,就是认证doGetAuthenticationInfo()方法的返回值的第一个参数loginName。你可以通过这个包装对象的getPrimaryPrincipal()方法拿到此值,然后再从数据库中拿到对应的角色和资源,构建SimpleAuthorizationInfo。
/**
* @Description 授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//拿到用户认证凭证信息
String loginName = (String) principals.getPrimaryPrincipal();
//从数据库中查询对应的角色和资源
SecurityService securityService = new SecurityServiceImpl();
List<String> roles = securityService.findRoleByloginName(loginName);
List<String> permissions = securityService.findPermissionByloginName(loginName);
//构建资源校验
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roles);
authorizationInfo.addStringPermissions(permissions);
return authorizationInfo;
}
⑵ 案例演示
1) 需求
1、实现doGetAuthorizationInfo方法实现鉴权
2、使用subject类实现权限的校验
2)实现
① 创建项目
新建shiro-authentication-realm-project
② 导入pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kejizhentan</groupId>
<artifactId>shiro-authentication-realm-project</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- compiler插件, 设定JDK版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
</plugins>
</build>
</project>
③ 创建shiro.ini
#声明自定义的realm,且为安全管理器指定realms
[main]
definitionRealm=com.kejizhentan.realm.DefinitionRealm
securityManager.realms=$definitionRealm
④ 添加EncodesUtil.java
package com.kejizhentan.utils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.Hex;
/**
* @Description:封装base64和16进制编码解码工具类
*/
public class EncodesUtil {
/**
* @Description HEX-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeHex(byte[] input){
return Hex.encodeToString(input);
}
/**
* @Description HEX-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeHex(String input){
return Hex.decode(input);
}
/**
* @Description Base64-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeBase64(byte[] input){
return Base64.encodeToString(input);
}
/**
* @Description Base64-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeBase64(String input){
return Base64.decode(input);
}
}
⑤ 添加HashUtil.java
package com.kejizhentan.utils;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import java.util.HashMap;
import java.util.Map;
/**
* @Auther: kejizhentan
* @Date 2022/12/31 14:20
* @Description: 散列算法加密工具类
*/
public class HashUtil {
public static final String SHA1 = "SHA-1";
public static final Integer ITERATIONS =512;
/**
* @Description sha1方法
* @param input 需要散列字符串
* @param salt 盐字符串
* @return
*/
public static String sha1(String input, String salt) {
return new SimpleHash(SHA1, input, salt,ITERATIONS).toString();
}
/**
* @Description 随机获得salt字符串
* @return
*/
public static String generateSalt(){
SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
return randomNumberGenerator.nextBytes().toHex();
}
/**
* @Description 生成密码字符密文和salt密文
* @param
* @return
*/
public static Map<String,String> entryptPassword(String passwordPlain) {
Map<String,String> map = new HashMap<>();
//随机获得salt字符串
String salt = generateSalt();
//通过加盐的方式生成加密后的密码
String password =sha1(passwordPlain,salt);
map.put("salt", salt);
map.put("password", password);
return map;
}
}
⑥ 创建DefinitionRealm.java
package com.kejizhentan.realm;
import com.kejizhentan.service.SecurityService;
import com.kejizhentan.service.impl.SecurityServiceImpl;
import com.kejizhentan.utils.HashUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.List;
import java.util.Map;
public class DefinitionRealm extends AuthorizingRealm {
/**
* @Description 构造函数
*/
public DefinitionRealm() {
//指定密码匹配方式为sha1
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(HashUtil.SHA1);
//指定密码迭代次数
matcher.setHashIterations(HashUtil.ITERATIONS);
//使用父亲方法使匹配方式生效
setCredentialsMatcher(matcher);
}
/**
* @Description 认证接口
* @param token 传递登录token
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从AuthenticationToken中获得登录名称
String loginName = (String) token.getPrincipal();
SecurityService securityService = new SecurityServiceImpl();
Map<String, String> map = securityService.findPasswordByLoginName(loginName);
if (map.isEmpty()){
throw new UnknownAccountException("账户不存在");
}
String salt = map.get("salt");
String password = map.get("password");
//传递账号和密码:参数1:缓存对象,参数2:密文密码,参数三:字节salt,参数4:当前DefinitionRealm名称
return new SimpleAuthenticationInfo(loginName,password, ByteSource.Util.bytes(salt),getName());
}
/**
* @Description 授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//拿到用户认证凭证信息
String loginName = (String) principals.getPrimaryPrincipal();
//从数据库中查询对应的角色和资源
SecurityService securityService = new SecurityServiceImpl();
List<String> roles = securityService.findRoleByloginName(loginName);
List<String> permissions = securityService.findPermissionByloginName(loginName);
//构建资源校验
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roles);
authorizationInfo.addStringPermissions(permissions);
return authorizationInfo;
}
}
⑦ 创建SecurityService.java
SecurityService.java
/**
* @Description:权限服务接口
*/
public interface SecurityService {
/**
* @Description 查找角色按用户登录名
* @param loginName 登录名称
* @return
*/
List<String> findRoleByloginName(String loginName);
/**
* @Description 查找资源按用户登录名
* @param loginName 登录名称
* @return
*/
List<String> findPermissionByloginName(String loginName);
/**
* @Description 查找密码按用户登录名
* @param loginName 登录名称
* @return
*/
Map<String,String> findPasswordByLoginName(String loginName);
}
SecurityServiceImpl.java
package com.kejizhentan.service.impl;
import com.kejizhentan.service.SecurityService;
import com.kejizhentan.utils.HashUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class SecurityServiceImpl implements SecurityService {
@Override
public List<String> findRoleByloginName(String loginName) {
List<String> list = new ArrayList<>();
list.add("admin");
list.add("dev");
return list;
}
@Override
public List<String> findPermissionByloginName(String loginName) {
List<String> list = new ArrayList<>();
list.add("order:add");
list.add("order:list");
list.add("order:del");
return list;
}
@Override
public Map<String,String> findPasswordByLoginName(String loginName) {
//模拟数据库中存储的密文信息
return HashUtil.entryptPassword("123456");
}
}
⑧ 创建ShiroController.java
package com.kejizhentan.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
/**
* @Auther: kejizhentan
* @Date 2022/12/30 23:14
* @Description: 模拟controller
*/
public class ShiroController {
public static void main(String[] args) {
Subject subject = shiroLogin("zhangsan", "123456");
//判断用户是否已经登录
System.out.println("是否登录成功:" + subject.isAuthenticated());
//---------检查当前用户的角色信息------------
System.out.println("是否有管理员角色:"+subject.hasRole("admin"));
//---------如果当前用户有此角色,无返回值。若没有此权限,则抛 UnauthorizedException------------
try {
subject.checkRole("coder");
System.out.println("有coder角色");
}catch (Exception e){
System.out.println("没有coder角色");
}
//---------检查当前用户的权限信息------------
System.out.println("是否有查看订单列表资源:"+subject.isPermitted("order:list"));
//---------如果当前用户有此权限,无返回值。若没有此权限,则抛 UnauthorizedException------------
try {
subject.checkPermissions("order:add", "order:del");
System.out.println("有添加和删除订单资源");
}catch (Exception e){
System.out.println("没有有添加和删除订单资源");
}
}
/**
* @Description 登录方法
*/
private static Subject shiroLogin(String loginName,String password) {
//导入权限ini文件构建权限工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//工厂构建安全管理器
SecurityManager securityManager = factory.getInstance();
//使用SecurityUtils工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建账号token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginName, password);
//登录操作
subject.login(usernamePasswordToken);
return subject;
}
}
3)授权源码追踪
(1)客户端调用 subject.hasRole(“admin”),判断当前用户是否有"admin"角色权限。
(2)Subject门面对象接收到要被验证的角色信息"admin",并将其委托给securityManager中验证。
(3)securityManager将验证请求再次委托给内部的小弟:内部组件Authorizer authorizer
(4)内部小弟authorizer也是个混子,将其委托给了我们自定义的Realm去做
(5) 先拿到PrincipalCollection principal对象,同时传入校验的角色循环校验,循环中先创建鉴权信息
(6)先看缓存中是否已经有鉴权信息
(7)都是一群懒货!!最后干活的还是我这个猴子!
⑶ 小结
1、鉴权需要实现doGetAuthorizationInfo方法
2、鉴权使用门面subject中方法进行鉴权
以check开头的会抛出异常
以is和has开头会返回布尔值
四、Web项目集成Shiro
1.Web集成原理分析
⑴ web集成的配置
还记得吗,以前我们在没有与WEB环境进行集成的时候,为了生成SecurityManager对象,是通过手动读取配置文件生成工厂对象,再通过工厂对象获取到SecurityManager的。就像下面代码展示的那样:
/**
* @Description 登录方法
*/
private Subject shiroLogin(String loginName,String password) {
//导入权限ini文件构建权限工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//工厂构建安全管理器
SecurityManager securityManager = factory.getInstance();
//使用SecurityUtils工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建账号token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginName, password);
//登录操作
subject.login(usernamePasswordToken);
return subject;
}
不过,现在我们既然说要与WEB集成,那么首先要做的事情就是把我们的shiro.ini这个配置文件交付到WEB环境中,定义shiro.ini文件如下:
#声明自定义的realm,且为安全管理器指定realms
[main]
definitionRealm=com.kejizhentan.realm.DefinitionRealm
securityManager.realms=$definitionRealm
1) 新建项目
创建shiro-web-project项目
2) pom.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kejizhentan</groupId>
<artifactId>shiro-web-project</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>shiro-web-project Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- tomcat7插件,命令: mvn tomcat7:run -DskipTests -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<uriEncoding>utf-8</uriEncoding>
<port>8080</port>
<path>/platform</path>
</configuration>
</plugin>
<!-- compiler插件, 设定JDK版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
</plugins>
</build>
</project>
3) web.xml配置
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>shiro-web-project</display-name>
<!-- 初始化SecurityManager对象所需要的环境-->
<context-param>
<param-name>shiroEnvironmentClass</param-name>
<param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value>
</context-param>
<!-- 指定Shiro的配置文件的位置 -->
<context-param>
<param-name>shiroConfigLocations</param-name>
<param-value>classpath:shiro.ini</param-value>
</context-param>
<!-- 监听服务器启动时,创建shiro的web环境。
即加载shiroEnvironmentClass变量指定的IniWebEnvironment类-->
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<!-- shiro的l过滤入口,过滤一切请求 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<!-- 过滤所有请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
⑵ SecurityManager对象创建
上面我们集成shiro到web项目了,下面我们来追踪下源码,看下SecurityManager对象是如何创建的
(1)我启动了服务器,监听器捕获到了服务器启动事件。我现在所处的位置EnvironmentLoaderListener监听器的入口处
(2)进入方法内查看,它先根据我们的shiroEnvironmentClass变量的值org.apache.shiro.web.env.IniWebEnvironment,初始化一个shiro环境对象
(3)最后在创建一个SecurityManager对象,再将其绑定到刚才通过字节码创建的Shiro环境对象中
到这来SecurityManager就完成了初始化
2. Shiro默认过滤器
Shiro内置了很多默认的过滤器,比如身份验证、授权等相关的。默认过滤器可以参考org.apache.shiro.web.filter.mgt.DefaultFilter中的枚举过滤器
⑴ 认证相关
过滤器 | 过滤器类 | 说明 | 默认 |
---|---|---|---|
authc | FormAuthenticationFilter | 基于表单的过滤器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录 | 无 |
logout | LogoutFilter | 退出过滤器,主要属性:redirectUrl:退出成功后重定向的地址,如“/logout=logout” | / |
anon | AnonymousFilter | 匿名过滤器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon” | 无 |
⑵ 授权相关
过滤器 | 过滤器类 | 说明 | 默认 |
---|---|---|---|
roles | RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]” | 无 |
perms | PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms[“user:create”]” | 无 |
port | PortFilter | 端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样 | 无 |
rest | HttpMethodPermissionFilter | rest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll) | 无 |
ssl | SslFilter | SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样; | 无 |
3. Web集成完整案例
⑴ 项目结构如下
⑵ 添加pom.xml依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kejizhentan</groupId>
<artifactId>shiro-web-project</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>shiro-web-project Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- tomcat7插件,命令: mvn tomcat7:run -DskipTests -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<uriEncoding>utf-8</uriEncoding>
<port>8080</port>
<path>/platform</path>
</configuration>
</plugin>
<!-- compiler插件, 设定JDK版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
</plugins>
</build>
</project>
⑶ 编写shiro.ini文件
#声明自定义的realm,且为安全管理器指定realms
[main]
definitionRealm=com.kejizhentan.realm.DefinitionRealm
securityManager.realms=$definitionRealm
#用户退出后跳转指定JSP页面
logout.redirectUrl=/login.jsp
#若没有登录,则被authc过滤器重定向到login.jsp页面
authc.loginUrl = /login.jsp
[urls]
/login=anon
#发送/home请求需要先登录
/home= authc
#发送/order/list请求需要先登录
/order-list = roles[admin]
#提交代码需要order:add权限
/order-add = perms["order:add"]
#更新代码需要order:del权限
/order-del = perms["order:del"]
#发送退出请求则用退出过滤器
/logout = logout
⑷ 相关工具类
封装base64和16进制编码解码工具类EncodesUtil.java
package com.kejizhentan.utils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.Hex;
/**
* @Description:封装base64和16进制编码解码工具类
*/
public class EncodesUtil {
/**
* @Description HEX-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeHex(byte[] input){
return Hex.encodeToString(input);
}
/**
* @Description HEX-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeHex(String input){
return Hex.decode(input);
}
/**
* @Description Base64-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeBase64(byte[] input){
return Base64.encodeToString(input);
}
/**
* @Description Base64-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeBase64(String input){
return Base64.decode(input);
}
}
散列算法加密工具类HashUtil.java
package com.kejizhentan.utils;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import java.util.HashMap;
import java.util.Map;
/**
* @Auther: kejizhentan
* @Date 2022/12/31 14:20
* @Description: 散列算法加密工具类
*/
public class HashUtil {
public static final String SHA1 = "SHA-1";
public static final Integer ITERATIONS =512;
/**
* @Description sha1方法
* @param input 需要散列字符串
* @param salt 盐字符串
* @return
*/
public static String sha1(String input, String salt) {
return new SimpleHash(SHA1, input, salt,ITERATIONS).toString();
}
/**
* @Description 随机获得salt字符串
* @return
*/
public static String generateSalt(){
SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
return randomNumberGenerator.nextBytes().toHex();
}
/**
* @Description 生成密码字符密文和salt密文
* @param
* @return
*/
public static Map<String,String> entryptPassword(String passwordPlain) {
Map<String,String> map = new HashMap<>();
//随机获得salt字符串
String salt = generateSalt();
//通过加盐的方式生成加密后的密码
String password =sha1(passwordPlain,salt);
map.put("salt", salt);
map.put("password", password);
return map;
}
}
⑸ 创建DefinitionRealm.java
package com.kejizhentan.realm;
import com.kejizhentan.service.SecurityService;
import com.kejizhentan.service.impl.SecurityServiceImpl;
import com.kejizhentan.utils.HashUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.List;
import java.util.Map;
public class DefinitionRealm extends AuthorizingRealm {
/**
* @Description 构造函数
*/
public DefinitionRealm() {
//指定密码匹配方式为sha1
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(HashUtil.SHA1);
//指定密码迭代次数
matcher.setHashIterations(HashUtil.ITERATIONS);
//使用父亲方法使匹配方式生效
setCredentialsMatcher(matcher);
}
/**
* @Description 认证接口
* @param token 传递登录token
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从AuthenticationToken中获得登录名称
String loginName = (String) token.getPrincipal();
SecurityService securityService = new SecurityServiceImpl();
Map<String, String> map = securityService.findPasswordByLoginName(loginName);
if (map.isEmpty()){
throw new UnknownAccountException("账户不存在");
}
String salt = map.get("salt");
String password = map.get("password");
//传递账号和密码:参数1:缓存对象,参数2:密文密码,参数三:字节salt,参数4:当前DefinitionRealm名称
return new SimpleAuthenticationInfo(loginName,password, ByteSource.Util.bytes(salt),getName());
}
/**
* @Description 授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//拿到用户认证凭证信息
String loginName = (String) principals.getPrimaryPrincipal();
//从数据库中查询对应的角色和资源
SecurityService securityService = new SecurityServiceImpl();
List<String> roles = securityService.findRoleByloginName(loginName);
List<String> permissions = securityService.findPermissionByloginName(loginName);
//构建资源校验
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roles);
authorizationInfo.addStringPermissions(permissions);
return authorizationInfo;
}
}
⑹ 编写LoginService
package com.kejizhentan.service;
import org.apache.shiro.authc.UsernamePasswordToken;
/**
* @Description:登录服务
*/
public interface LoginService {
/**
* @Description 登录方法
* @param token 登录对象
* @return
*/
boolean login(UsernamePasswordToken token);
/**
* @Description 登出方法
*/
void logout();
}
package com.kejizhentan.service.impl;
import com.kejizhentan.service.LoginService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
/**
* @Description:登录服务
*/
public class LoginServiceImpl implements LoginService {
@Override
public boolean login(UsernamePasswordToken token) {
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
}catch (Exception e){
return false;
}
return subject.isAuthenticated();
}
@Override
public void logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
}
}
⑺ 编写SecurityService
package com.kejizhentan.service;
import java.util.List;
import java.util.Map;
/**
* @Description:权限服务接口
*/
public interface SecurityService {
/**
* @Description 查找角色按用户登录名
* @param loginName 登录名称
* @return
*/
List<String> findRoleByloginName(String loginName);
/**
* @Description 查找资源按用户登录名
* @param loginName 登录名称
* @return
*/
List<String> findPermissionByloginName(String loginName);
/**
* @Description 查找密码按用户登录名
* @param loginName 登录名称
* @return
*/
Map<String,String> findPasswordByLoginName(String loginName);
}
package com.kejizhentan.service.impl;
import com.kejizhentan.service.SecurityService;
import com.kejizhentan.utils.HashUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @Description:权限服务层
*/
public class SecurityServiceImpl implements SecurityService {
@Override
public Map<String,String> findPasswordByLoginName(String loginName) {
return HashUtil.entryptPassword("123456");
}
@Override
public List<String> findRoleByloginName(String loginName) {
List<String> list = new ArrayList<>();
if ("admin".equals(loginName)){
list.add("admin");
}
list.add("dev");
return list;
}
@Override
public List<String> findPermissionByloginName(String loginName) {
List<String> list = new ArrayList<>();
if ("zhangsan".equals(loginName)){
list.add("order:list");
list.add("order:add");
list.add("order:del");
}
return list;
}
}
⑻ 添加web层内容
1) LoginServlet
package com.kejizhentan.servlet;
import com.kejizhentan.service.LoginService;
import com.kejizhentan.service.impl.LoginServiceImpl;
import org.apache.shiro.authc.UsernamePasswordToken;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:登录方法
*/
@WebServlet(urlPatterns = "/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//获取输入的帐号密码
String username = req.getParameter("loginName");
String password = req.getParameter("password");
//封装用户数据,成为Shiro能认识的token标识
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
LoginService loginService = new LoginServiceImpl();
//将封装用户信息的token进行验证
boolean isLoginSuccess = loginService.login(token);
if (!isLoginSuccess) {
//重定向到未登录成功页面
resp.sendRedirect("login.jsp");
return;
}
req.getRequestDispatcher("/home").forward(req, resp);
}
}
2)HomeServlet
package com.kejizhentan.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:系统home页面
*/
@WebServlet(urlPatterns = "/home")
public class HomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.getRequestDispatcher("home.jsp").forward(req, resp);
}
}
3)OrderAddServlet
package com.kejizhentan.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:添加页码
*/
@WebServlet(urlPatterns = "/order-add")
public class OrderAddServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.getRequestDispatcher("order-add.jsp").forward(req, resp);
}
}
4)OrderListServlet
package com.kejizhentan.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:订单列表
*/
@WebServlet(urlPatterns = "/order-list")
public class OrderListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.getRequestDispatcher("order-list.jsp").forward(req, resp);
}
}
5)LogoutServlet
package com.kejizhentan.servlet;
import com.kejizhentan.service.LoginService;
import com.kejizhentan.service.impl.LoginServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:登出
*/
@WebServlet(urlPatterns = "/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
LoginService loginService = new LoginServiceImpl();
loginService.logout();
}
}
6)web.xml
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>shiro-web-project</display-name>
<!-- 初始化SecurityManager对象所需要的环境-->
<context-param>
<param-name>shiroEnvironmentClass</param-name>
<param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value>
</context-param>
<!-- 指定Shiro的配置文件的位置 -->
<context-param>
<param-name>shiroConfigLocations</param-name>
<param-value>classpath:shiro.ini</param-value>
</context-param>
<!-- 监听服务器启动时,创建shiro的web环境。
即加载shiroEnvironmentClass变量指定的IniWebEnvironment类-->
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<!-- shiro的l过滤入口,过滤一切请求 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<!-- 过滤所有请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
⑼ 添加JSP
login.jsp登录页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" action="${pageContext.request.contextPath}/login">
<table>
<tr>
<th>登陆名称</th>
<td><input type="text" name="loginName"></td>
</tr>
<tr>
<th>密码</th>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="提交"/>
</td>
</tr>
</table>
</form>
</body>
</html>
home.jsp系统页
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title></title>
</head>
<body>
<h6>
<a href="${pageContext.request.contextPath}/logout">退出</a>
<a href="${pageContext.request.contextPath}/order-list">列表</a>
<a href="${pageContext.request.contextPath}/order-add">添加</a>
</h6>
</body>
</html>
order-add.jsp订单添加(伪代码)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Title</title>
</head>
<body>
添加页面
</body>
</html>
order-list.jsp订单列表
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--导入jstl标签库--%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户列表jsp页面</title>
<style>
table {border:1px solid #000000}
table th{border:1px solid #000000}
table td{border:1px solid #000000}
</style>
</head>
<body>
<table cellpadding="0" cellspacing="0" width="80%">
<tr>
<th>编号</th>
<th>公司名称</th>
<th>信息来源</th>
<th>所属行业</th>
<th>级别</th>
<th>联系地址</th>
<th>联系电话</th>
</tr>
<tr>
<td>1</td>
<td>柯基侦探</td>
<td>网络营销</td>
<td>互联网</td>
<td>普通客户</td>
<td>北京市东城区</td>
<td>8888888888</td>
</tr>
<tr>
<td>2</td>
<td>柯基侦探微信公众号</td>
<td>j2ee</td>
<td>互联网</td>
<td>VIP客户</td>
<td>北京市东城区</td>
<td>8888888887</td>
</tr>
<tr>
<td>3</td>
<td>柯基</td>
<td>大数据</td>
<td>互联网</td>
<td>VIP客户</td>
<td>北京市东城区</td>
<td>888888888888</td>
</tr>
</table>
</body>
</html>
⑽ 测试
1) 启动操作
点击apply然后点击OK
2) 登录过滤
访问http://localhost:8080/platform/home的时候,会被拦截,跳转到登录页面
3) 角色过滤
使用admin
用户登录,密码:123456
根据SecurityServiceImpl我们可以知道使用admin账号
登录成功之后:
此时点击“列表”,因为当前admin用户是有admin角色
所以能正常访问“列表”中的内容
点击“添加”,因为当前admin用户是没有order:add的资源
所以回401
4) 资源过滤
点击“退出”,使用“zhangsan”用户登录,密码为123456 ,点击“添加”
因为SecurityServiceImpl中为zhangsan用户添加如下的资源
点击“添加”之后正常访问
点击“列表”之后,因为“zhangsan”用户没有“admin”角色,所以访问受限
4. web项目授权
前面我们学习了基于ini
文件配置方式来完成授权,下面我们来看下其他2种方式的授权
⑴ 基于代码
1) 登录相关
Subject 登录相关方法 | 描述 |
---|---|
isAuthenticated() | 返回true 表示已经登录,否则返回false。 |
2)角色相关
Subject 角色相关方法 | 描述 |
---|---|
hasRole(String roleName) | 如果Subject 被分配了指定的角色,返回true ,否则返回false。 |
hasRoles(List<String> roleNames) | 如果Subject 被分配了所有指定的角色,返回true ,否则返回false。 |
hasAllRoles(Collection<String>roleNames) | 返回一个与方法参数中目录一致的hasRole 结果的集合。有性能的提高如果许多角色需要执行检查(例如,当自定义一个复杂的视图)。 |
checkRole(String roleName) | 没有返回值,如果Subject 被分配了指定的角色,不然的话就抛出AuthorizationException。 |
checkRoles(Collection<String>roleNames) | 没有返回值,如果Subject 被分配了所有的指定的角色,不然的话就抛出AuthorizationException。 |
checkRoles(String… roleNames) | 与上面的checkRoles 方法的效果相同,但允许Java5 的var-args 类型的参数 |
3) 资源相关
Subject 资源相关方法 | 描述 |
---|---|
isPermitted(Permission p) | 如果该Subject 被允许执行某动作或访问被权限实例指定的资源,返回true,否则返回false |
isPermitted(List<Permission> perms) | 返回一个与方法参数中目录一致的isPermitted 结果的集合。 |
isPermittedAll(Collection<Permission>perms) | 如果该Subject 被允许所有指定的权限,返回true,否则返回false有性能的提高如果需要执行许多检查(例如,当自定义一个复杂的视图) |
isPermitted(String perm) | 如果该Subject 被允许执行某动作或访问被字符串权限指定的资源,返回true,否则返回false。 |
isPermitted(String…perms) | 返回一个与方法参数中目录一致的isPermitted 结果的数组。有性能的提高如果许多字符串权限检查需要被执行(例如,当自定义一个复杂的视图)。 |
isPermittedAll(String…perms) | 如果该Subject 被允许所有指定的字符串权限,返回true,否则返回false。 |
checkPermission(Permission p) | 没有返回值,如果Subject 被允许执行某动作或访问被特定的权限实例指定的资源,不然的话就抛出AuthorizationException 异常。 |
checkPermission(String perm) | 没有返回值,如果Subject 被允许执行某动作或访问被特定的字符串权限指定的资源,不然的话就抛出AuthorizationException 异常。 |
checkPermissions(Collection<Permission> perms) | 没有返回值,如果Subject 被允许所有的权限,不然的话就抛出AuthorizationException 异常。有性能的提高如果需要执行许多检查(例如,当自定义一个复杂的视图) |
checkPermissions(String… perms) | 和上面的checkPermissions 方法效果相同,但是使用的是基于字符串的权限。 |
4) 案例如下
① 项目结构如下
② 添加pom.xml依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kejizhentan</groupId>
<artifactId>shiro-web-project</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>shiro-web-project Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- tomcat7插件,命令: mvn tomcat7:run -DskipTests -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<uriEncoding>utf-8</uriEncoding>
<port>8080</port>
<path>/platform</path>
</configuration>
</plugin>
<!-- compiler插件, 设定JDK版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
</plugins>
</build>
</project>
③ 编写shiro.ini文件
#声明自定义的realm,且为安全管理器指定realms
[main]
definitionRealm=com.kejizhentan.realm.DefinitionRealm
securityManager.realms=$definitionRealm
#用户退出后跳转指定JSP页面
logout.redirectUrl=/login.jsp
#若没有登录,则被authc过滤器重定向到login.jsp页面
authc.loginUrl = /login.jsp
[urls]
/login=anon
#发送退出请求则用退出过滤器
/logout = logout
④ 相关工具类
封装base64和16进制编码解码工具类EncodesUtil.java
package com.kejizhentan.utils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.Hex;
/**
* @Description:封装base64和16进制编码解码工具类
*/
public class EncodesUtil {
/**
* @Description HEX-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeHex(byte[] input){
return Hex.encodeToString(input);
}
/**
* @Description HEX-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeHex(String input){
return Hex.decode(input);
}
/**
* @Description Base64-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeBase64(byte[] input){
return Base64.encodeToString(input);
}
/**
* @Description Base64-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeBase64(String input){
return Base64.decode(input);
}
}
散列算法加密工具类HashUtil.java
package com.kejizhentan.utils;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import java.util.HashMap;
import java.util.Map;
/**
* @Auther: kejizhentan
* @Date 2022/12/31 14:20
* @Description: 散列算法加密工具类
*/
public class HashUtil {
public static final String SHA1 = "SHA-1";
public static final Integer ITERATIONS =512;
/**
* @Description sha1方法
* @param input 需要散列字符串
* @param salt 盐字符串
* @return
*/
public static String sha1(String input, String salt) {
return new SimpleHash(SHA1, input, salt,ITERATIONS).toString();
}
/**
* @Description 随机获得salt字符串
* @return
*/
public static String generateSalt(){
SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
return randomNumberGenerator.nextBytes().toHex();
}
/**
* @Description 生成密码字符密文和salt密文
* @param
* @return
*/
public static Map<String,String> entryptPassword(String passwordPlain) {
Map<String,String> map = new HashMap<>();
//随机获得salt字符串
String salt = generateSalt();
//通过加盐的方式生成加密后的密码
String password =sha1(passwordPlain,salt);
map.put("salt", salt);
map.put("password", password);
return map;
}
}
⑤ 创建DefinitionRealm.java
package com.kejizhentan.realm;
import com.kejizhentan.service.SecurityService;
import com.kejizhentan.service.impl.SecurityServiceImpl;
import com.kejizhentan.utils.HashUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.List;
import java.util.Map;
public class DefinitionRealm extends AuthorizingRealm {
/**
* @Description 构造函数
*/
public DefinitionRealm() {
//指定密码匹配方式为sha1
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(HashUtil.SHA1);
//指定密码迭代次数
matcher.setHashIterations(HashUtil.ITERATIONS);
//使用父亲方法使匹配方式生效
setCredentialsMatcher(matcher);
}
/**
* @Description 认证接口
* @param token 传递登录token
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从AuthenticationToken中获得登录名称
String loginName = (String) token.getPrincipal();
SecurityService securityService = new SecurityServiceImpl();
Map<String, String> map = securityService.findPasswordByLoginName(loginName);
if (map.isEmpty()){
throw new UnknownAccountException("账户不存在");
}
String salt = map.get("salt");
String password = map.get("password");
//传递账号和密码:参数1:缓存对象,参数2:密文密码,参数三:字节salt,参数4:当前DefinitionRealm名称
return new SimpleAuthenticationInfo(loginName,password, ByteSource.Util.bytes(salt),getName());
}
/**
* @Description 授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//拿到用户认证凭证信息
String loginName = (String) principals.getPrimaryPrincipal();
//从数据库中查询对应的角色和资源
SecurityService securityService = new SecurityServiceImpl();
List<String> roles = securityService.findRoleByloginName(loginName);
List<String> permissions = securityService.findPermissionByloginName(loginName);
//构建资源校验
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roles);
authorizationInfo.addStringPermissions(permissions);
return authorizationInfo;
}
}
⑥ 编写LoginService
package com.kejizhentan.service;
import org.apache.shiro.authc.UsernamePasswordToken;
/**
* @Description:登录服务
*/
public interface LoginService {
/**
* @Description 登录方法
* @param token 登录对象
* @return
*/
boolean login(UsernamePasswordToken token);
/**
* @Description 登出方法
*/
void logout();
}
package com.kejizhentan.service.impl;
import com.kejizhentan.service.LoginService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
/**
* @Description:登录服务
*/
public class LoginServiceImpl implements LoginService {
@Override
public boolean login(UsernamePasswordToken token) {
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
}catch (Exception e){
return false;
}
return subject.isAuthenticated();
}
@Override
public void logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
}
}
⑦ 编写SecurityService
package com.kejizhentan.service;
import java.util.List;
import java.util.Map;
/**
* @Description:权限服务接口
*/
public interface SecurityService {
/**
* @Description 查找角色按用户登录名
* @param loginName 登录名称
* @return
*/
List<String> findRoleByloginName(String loginName);
/**
* @Description 查找资源按用户登录名
* @param loginName 登录名称
* @return
*/
List<String> findPermissionByloginName(String loginName);
/**
* @Description 查找密码按用户登录名
* @param loginName 登录名称
* @return
*/
Map<String,String> findPasswordByLoginName(String loginName);
}
package com.kejizhentan.service.impl;
import com.kejizhentan.service.SecurityService;
import com.kejizhentan.utils.HashUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @Description:权限服务层
*/
public class SecurityServiceImpl implements SecurityService {
@Override
public Map<String,String> findPasswordByLoginName(String loginName) {
return HashUtil.entryptPassword("123456");
}
@Override
public List<String> findRoleByloginName(String loginName) {
List<String> list = new ArrayList<>();
if ("admin".equals(loginName)){
list.add("admin");
}
list.add("dev");
return list;
}
@Override
public List<String> findPermissionByloginName(String loginName) {
List<String> list = new ArrayList<>();
if ("zhangsan".equals(loginName)){
list.add("order:list");
list.add("order:add");
list.add("order:del");
}
return list;
}
}
⑧ 添加web层内容
○ LoginServlet
package com.kejizhentan.servlet;
import com.kejizhentan.service.LoginService;
import com.kejizhentan.service.impl.LoginServiceImpl;
import org.apache.shiro.authc.UsernamePasswordToken;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:登录方法
*/
@WebServlet(urlPatterns = "/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//获取输入的帐号密码
String username = req.getParameter("loginName");
String password = req.getParameter("password");
//封装用户数据,成为Shiro能认识的token标识
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
LoginService loginService = new LoginServiceImpl();
//将封装用户信息的token进行验证
boolean isLoginSuccess = loginService.login(token);
if (!isLoginSuccess) {
//重定向到未登录成功页面
resp.sendRedirect("login.jsp");
return;
}
req.getRequestDispatcher("/home").forward(req, resp);
}
}
○ 登录相关 HomeServlet
package com.kejizhentan.servlet;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:系统home页面
*/
@WebServlet(urlPatterns = "/home")
public class HomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//通过subjectd对象去判断是否登录
Subject subject = SecurityUtils.getSubject();
boolean flag = subject.isAuthenticated();
if (flag){
resp.sendRedirect("home.jsp");
}else {
req.getRequestDispatcher("/login").forward(req, resp);
}
}
}
访问http://localhost:8080/platform/home 进行debug
此时我们通过subject.isAuthenticated()判断是否登录,如果登录则重定向到home.jsp,如果没有登录则转发到/login对应的servlet
○ 资源相关OrderAddServlet
package com.kejizhentan.servlet;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:添加页码
*/
@WebServlet(urlPatterns = "/order-add")
public class OrderAddServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Subject subject = SecurityUtils.getSubject();
//判断是否有对应资源
boolean flag = subject.isPermitted("order:add");
if (flag){
req.getRequestDispatcher("order-add.jsp").forward(req, resp);
}else {
req.getRequestDispatcher("/login").forward(req, resp);
}
}
}
访问http://localhost:8080/platform/order-add
因为此时我未登录,也就是说当前没有order:add资源,通过 subject.isPermitted(“order:add”)返回false
○ 角色相关 OrderListServlet
package com.kejizhentan.servlet;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:订单列表
*/
@WebServlet(urlPatterns = "/order-list")
public class OrderListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Subject subject = SecurityUtils.getSubject();
//判断当前角色
boolean flag = subject.hasRole("admin");
if (flag){
req.getRequestDispatcher("order-list.jsp").forward(req, resp);
}else {
req.getRequestDispatcher("/login").forward(req, resp);
}
}
}
访问 http://localhost:8080/platform/order-list
因为此时我未登录,也就是说当前没有admin角色,这是通过subject.hasRole(“admin”)返回未false
○ LogoutServlet
package com.kejizhentan.servlet;
import com.kejizhentan.service.LoginService;
import com.kejizhentan.service.impl.LoginServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description:登出
*/
@WebServlet(urlPatterns = "/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
LoginService loginService = new LoginServiceImpl();
loginService.logout();
}
}
○ web.xml
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>shiro-web-project</display-name>
<!-- 初始化SecurityManager对象所需要的环境-->
<context-param>
<param-name>shiroEnvironmentClass</param-name>
<param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value>
</context-param>
<!-- 指定Shiro的配置文件的位置 -->
<context-param>
<param-name>shiroConfigLocations</param-name>
<param-value>classpath:shiro.ini</param-value>
</context-param>
<!-- 监听服务器启动时,创建shiro的web环境。
即加载shiroEnvironmentClass变量指定的IniWebEnvironment类-->
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<!-- shiro的l过滤入口,过滤一切请求 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<!-- 过滤所有请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
○ 添加JSP
login.jsp登录页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" action="${pageContext.request.contextPath}/login">
<table>
<tr>
<th>登陆名称</th>
<td><input type="text" name="loginName"></td>
</tr>
<tr>
<th>密码</th>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="提交"/>
</td>
</tr>
</table>
</form>
</body>
</html>
home.jsp系统页
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title></title>
</head>
<body>
<h6>
<a href="${pageContext.request.contextPath}/logout">退出</a>
<a href="${pageContext.request.contextPath}/order-list">列表</a>
<a href="${pageContext.request.contextPath}/order-add">添加</a>
</h6>
</body>
</html>
order-add.jsp订单添加(伪代码)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Title</title>
</head>
<body>
添加页面
</body>
</html>
order-list.jsp订单列表
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--导入jstl标签库--%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户列表jsp页面</title>
<style>
table {border:1px solid #000000}
table th{border:1px solid #000000}
table td{border:1px solid #000000}
</style>
</head>
<body>
<table cellpadding="0" cellspacing="0" width="80%">
<tr>
<th>编号</th>
<th>公司名称</th>
<th>信息来源</th>
<th>所属行业</th>
<th>级别</th>
<th>联系地址</th>
<th>联系电话</th>
</tr>
<tr>
<td>1</td>
<td>柯基侦探</td>
<td>网络营销</td>
<td>互联网</td>
<td>普通客户</td>
<td>北京市东城区</td>
<td>8888888888</td>
</tr>
<tr>
<td>2</td>
<td>柯基侦探微信公众号</td>
<td>j2ee</td>
<td>互联网</td>
<td>VIP客户</td>
<td>北京市东城区</td>
<td>8888888887</td>
</tr>
<tr>
<td>3</td>
<td>柯基</td>
<td>大数据</td>
<td>互联网</td>
<td>VIP客户</td>
<td>北京市东城区</td>
<td>888888888888</td>
</tr>
</table>
</body>
</html>
⑵ 基于Jsp标签
1) 使用方式
Shiro提供了一套JSP标签库来实现页面级的授权控制, 在使用Shiro标签库前,首先需要在JSP引入shiro标签:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
2) 相关标签
标签 | 说明 |
---|---|
< shiro:guest > | 验证当前用户是否为“访客”,即未认证(包含未记住)的用户 |
< shiro:user > | 认证通过或已记住的用户 |
< shiro:authenticated > | 已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在 |
< shiro:notAuthenticated > | 未认证通过用户。与guest标签的区别是,该标签包含已记住用户 |
< shiro:principal /> | 输出当前用户信息,通常为登录帐号信息 |
< shiro:hasRole name="角色"> | 验证当前用户是否属于该角色 |
< shiro:lacksRole name="角色"> | 与hasRole标签逻辑相反,当用户不属于该角色时验证通过 |
< shiro:hasAnyRoles name="a,b"> | 验证当前用户是否属于以下任意一个角色 |
<shiro:hasPermission name=“资源”> | 验证当前用户是否拥有制定权限 |
<shiro:lacksPermission name="资源"> | 与permission标签逻辑相反,当前用户没有制定权限时,验证通过 |
3) 案例
① 修改home.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title></title>
</head>
<body>
<h6>
<a href="${pageContext.request.contextPath}/logout">退出</a>
<shiro:hasRole name="admin">
<a href="${pageContext.request.contextPath}/order-list">列表</a>
</shiro:hasRole>
<shiro:hasPermission name="order:add">
<a href="${pageContext.request.contextPath}/order-add">添加</a>
</shiro:hasPermission>
</h6>
</body>
</html>
② 测试
访问http://localhost:8080/platform/login
使用admin/123456登录
这个时候我们只能看见“列表”,看不见“添加”,点击“退出”
使用zhangsan/123456登录
这个时候我们只能看见“添加”,看不见“列表”,点击“退出”
需要注意的是,这里只是页面是否显示内容,不能防止盗链的发生