目录
一、什么是shiro?
shiro是apache旗下的一个开源框架,它将软件系统的安全认证的功能抽取出来,简单易用,实现用户身份认证,权限授权,加密、会话、管理等功能,组成了一个通用的安全认证框架。
详细了解shiro链接如下:
https://shiro.apache.org/architecture.html
如下图,红色框住的就是shiro最核心部分,图大家简单有个印象即可
若文章有错误地方,欢迎评论交流
二、第一个shiro用户认证程序
认证:我们现在应该知道了,身份认证,就是判断一个用户是否为合法用户的过程,也就是最常用的简单身份认证方式,比如登录时对用户输入的账号密码进行验证,判断是否与系统中存储的用户名和密码一致。
注:我IDEA写的代码会压缩成压缩包放在最后以百度网盘链接给出,供大家参考
shiro中对于不同对象有自己的称呼
1、Subject:主体
如登录的用户
2、Principal:身份信息
如用户的用户名,手机号,邮箱这种具有唯一标识的身份信息,主身份(Primary Principal)作为登录的信息,因为不可能所有身份都作为登录的,那多且太麻烦了,只要账号这种身份信息作为登录即可。
3、Credential:凭证信息
主体对象自己知道的安全信息,如密码。注意下面图,等下认证程序就是跟着这幅图写
构建一个Maven项目,直接构建一个quickstart的即可
建好项目就导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
接下来新建resources文件夹,放入shiro的配置文件,叫shiro.ini,这个配置文件只在刚学习的时候用,后面整合SpringBoot不用这个,这里相当于伪造数据。
[users]
xiaoming=123
zhangsan=123456
yx5411=12345
接着建立一个认证类TestAuthenticator,目录结构如下
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
public class TestAuthenticator {
//根据认证流程写
public static void main(String[] args) {
//1、创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2、给安全管理器设置realm,new IniRealm()是读取shiro.ini
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//3、给全局安全工具类SecurityUtils设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//4、关键对象subject主体
Subject subject = SecurityUtils.getSubject();
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaoming","123");
try{
System.out.println("认证状态:"+subject.isAuthenticated());
subject.login(token);//用户认证(登录)
System.out.println("认证状态:"+subject.isAuthenticated());
}catch (Exception e){
e.printStackTrace();//认证不通过会报异常
}
}
}
若将令牌的账号变成xiaobai,没有这个账号,则会报未知账户异常
如果是密码不对,就会报凭证不正确异常,也就是密码错误
于是可以专门捕获这两个异常,用于提示用户
三、自定义Realm实现
上面是用配置文件自己伪造了数据,如果我们要连接上数据库呢?这里就需要自定义Realm了,Realm就是和数据库连接的。
上面代码就是运行TestCustomerRealmAuthenticator类,假设登录输入账号密码是xiaoming,1234,由SimpleAuthenticationInfo查到数据库中对应用户名的密码是123,密码不同,就会返回凭证不正确异常(即密码错误),注意这里都是自己弄的假数据,真正连接数据库要用jdbc和mybatis。我们后面都先用CustomerRealm这种造伪数据,也就是假设从数据库中查出的,等真正整合SpringBoot再连接真正数据库。
如果改成下图,数据库中没有输入的用户,就会显示用户名错误
CustomerRealm.java
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* 自定义realm实现,将认证/授权数据的来源转为数据库的实现
*/
public class CustomerRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//token中获取用户名
String principal = (String) authenticationToken.getPrincipal();//用户名(输入的)
//根据这个身份信息,用jdbc或者mybatis和数据库中数据比对,看有没有这个用户
//下面我们就不查了,做一个伪数据,假设数据库中只有小明这一个用户数据,后续和SpringBoot整合再连接数据库,假设xiaoming是数据库中仅存的数据
if ("xiaoming".equals(principal)){
//三个参数表示从数据库中查到的账号和密码,最后一个就是Realm的名字,直接用getName即可
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
TestCustomerRealmAuthenticator.java
import com.yx.realm.CustomerRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
/**
* 使用自定义的realm
*/
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
//1、创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//2、设置自定义realm
defaultSecurityManager.setRealm(new CustomerRealm());
//3、给全局安全工具类SecurityUtils设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//4、关键对象subject主体
Subject subject = SecurityUtils.getSubject();
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaoming","1234");
try{
subject.login(token);
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();//认证不通过会报异常
System.out.println("密码错误");
}
}
}
四、MD5算法加密
我们这里是没有进行加密的,伪造查到的数据库中数据123也是明文,不安全,尤其是以后用户支付密码,那就更不能明文存储了
我们会用MD5+Salt进行加密,MD5是不可逆的,只能明文生成密文,同一个明文每次生成的密文都是一致的,有些网站所谓的MD5破解,只是将一些常见的密码,比如123456,root,888888,这种简单密码,做了一个密文数据库,如果有匹配,那就给你返回它存储的明文,但稍微难一点的都不能。所以我们在设置密码的时候,都会提示你要数字+英文字符这样这种网站就推不出来了。但是虽然网站这样提示要求,最后用户也可能输入一个aaa123456这种简单又符合输入要求的,所以就需要加密算法再提升一下,也就是用Salt,让加密更加复杂化,会在MD5加密后的字符串后面再加几个随机复杂字符。
测试一下Md5加密
import org.apache.shiro.crypto.hash.Md5Hash;
public class TestShiroMd5 {
public static void main(String[] args) {
Md5Hash md5Hash = new Md5Hash("123");//给123加密
System.out.println(md5Hash.toHex());
//Md5+salt处理
Md5Hash md5Hash1 = new Md5Hash("123","x0*7p");//这个盐这里定死了,后面一个写一个随机生成字符的方法
System.out.println(md5Hash1.toHex());
//Md5+salt处理+hash散列
Md5Hash md5Hash2 = new Md5Hash("123","x0*7p",1024);//1024表示散列1024次,更加复杂与安全
System.out.println(md5Hash2.toHex());
}
}
加密后就是16进制的32位字符串,最后一个最复杂,也最安全
两个类
TestCustomerMD5RealmAuthenticator .java
import com.yx.realm.CustomerMd5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
public class TestCustomerMD5RealmAuthenticator {
public static void main(String[] args) {
//1、创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//2、设置自定义realm
CustomerMd5Realm realm = new CustomerMd5Realm();
//设置realm使用hash凭证匹配器,用md5
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
realm.setCredentialsMatcher(hashedCredentialsMatcher);
defaultSecurityManager.setRealm(realm);
//3、给全局安全工具类SecurityUtils设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//4、关键对象subject主体
Subject subject = SecurityUtils.getSubject();
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaoming","123");
try{
subject.login(token);
System.out.println("登录成功");
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();//认证不通过会报异常
System.out.println("密码错误");
}
}
}
CustomerMd5Realm.java
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
//Md5+salt+hash
public class CustomerMd5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();//用户名(输入的)
if ("xiaoming".equals(principal)){
//三个参数表示从数据库中查到的账号和密码,最后一个就是Realm的名字,直接用getName即可
return new SimpleAuthenticationInfo(principal,"202cb962ac59075b964b07152d234b70",this.getName());
}
return null;
}
}
用MD5加密需要在设置自定义realm改一改,加一个设置md5,因为默认不是md5,相当于告诉程序用md5加密,运行TestCustomerMD5RealmAuthenticator 类
CustomerMd5Realm.java加入salt
同样运行TestCustomerMD5RealmAuthenticator 类
这里他会自动加上盐,再与数据库中这个加密的字符串比较
再加上Hash散列呢,散列可是散列1024次
TestCustomerMD5RealmAuthenticator 类中加上散列多少次,相当于告诉密码散列了多少次
以上就是MD5+Salt+Hash散列加密方式,我们再把核心代码贴出来
public class TestCustomerMD5RealmAuthenticator {
public static void main(String[] args) {
//1、创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//2、设置自定义realm
CustomerMd5Realm realm = new CustomerMd5Realm();
//设置realm使用hash凭证匹配器,用md5
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(hashedCredentialsMatcher);
defaultSecurityManager.setRealm(realm);
//3、给全局安全工具类SecurityUtils设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//4、关键对象subject主体
Subject subject = SecurityUtils.getSubject();
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaoming","123");
try{
subject.login(token);
System.out.println("登录成功");
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();//认证不通过会报异常
System.out.println("密码错误");
}
}
}
//MD5+Salt+Hash
public class CustomerMd5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();//用户名(输入的)
if ("xiaoming".equals(principal)){
//三个参数表示从数据库中查到的账号和密码,最后一个就是Realm的名字,直接用getName即可
return new SimpleAuthenticationInfo(principal,
"4d41fdb34ea9f4b5dd2b890a3b89943e",
ByteSource.Util.bytes("x0*7p"),
this.getName());
}
return null;
}
}
五、shiro中的授权
之前是认证,也就是登录进行认证,现在是授权,也就是访问权限,不同用户具有不同的访问权限,如对某些用户密码修改权限,只能管理员才能有。授权是基于认证的,因为你只有合法登录通过,你才有一些访问权限。
两种授权方式:1、基于角色的访问控制 2、基于资源的访问控制
我们直接来实践一下,再来讲解,我们接着用md5+salt
+hash的认证后面加内容,因为授权要先认证通过,也就是要先登录通过。
CustomerMd5Realm.java
首先基于角色的访问控制,假设xiaoming有两个角色,admin和user,后面SpringBoot整合真正写数据库,现在先造这两个数据,一般用户有哪些角色都会存在数据库中。
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
//Md5+salt+hash
public class CustomerMd5Realm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();//用户名
System.out.println("身份信息:"+primaryPrincipal);
//将数据库查询的角色信息赋值给权限对象,这里先用伪数据
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");//管理员角色
simpleAuthorizationInfo.addRole("user");//用户角色
return simpleAuthorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();//用户名(输入的)
if ("xiaoming".equals(principal)){
//三个参数表示从数据库中查到的账号和密码,最后一个就是Realm的名字,直接用getName即可
return new SimpleAuthenticationInfo(principal,
"4d41fdb34ea9f4b5dd2b890a3b89943e",
ByteSource.Util.bytes("x0*7p"),
this.getName());
}
return null;
}
}
TestCustomerMD5RealmAuthenticator.java
import com.yx.realm.CustomerMd5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import java.util.Arrays;
public class TestCustomerMD5RealmAuthenticator {
public static void main(String[] args) {
//认证
//1、创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//2、设置自定义realm
CustomerMd5Realm realm = new CustomerMd5Realm();
//设置realm使用hash凭证匹配器,用md5
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(hashedCredentialsMatcher);
defaultSecurityManager.setRealm(realm);
//3、给全局安全工具类SecurityUtils设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//4、关键对象subject主体
Subject subject = SecurityUtils.getSubject();
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaoming","123");
try{
subject.login(token);
System.out.println("登录成功");
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();//认证不通过会报异常
System.out.println("密码错误");
}
//授权
if(subject.isAuthenticated()){
//基于角色权限控制
System.out.println(subject.hasRole("admin"));
System.out.println("--------------------");
//基于多角色权限控制(要都有,一个没有都不行)
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));
System.out.println(subject.hasAllRoles(Arrays.asList("super","user")));
System.out.println("--------------------");
//基于多角色权限控制(只要有其中一个)
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "super"));
for (boolean b : booleans) {
System.out.println(b);
}
}
}
}
如下基于角色的结果,可以用hasRole、hasAllRoles、hasRoles
再来讲基于权限的访问控制
资源标识符:操作:资源类型,操作就是创建+增删改查,举个例子,xiaoming依然有两个角色,admin和user。数据库中查询到的也有两个权限,user:*:yx5411
和 product:create:*
,意思分别是user对yx5411这个用户数据有所有操作权限,product对所有商品都有创建权限
CustomerMd5Realm.java授权部分代码,这里用于造数据,后面整合SpringBoot再连接真正数据库,先用伪数据
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();//用户名
System.out.println("身份信息:"+primaryPrincipal);
//将数据库查询的角色信息赋值给权限对象,这里先用伪数据
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");//管理员角色
simpleAuthorizationInfo.addRole("user");//用户角色
//将数据库查询的权限信息赋值给权限对象,这里先用伪数据,表示用户对yx5411有操作权限,即增删改查的权限
simpleAuthorizationInfo.addStringPermission("user:*:yx5411");
simpleAuthorizationInfo.addStringPermission("product:create:*");
return simpleAuthorizationInfo;
}
TestCustomerMD5RealmAuthenticator.java授权部分代码
//授权
if(subject.isAuthenticated()){
//基于角色权限控制
System.out.println(subject.hasRole("admin"));
System.out.println("--------------------");
//基于多角色权限控制(要都有,一个没有都不行)
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));
System.out.println(subject.hasAllRoles(Arrays.asList("super","user")));
System.out.println("--------------------");
//基于多角色权限控制(只要有其中一个)
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "super"));
for (boolean b : booleans) {
System.out.println(b);
}
System.out.println("--------------------");
//基于权限的访问控制,资源标识符:操作:资源类型,操作就是创建+增删改查
System.out.println("权限:"+subject.isPermitted("user:*:*"));//xiaoming对所有用户都有操作权限吗?false
System.out.println("权限:"+subject.isPermitted("user:*:yx"));//xiaoming对yx用户都有操作权限吗?false
System.out.println("权限:"+subject.isPermitted("user:*:yx5411"));//xiaoming对yx5411用户都有操作权限吗?true
System.out.println("-----------------------");
System.out.println("权限:"+subject.isPermitted("product:update:*"));//product:update:*也可以写成product:update
System.out.println("权限:"+subject.isPermitted("product:create"));
System.out.println("----------------");
//分别具有哪些权限
boolean[] booleans1 = subject.isPermitted("user:*:aaa", "user:*:yx5411","product:update");
for (boolean b : booleans1) {
System.out.println(b);
}
System.out.println("------------------");
//同时具有哪些权限
System.out.println(subject.isPermittedAll("user:*:yx5411", "product:create:*"));
}
六、shiro整合SpringBoot
之前用的数据都是在Realm自己造的,现在我们真正整合SpringBoot创建数据库。shiro会将所有页面请求都拦截,然后进行安全判断,通过才放行,这个通过也分认证通过和授权通过。
6.1 环境搭建
我们先用Jsp+SpringBoot进行演示,不用纠结于Jsp过时问题,jsp页面只是一个静态资源,换成html也一样,也不搞啥前后端分离了,这只是为了整合起来学习shiro,后面还会写Thymeleaf+SpringBoot版本。
首先IDEA创建一个SpringBoot项目,不过多赘述,大家这个肯定没问题
main下新建webapp然后创建一个index.jsp,这三部分下面贴上代码
index.jsp
<%@ page contentType="text/html; charset=utf-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title></title>
<meta name="keywords" content="">
<meta name="description" content="">
</head>
<body>
<h1>系统主页</h1>
<ul>
<li><a href="">用户管理</a></li>
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">物流管理</a></li>
</ul>
</body>
</html>
login.jsp
<%@ page contentType="text/html; charset=utf-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title></title>
<meta name="keywords" content="">
<meta name="description" content="">
</head>
<body>
<h1>用户登录界面</h1>
</body>
</html>
application.properties
server.port=8080
server.servlet.context-path=/shiro
spring.application.name=shiro
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yx</groupId>
<artifactId>shiro-springboot-jsp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro-springboot-jsp</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--jsp解析依赖-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
配置一下工作目录,jsp需要弄下这个
运行ShiroSpringbootJspApplication,输入地址,SpringBoot环境搭建成功
接下来整合shiro
首先引入依赖
<!--Shiro-SpringBoot依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
然后进行shiro配置
ShiroConfig.java
import com.yx.shiro.realms.CustomerRealm;
import org.apache.shiro.realm.Realm;
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;
import java.util.Map;
@Configuration
public class ShiroConfig {
//1、创建shiroFilter(用于拦截所有请求)
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统的受限资源和公共资源
Map<String,String> map = new HashMap<>();
map.put("/index.jsp","authc");//index.jsp要求认证,没有认证就会默认跳转到/login.jsp页面,这个也可以设置,用shiroFilterFactoryBean.setLoginUrl("跳转地址");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//2、创建web安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(getRealm());
return defaultWebSecurityManager;
}
//3、创建自定义realm
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
return customerRealm;
}
}
常用anon和authc,也就是所有人都可访问和必须认证登录后才能访问
CustomerRealm.java,现在还没有做操作,只是有基本框架
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
//自定义Realm
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
重新运行,然后输入地址 http://localhost:8080/shiro/index.jsp,发现会自动跳转到login.jsp页面,这是由于设置了index.jsp页面需要认证,没有认证默认会跳转到login.jsp,这个默认跳转的界面可以自己设置,不设置默认是login.jsp
接下来我们做一个真正的登录页面,如下,将body替换一下即可
login.jsp
<body>
<h1>用户登录界面</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
<tr>
<td>用户:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码</td>
<td><input type="text" name="password"></td>
</tr>
<input type="submit" value="登录">
</form>
</body>
ShiroConfig也需要改变一点,主要就是map.put,拦截所有/**,只让/user/login是公共资源,可以放行,不拦截
//1、创建shiroFilter(用于拦截所有请求)
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统的受限资源和公共资源
Map<String,String> map = new HashMap<>();
map.put("/user/login","anon");//login为公共资源,不要认证,不写这个将login也拦截,那么就会一直在login.jsp界面跳转
map.put("/**","authc");//拦截所有,所有都要认证
//map.put("/index.jsp","authc");//index.jsp要求认证,没有认证就会默认跳转到/login.jsp页面,这个也可以设置,用shiroFilterFactoryBean.setLoginUrl("跳转地址");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
UserController.java负责用户登录的业务逻辑,登录就是获取subject主体对象,也就是登录的对象,然后subject.login()进行登录认证
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("user")
public class UserController {
@RequestMapping("loginout")
public String loginout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login.jsp";
}
/**
* 身份认证
* @param username
* @param password
* @return
*/
@RequestMapping("login")
public String login(String username,String password){
//用shiro来做安全认证
//获取主题对象
Subject subject = SecurityUtils.getSubject();//会自动调认证相关的,也就是会到CustomerRealm中的认证部分
try{
subject.login(new UsernamePasswordToken(username,password));//登录认证
return "redirect:/index.jsp";//登录成功,用户信息会放在shiro的缓存
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
return "redirect:/login.jsp";
}
}
index.jsp加了一个登出
<body>
<h1>系统主页</h1>
<ul>
<li><a href="">用户管理</a></li>
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">物流管理</a></li>
</ul>
<a href="${pageContext.request.contextPath}/user/loginout">退出登录</a>
</body>
箭头指向的就是改动的
页面输入 http://localhost:8080/shiro/login.jsp
,依然先用下造的假数据xiaoming和123
点击登录后台会报出用户名错误异常
用户名正确密码错误,点击登录,后台会出现密码错误异常
用户名密码都对,登录成功
点击退出登录,那么执行subject.logout()方法,会清除shiro存的用户信息缓存,然后跳转到登录界面
6.2 shiro连接数据库实现MD5+Salt加密认证
我们这节完成注册,注册时对密码进行加密,然后进行登录操作,注册界面也是公共资源,所以也要不拦截,到ShiroConfig配置一下,中间两个加注册的
map.put("/user/login","anon");//login为公共资源,不要认证,不写这个将login也拦截,那么就会一直在login.jsp界面跳转
map.put("/user/register","anon");
map.put("/register.jsp","anon");//注册界面放行
map.put("/**","authc");//拦截所有,所有都要认证
register.jsp
<%@ page contentType="text/html; charset=utf-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title></title>
<meta name="keywords" content="">
<meta name="description" content="">
</head>
<body>
<h1>用户注册界面</h1>
<form action="${pageContext.request.contextPath}/user/register" method="post">
<tr>
<td>用户:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码</td>
<td><input type="text" name="password"></td>
</tr>
<input type="submit" value="注册">
</form>
</body>
</html>
输入地址
创建数据库shiro,然后在数据库中创建t_user表
创建成功后导入相关依赖
<!--mybatis-->
<dependency>
<groupId>org.apache.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.3</version>
</dependency>
然后写业务逻辑
UserDao.java
import com.yx.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface UserDao {
void add(User user);
}
UserDaoMapper.xml这个文件创建文件夹别com.yx.mapper创建,要com/yx/mapper才会有层级,不然就是一个目录名字
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yx.dao.UserDao">
<insert id="add" parameterType="User">
insert into t_user values(#{id},#{username},#{password},#{salt})
</insert>
</mapper>
UserService.java
import com.yx.entity.User;
public interface UserService {
void register(User user);
}
UserServiceImpl.java
import com.yx.dao.UserDao;
import com.yx.entity.User;
import com.yx.utils.SaltUtils;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class UserServiceImpl implements UserService{
@Autowired
private UserDao userDao;
@Override
public void register(User user) {
//加密md5+salt+hash
String salt = SaltUtils.getSalt(6);//生成8位随机盐
user.setSalt(salt);//保存盐数据
Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
user.setPassword(md5Hash.toHex());
userDao.add(user);
}
}
SaltUtils随即盐生成的代码,自己封装的
import java.util.Random;
//生成随机盐,随机字符串
public class SaltUtils {
public static String getSalt(int n){
String code="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
char[] chars = code.toCharArray();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < n; i++) {
char aChar = chars[new Random().nextInt(chars.length)];
sb.append(aChar);
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(getSalt(8));
}
}
UserController中加注册的代码
@Autowired
private UserService userService;
@RequestMapping("register")
public String register(User user){
try{
userService.register(user);
return "redirect:/login.jsp";//注册成功则跳转
}catch (Exception e){
e.printStackTrace();
return "redirect:/register.jsp";
}
}
输入 http://localhost:8080/shiro/register.jsp
点击注册,成功则跳转到登录页面
数据库中出现注册的数据
下面来登录认证
首先需要在UserDao.java和UserService.java中加入下面的方法,用于查找数据库中是否存在用户方法
User findByUserName(String username);
UserDaoMapper.xml加入
<select id="findByUserName" parameterType="String" resultType="User">
select * from t_user
where username=#{username}
</select>
UserServiceImpl.java进行实现
@Override
public User findByUserName(String username) {
return userDao.findByUserName(username);
}
我们都知道数据在CustomerRealm中做出的假数据,现在我们要从真的数据库中查找数据了,怎么做呢?
我们知道所有对象在SpringBoot中都是以工厂形式加载,所以我们需要获取加载到工厂的UserService这个bean对象
写一个工具类ApplicationContextUtils.java,用于获取UserService这个bean对象,以便用UserService中findByUserName方法
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
//根据bean的名字获取指定的bean对象
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
CustomerRealm.java
import com.yx.entity.User;
import com.yx.service.UserService;
import com.yx.utils.ApplicationContextUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
//自定义Realm
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
//工厂中获取UserService对象
UserService userService = (UserService) ApplicationContextUtils.getBean("userServiceImpl");//UserServiceImpl对应Bean对象名字默认第一个字母小写
User user = userService.findByUserName(principal);
if (user != null){
return new SimpleAuthenticationInfo(principal,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());//找到对应用户就会进行比较密码
}
return null;
}
}
ShiroConfig.java配置一下凭证校验匹配器,这样可以让登录表单输入的密码进行md5+salt+hash加密,然后再和数据库中的加密的密码比较
//3、创建自定义realm
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密算法,这样输入的密码才会进行加密,否则就是输入的和数据库中加密的比较,肯定不对
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
输入登录地址 http://localhost:8080/shiro/login.jsp
我们也可以再注册一个用户测试一下,在index.jsp加入欢迎您,某用户
index.jsp加一个${msg}
重新运行,再进注册界面注册
成功
6.3 shiro整合SpringBoot授权
授权就是对菜单功能进行授权,不同用户权限不同,比如普通用户和管理的权限就不同,普通用户只能看到下面三个,管理能操作全部,即也可以看到普通用户,对普通用户进行操作
CustomerRealm.java对授权部分写对应角色,造假数据,假设yx5411是user普通用户
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
if ("yx5411".equals(primaryPrincipal)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("user");
return simpleAuthorizationInfo;
}
return null;
}
index.jsp改动,加上<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
和<shiro:hasRole name="admin"></shiro:hasRole>
标签,使得必须具有对应角色才能看到对应功能
<%@ page contentType="text/html; charset=utf-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title></title>
<meta name="keywords" content="">
<meta name="description" content="">
</head>
<body>
<h1>系统主页,欢迎您,${msg}</h1>
<ul>
<shiro:hasRole name="admin">
<li><a href="">用户管理</a></li>
</shiro:hasRole>
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">物流管理</a></li>
</ul>
<a href="${pageContext.request.contextPath}/user/loginout">退出登录</a>
</body>
</html>
我们以yx5411登录,由于是普通用户,所以只有下面三个权限
CustomerRealm中改成admin用户具有admin角色权限
重新运行,admin登录
也可以写多个,就是user,admin都可以看到并操作
index.jsp
<body>
<h1>系统主页,欢迎您,${msg}</h1>
<ul>
<shiro:hasRole name="admin">
<li><a href="">用户管理</a>
<ul>
<shiro:hasPermission name="admin:add:*">
<li>添加</li>
</shiro:hasPermission>
<shiro:hasPermission name="admin:update:*">
<li>修改</li>
</shiro:hasPermission>
<shiro:hasPermission name="admin:delete:*">
<li>删除</li>
</shiro:hasPermission>
<shiro:hasPermission name="admin:find:*">
<li>查询</li>
</shiro:hasPermission>
</ul>
</li>
</shiro:hasRole>
<shiro:hasAnyRoles name="user,admin">
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">物流管理</a></li>
</shiro:hasAnyRoles>
</ul>
<a href="${pageContext.request.contextPath}/user/loginout">退出登录</a>
</body>
CustomerRealm
这样就可以再细致的设置权限,这是标签形式,shiro还提供注解方式@RequiresRoles和@RequiresPermissions
,并且我们再加上数据库
总共有三个表:用户、角色、权限,下面用图形式展示三者关系
shiro数据库中建立表格,id为主键非空自增
两个角色,用户和管理员
下面是用户表比如yx5411的id为1,它是用户角色,那么我们到t_user_role添加这个,admin的id为2,它既是管理又是用户
然后数据库搞定就是实体,dao,service一顿操作,由于改动较多,我就不一个一个贴出来了,后面直接给代码+数据库。这里大家可以看视频讲解,链接如下https://www.bilibili.com/video/BV1uz4y197Zm?p=19
核心sql
SELECT u.id uid,u.username uname,r.id rid,r.name rname
FROM t_user u
LEFT JOIN t_user_role ur ON u.id=ur.userid
LEFT JOIN t_role r ON r.id=ur.roleid
WHERE u.username='admin'
不同用户看到的内容不同,授权角色就演示成功,接下来是授权权限的
改动比较多
可参考视频讲解
https://www.bilibili.com/video/BV1uz4y197Zm?p=20
核心sql
SELECT p.*,r.name FROM t_role r
LEFT JOIN t_role_permission rp ON r.id=rp.roleid
LEFT JOIN t_permission p ON p.id=rp.permissionid
WHERE r.id=1
6.4 代码+数据库+up主视频讲解
代码+数据库
链接:https://pan.baidu.com/s/1QmRByAzhdReJBI_iixBz2A
提取码:k3le