shiro官方文档:传送门
1. 权限的管理
目前主流的解决权限管理的方案:①Apache基金会开源的Shiro框架;②Spring技术栈中的SpringSecurity
1.1 什么是权限管理
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制
,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证
和授权
两部分,简称认证授权
。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
1.2 什么是身份认证
身份认证
,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
1.3 什么是授权
授权,即访问控制
,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
2. 什么是shiro
Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Shiro 是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的web和企业应用程序。
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
3.shiro的核心架构
3.1 简介
3.1.1 Subject
Subject
即主体,外部应用与subject
进行交互,subject
记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。
Subject
在shiro
中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject
进行认证授,而subject
是通过SecurityManager
安全管理器进行认证授权。
3.1.2 Security Manager
Security Manager
(安全管理器)是架构图中最重要的部分,在使用shiro
进行权限管理时,首先就要拿到Security Manager
,Security Manager
包裹了整个shiro
中的所有功能。
SecurityManager
对全部的subject
进行安全管理,它是shiro
的核心。通过SecurityManager
可以完成subject
的认证、授权等,实质上SecurityManager
是通过Authenticator
进行认证,通过Authorizer
进行授权,通过SessionManager
进行会话管理等。
SecurityManager
是一个接口,继承了Authenticator
, Authorizer
, SessionManager
这三个接口。
下面是对Security Manager
中各个模块的介绍:
Authenticator
:用于做认证
。Authenticator
是一个接口,shiro
提供ModularRealmAuthenticator
实现类,通过ModularRealmAuthenticator
基本上可以满足大多数需求,也可以自定义认证器。
Authorizer
:用于实现授权
。用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
Session Manager
:在web
环境下。因为shiro
要帮我们解决用户认证和用户授权。用户在认证通过之后,原来在没有用权限管理时,需要把用户标志存储在对应的服务器上(以会话的形式存储用户(典型的就是session对象
)),用来对用户的会话进行追踪。如果用shiro
进行用户认证,就需要把会话交给Session Manager
进行管理。所以Session Manager
是管理整个shiro
在web
环境下的核心会话的。注意:在写本地案例的时候是不具有web
功能的。
SessionDAO
:对会话数据进行crud
的一组操作接口。用于操作Session Manager
中的数据,实现数据的持久化,以及内存的存储。
Cache Manager
:用于缓存用户认证和授权的数据。e.g.
:用户认证通过之后,需要对用户进行授权。用户的每一次操作,都需要拿当前用户的标志去对应数据库中查看是否有操作权限。如果用户每操作一次,就调数据库验证一次权限,就无疑增加了磁盘IO
的次数,数据库的压力也会变大。在数据不发生变化的情况下,用户授权仅在第一次会调用数据库,拿到数据之后统一放到Cache Manager
中,以后再对用户进行授权,直接在Cache Manager
中获取数据,减少了数据库磁盘IO
的次数。
Pluggable Realms
:具体做认证和授权的数据的调配。获取认证和授权的相应数据,并完成相应的认证和授权操作。Pluggable Realms
相当于datasource
数据源,securityManager
进行安全认证需要通过Realm
获取用户权限数据,比如:如果用户身份数据在数据库那么realm
就需要从数据库获取用户身份信息。
3.1.3 Cryptography
Cryptography
即密码管理,shiro
提供了一套加密/解密
的组件,方便开发。比如提供常用的散列、加/解密等功能。
4. shiro中的认证
4.1 认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
4.2 shiro中认证的关键对象
Subject:主体
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
Principal:身份信息
是主体(subject)进行身份认证的标识,标识必须具有
唯一性
,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。credential:凭证信息
是只有主体自己知道的安全信息,如密码、证书等。
4.3 认证流程
4.4 认证的开发
4.4.1 创建项目并引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
4.4.2 shiro配置文件
注意:shiro
的配置文件是以.ini
结尾的,相当于.txt
,.ini
是专门用于做配置的文件,其中开以写比较复杂的数据格式。
注意:在实际的项目开发中并不会使用这种方式,这种方法可以用来初学时练手。
.ini
配置文件是用来学习shiro
时书写系统中相关的权限数据。
[users]
prannt=123
mano=456
lucy=789
4.4.3 开发认证代码
public class TestAuthenticator {
public static void main(String[] args) {
// 1.创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2.给安全管理器设置Realms
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// 3.SecurityUtils 全局安全工具类,该工具类提供了认证、退出等方法
// 给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 4.关键对象:subject:主体
Subject subject = SecurityUtils.getSubject();
// 5.创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("prannt","123");
try {
System.out.println("认证状态:" + subject.isAuthenticated());
subject.login(token); // 用户认证
System.out.println("认证状态:" + subject.isAuthenticated());
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("认证失败,用户名不存在!");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败,密码不存在!");
}
}
}
4.4.4 常见的异常类型
- DisabledAccountException(帐号被禁用)
- LockedAccountException(帐号被锁定)
- ExcessiveAttemptsException(登录失败次数过多)
- ExpiredCredentialsException(凭证过期)等
4.5 认证流程源码
首先在subject.login(token);
这一行打个断点,使用debug
方式启动。
查看流程:
subject.login(token);
→ this.clearRunAsIdentitiesInternal();
→ Subject subject = this.securityManager.login(this, token);
→ info = this.authenticate(token);
→ return this.authenticator.authenticate(token);
→ if (token == null)
→ info = this.doAuthenticate(token);
→ this.assertRealmsConfigured();
→Collection<Realm> realms = this.getRealms();
→ return realms.size() == 1 ?...
→ AuthenticationInfo info = realm.getAuthenticationInfo(token);
→ AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
→ if (info == null)
→ info = this.doGetAuthenticationInfo(token);
→ UsernamePasswordToken upToken = (UsernamePasswordToken)token;
→ SimpleAccount account = this.getUser(upToken.getUsername());
→ this.USERS_LOCK.readLock().lock();
→ var2 = (SimpleAccount)this.users.get(username);
→ SimpleAccount account = this.getUser(upToken.getUsername());
→ if (account != null)
→ if (account.isLocked())
→ if (account.isCredentialsExpired())
总结:认证最终执行用户名比较,是在SimpleAccountRealm
类中的doGetAuthenticationInfo()
方法,在这个方法中完成了用户名的校验。
return account;
→ info = this.doGetAuthenticationInfo(token);
→ CredentialsMatcher cm = this.getCredentialsMatcher();
总结:最终的密码校验是在AuthenticatingRealm
中的assertCredentialsMatch()
方法
4.6 自定义Realm
通过分析源码可得:
- 最终执行用户名比较是 在SimpleAccountRealm类 的 doGetAuthenticationInfo 方法中完成用户名校验
- 最终密码校验是在 AuthenticatingRealm类 的 assertCredentialsMatch方法中
总结:
AuthenticatingRealm 认证realm doGetAuthenticationInfo
AuthorizingRealm 授权realm doGetAuthorizationInfo
自定义Realm
的作用:放弃使用.ini文件,使用数据库查询
上边的程序使用的是Shiro自带的IniRealm
,IniRealm
从.ini
配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm
。
源码:
public class SimpleAccountRealm extends AuthorizingRealm {
// ...省略
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}
}
package com.prannt.realm;
/**
* @Description 自定义realm实现,将认证/授权数据的来源转换为数据库的实现
* @Date 2022/1/21 14:39
* @Created Prannt
*/
public class CustomerRealm extends AuthorizingRealm {
// 做授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 做认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 在token中获取用户名
String principal = (String) token.getPrincipal(); // return this.getUsername();
System.out.println(principal);
// 根据身份信息使用mybatis查询相关数据库
if ("prannt".equals(principal)){
// 参数1:返回数据库中正确的用户名;参数2:返回数据库中的正确密码;参数3:提供当前realm的名字 this.getName())
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,"123",this.getName());
return info;
}
return null;
}
}
// 使用自定义realm
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
// 1.创建安全管理器对象:SecurityManager
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2.设置自定义realm
securityManager.setRealm(new CustomerRealm());
// 3.将安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 4.通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();
// 5.创建token(用户在浏览器上输入的用户名、密码)
UsernamePasswordToken token = new UsernamePasswordToken("prannt", "123");
try {
subject.login(token);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("认证失败,用户名错误!");
} catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败,密码错误!");
}
}
}
// 结果:
prannt
4.7 MD5+Salt+Hash
4.7.1 MD5算法
作用:一般用来加密或者签名(检验和,校验两个文件内容是否一致)
特点:
- MD5算法不可逆,即只能根据明文算出密文,不能根据密文反推出明文
- 如果内容相同,无论执行多少次MD5,生成的结果始终是相同的
面试题:如何比较aa.txt
和bb.txt
的内容是否一致?可以使用MD5,MD5可以对文件的内容进行校验,检验之后如果两个文件生成的值相等,那么这两个文件的内容一定相同。
“123” → fe1dsadsad…
有些破解MD5的网站,实际上是使用了穷举的做法。把一些常用的字符串转换为MD5,有人搜索的时候,直接从数据库中查询该数据。
生成结果:始终是一个16进制32位长度字符串。
4.7.2 随机盐
在原来明文的基础上随机拼接一个字符串,再进行MD5加密。
4.7.3 MD5的基本使用
public class TestShiroMD5 {
// MD5
@Test
public void md5Test(){
Md5Hash md5Hash = new Md5Hash("123");
String s = md5Hash.toHex(); // 转为16进制
System.out.println(s); // 202cb962ac59075b964b07152d234b70
}
// MD5 + Salt
@Test
public void md5SaltTest(){
Md5Hash md5Hash = new Md5Hash("123","X0*7ps");
System.out.println(md5Hash.toHex()); // 8a83592a02263bfe6752b2b5b03a4799
}
// MD5 + Salt + Hash散列(参数代表要散列多少次,一般是1024或2048)
@Test
public void md5SaltHashTest(){
Md5Hash md5Hash = new Md5Hash("123","X0*7ps",1024);
System.out.println(md5Hash.toHex()); // e4f9bf3e0c58f045e62c23c533fcf633
}
}
4.7.4 MD5 + Salt
package com.prannt.realm;
// 使用自定义realm加入MD5 + Salt + Hash散列
public class CustomerMD5Realm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户名
String principal = (String) token.getPrincipal();
String username="prannt";
String password="e4f9bf3e0c58f045e62c23c533fcf633"; // 123加密后
// 根据用户名查询数据库(模拟)
if (username.equals(principal)){
// 参数1:数据库用户名
// 参数2:数据库md5+salt之后的密码
// 参数3:注册时的随机盐
// 参数4:realm的名字
return new SimpleAuthenticationInfo(principal,
password,
ByteSource.Util.bytes("X0*7ps"),
this.getName());
}
return null;
}
}
package com.prannt;
public class TestCustomerMD5RealmAuthenticator {
public static void main(String[] args) {
// 1.创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2.注入realm
CustomerMD5Realm realm = new CustomerMD5Realm();
// 3.设置realm使用Hash凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 声明:使用的算法
credentialsMatcher.setHashAlgorithmName("md5");
// 声明:散列次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
securityManager.setRealm(realm);
// 4.将安全管理器注入安全工具类
SecurityUtils.setSecurityManager(securityManager);
// 5.通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();
// 6.认证
UsernamePasswordToken token = new UsernamePasswordToken("prannt", "123");
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("认证失败,用户名错误!");
} catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败,密码错误!");
}
}
}
5.shiro中的授权
5.1 授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
5.2 关键对象
授权可简单理解为who对what(which)进行How操作:
Who,即主体(Subject)
,主体需要访问系统中的资源。
What,即资源(Resource)
,如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型
和资源实例
,比如商品信息为资源类型
,类型为t01
的商品为资源实例
,编号为001
的商品信息也属于资源实例
。
How,权限/许可(Permission)
,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
5.3 授权流程
5.4 授权方式
5.4.1 基于角色的访问控制
RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
// 伪代码
if(subject.hasRole("admin")){ // 如果系统中具有admin角色
// 操作什么资源
}
5.4.2 基于资源的访问控制
RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
if(subject.isPermission("user:update:01")){ // 资源实例,谁(user)对什么资源(01)有什么样的操作权限(update)
// user对资源01用户具有修改的权限
}
if(subject.isPermission("user:update:*")){ // 资源类型
// 对所有的资源 用户具有更新的权限
}
5.5 权限字符串
权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。
e.g.
- 用户创建权限:user:create,或user:create:*
- 用户修改实例001的权限:user:update:001
- 用户实例001的所有权限:user:*:001
5.6 shiro中授权编程实现方式
5.6.1 编程式
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
// 有权限
} else {
// 无权限
}
5.6.2 注解式
@RequiresRoles("admin")
public void hello() {
// 有权限
}
5.6.3 标签式
JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
<!— 有权限—>
</shiro:hasRole>
注意: Thymeleaf 中使用shiro需要额外集成!
5.7 开发授权
CustomerMD5Realm
package com.prannt.realm;
// 使用自定义realm加入MD5 + Salt + Hash散列
public class CustomerMD5Realm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principles) {
System.out.println("==================");
String primaryPrincipal = (String) principles.getPrimaryPrincipal();
System.out.println("身份信息:" + primaryPrincipal);
// 根据身份信息(用户名)获取当前用户的角色信息以及权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 将数据库中查询的角色信息赋值给权限对象
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");
// 将数据库中查询的权限信息赋值给权限对象
simpleAuthorizationInfo.addStringPermission("user:*:01");
simpleAuthorizationInfo.addStringPermission("product:create");
return simpleAuthorizationInfo;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户名
String principal = (String) token.getPrincipal();
String username="prannt";
String password="e4f9bf3e0c58f045e62c23c533fcf633"; // 加密后
// 根据用户名查询数据库(模拟)
if (username.equals(principal)){
// 参数1:数据库用户名
// 参数2:数据库md5+salt之后的密码
// 参数3:注册时的随机盐
// 参数4:realm的名字
return new SimpleAuthenticationInfo(principal,
password,
ByteSource.Util.bytes("X0*7ps"),
this.getName());
}
return null;
}
}
TestCustomerMD5RealmAuthenticator
package com.prannt;
/**
* @Description 测试
* @Date 2022/1/21 17:34
* @Created Prannt
*/
public class TestCustomerMD5RealmAuthenticator {
public static void main(String[] args) {
// 1.创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2.注入realm
CustomerMD5Realm realm = new CustomerMD5Realm();
// 3.设置realm使用Hash凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 声明:使用的算法
credentialsMatcher.setHashAlgorithmName("md5");
// 声明:散列次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
securityManager.setRealm(realm);
// 4.将安全管理器注入安全工具类
SecurityUtils.setSecurityManager(securityManager);
// 5.通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();
// 6.认证
UsernamePasswordToken token = new UsernamePasswordToken("prannt", "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()) {
// 1.基于单角色的权限控制
boolean flag = subject.hasRole("admin");
System.out.println(flag);
// 2.基于多角色的权限控制(同时具有多个角色)
boolean flags = subject.hasAllRoles(Arrays.asList("admin", "user"));
System.out.println(flags);
// 3.是否具有其中一个角色
boolean[] roles = subject.hasRoles(Arrays.asList("admin", "super", "user"));
for (boolean role : roles) {
System.out.println(role);
}
System.out.println("===========分割线============");
// 4.基于权限字符串的访问控制 资源标志符:操作:资源类型
System.out.println("权限:" + subject.isPermitted("user:update:01"));
System.out.println("权限:" + subject.isPermitted("product:create:02"));
System.out.println("==========分割线2==========");
// 5.分别具有哪些权限
boolean[] permitted = subject.isPermitted("user:*:01", "order:*:10");
for (boolean b : permitted) {
System.out.println(b);
}
// 6.同时具有哪些权限
boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01");
System.out.println(permittedAll);
}
}
}
6. 整合SpringBoot项目实战
6.1 整合思路
6.2 环境配置
6.2.1 创建项目
就是个普通的SpringBoot
工程。在main
目录下创建目录webapp
,在其中新建index.jsp
文件,输入!
然后tab
可以生成jsp
模板,在body
中写hello world
。
6.2.2 引入依赖
<!--引入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>
<!--引入shiro整合Springboot依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
6.2.3 修改视图
application.properties 文件
server.port=8888
server.servlet.context-path=/shiro
spring.application.name=shiro
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
6.2.4 修改配置
JSP 与IDEA 与SpringBoot存在一定的不兼容,修改此配置即可解决
本工程使用
SpringBoot 2.4.3
,不需要此步骤,可以直接访问到页面。
6.2.5 首页和登录页
index.jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>系统主页v1.0(受限资源)</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; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>用户登录(公共资源)</h1>
</body>
</html>
6.2.6 访问页面
http://localhost:8888/shiro/index.jsp
6.3 简单使用
6.3.1 创建配置类
package com.prannt.springboot_jsp_shiro.config;
/**
* @Deception 整合shiro框架相关的配置类
* shiroFilterFactoryBean依赖于defaultWebSecurityManager
* @Date 2022/1/23 12:37
* @Created Prannt
*/
@Configuration
public class ShiroConfig {
// 1.创建shiroFilter,负责拦截所有请求
@Bean // 是工厂中的一个filter对象,通过参数注入defaultWebSecurityManager
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 配置系统的受限资源
Map<String,String> map = new HashMap<>();
map.put("/index.jsp","authc"); // 两个参数都是受限资源,authc表示请求这个资源需要认证和授权
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 配置系统的公共资源,不需要配置,因为除了受限资源就是公共资源
// 默认认证界面路径,不写路径默认就是login.jsp,也可以指定其他页面为默认路径
// shiroFilterFactoryBean.setLoginUrl("/login.jsp");
return shiroFilterFactoryBean;
}
// 2.创建shiroFilter需要的安全管理器
@Bean //是工厂中的一个对象
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
// 3.创建自定义realm
@Bean //是工厂中的一个对象
public Realm getRealm(){
return new CustomerRealm();
}
}
6.3.2 自定义realm
package com.prannt.springboot_jsp_shiro.shiro.realms;
/**
* @Deception 自定义realm
* @Date 2022/1/23 13:00
* @Created Prannt
*/
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
6.4 常见过滤器
注意: shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限:
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名访问(访问时不需要认证授权) |
authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取username 、password ,rememberMe 等参数并尝试登录,如果登录不了就会跳转到loginUrl 配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 |
logout | LogoutFilter | 登出过滤器,配置指定url就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
port | PortFilter | 需要指定端口才能访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要https请求才能访问 |
user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
6.5 认证和退出实现
login.jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>用户登录(公共资源)</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
提交:<input type="submit" value="登录">
</form>
</body>
</html>
index.jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>系统主页v1.0(受限资源)</h1>
<a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
<ul>
<li><a href="">用户管理</a></li>
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">物流管理</a></li>
</ul>
</body>
</html>
ShiroConfig
package com.prannt.springboot_jsp_shiro.config;
/**
* @Deception 整合shiro框架相关的配置类
* shiroFilterFactoryBean依赖于defaultWebSecurityManager
* @Date 2022/1/23 12:37
* @Created Prannt
*/
@Configuration
public class ShiroConfig {
// 1.创建shiroFilter,负责拦截所有请求
@Bean // 是工厂中的一个filter对象,通过参数注入defaultWebSecurityManager
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 配置系统的受限资源
Map<String,String> map = new HashMap<>();
// map.put("/index.jsp","authc"); // 两个参数都是受限资源,authc表示请求这个资源需要认证和授权
map.put("/user/login","anon"); // 把登录页设为公共资源
map.put("/**","authc"); // 除了login.jsp以外的资源全部需要用户验证
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 配置系统的公共资源,不需要配置,因为除了受限资源就是公共资源
// 默认认证界面路径,不写路径默认就是login.jsp,也可以指定其他页面为默认路径
// shiroFilterFactoryBean.setLoginUrl("/login.jsp");
return shiroFilterFactoryBean;
}
// 2.创建shiroFilter需要的安全管理器
@Bean //是工厂中的一个对象
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
// 3.创建自定义realm
@Bean //是工厂中的一个对象
public Realm getRealm(){
return new CustomerRealm();
}
}
UserController
package com.prannt.springboot_jsp_shiro.controller;
@RequestMapping("user")
@Controller
public class UserController {
/**
* 用于处理身份认证
* @param username 用户名
* @param password 密码
* @return
*/
@RequestMapping("login")
public String login(String username, String password){
// 1.获取主体对象
// 注意:在web环境中,只要创建了ShiroConfig,就会自动注入安全管理器
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username,password));
return "redirect:/index.jsp";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
return "redirect:/login.jsp";
}
/**
* 退出登录
* @return
*/
@RequestMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout(); // 退出用户
return "redirect:/login.jsp";
}
}
CustomerRealm
package com.prannt.springboot_jsp_shiro.shiro.realms;
/**
* @Deception 自定义realm
* @Date 2022/1/23 13:00
* @Created Prannt
*/
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("================================");
String principal = (String) authenticationToken.getPrincipal();
if ("prannt".equals(principal)) {
return new SimpleAuthenticationInfo(principal,"123",this.getName());
}
return null;
}
}
测试:
输入:http://localhost:8888/shiro/login.jsp
进行登录,点击退出重新登录。
6.6 MD5、Salt的认证实现
6.6.1 用户注册 + 随机盐处理
1、导入依赖
<!--mp-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--mp代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
2、application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=630586
mybatis-plus.mapper-locations=classpath*:/mapper/**Mapper.xml
mybatis-plus.type-aliases-package=com.prannt.springboot_jsp_shiro.entity
3、创建数据库
CREATE DATABASE shiro;
USE shiro;
CREATE TABLE t_user(
`id` INT(6) PRIMARY KEY auto_increment,
`username` VARCHAR(40) NOT NULL,
`password` VARCHAR(40) NOT NULL,
`salt` VARCHAR(255) NOT NULL
);
4、代码生成器
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
// gc.setOutputDir("D:\\test");
gc.setAuthor("prannt");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("630586");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(null);
pc.setParent("com.prannt");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/"
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix("m_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
5、entity层
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class TUser implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private String salt;
}
6、controller层
@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class TUserController {
private final TUserService tUserService;
/**
* 用于处理身份认证
* @param username 用户名
* @param password 密码
* @return
*/
@RequestMapping("login")
public String login(String username, String password){
// 1.获取主体对象
// 注意:在web环境中,只要创建了ShiroConfig,就会自动注入安全管理器
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username,password));
return "redirect:/index.jsp";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
return "redirect:/login.jsp";
}
/**
* 退出登录
* @return
*/
@RequestMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout(); // 退出用户
return "redirect:/login.jsp";
}
/**
* 用户注册
* @param tUser
* @return
*/
@RequestMapping("/register")
public String register(TUser tUser){
try {
tUserService.register(tUser);
return "redirect:/login.jsp";
}catch (Exception e){
e.printStackTrace();
return "redirect:/register.jsp";
}
}
}
7、service层
接口
public interface TUserService extends IService<TUser> {
// 注册用户
void register(TUser tUser);
}
实现类
@Service
@Transactional
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements TUserService {
@Autowired
private TUserMapper tUserMapper;
// 注册用户
@Override
public void register(TUser tUser) {
// 调用mapper层处理业务
// 明文密码进行MD5加密 + 随机盐 + Hash散列
// 1.生成随机盐
String salt = SaltUtils.getSalt(8);
// 2.将随机盐保存到数据库中
tUser.setSalt(salt);
// 3.根据明文密码MD5 + salt + 散列
Md5Hash md5Hash = new Md5Hash(tUser.getPassword(),salt,1024);
tUser.setPassword(md5Hash.toHex());
// 通过TUserMapper保存对象
tUserMapper.save(tUser);
}
}
8、mapper层
接口
@Mapper
public interface TUserMapper extends BaseMapper<TUser> {
void save(TUser tUser);
}
mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.prannt.springboot_jsp_shiro.mapper.TUserMapper">
<insert id="save" parameterType="TUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user values (#{id},#{username},#{password},#{salt})
</insert>
</mapper>
9、配置类
/**
* @Deception 整合shiro框架相关的配置类
* shiroFilterFactoryBean依赖于defaultWebSecurityManager
* @Date 2022/1/23 12:37
* @Created Prannt
*/
@Configuration
public class ShiroConfig {
// 1.创建shiroFilter,负责拦截所有请求
@Bean // 是工厂中的一个filter对象,通过参数注入defaultWebSecurityManager
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 配置系统的受限资源
Map<String,String> map = new HashMap<>();
// map.put("/index.jsp","authc"); // 两个参数都是受限资源,authc表示请求这个资源需要认证和授权
map.put("/user/login","anon"); // 把登录页设为公共资源
map.put("/user/register","anon"); // 放行注册控制层
map.put("/register.jsp","anon"); // 放行注册页面
map.put("/**","authc"); // 除了login.jsp以外的资源全部需要用户验证
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 配置系统的公共资源,不需要配置,因为除了受限资源就是公共资源
// 默认认证界面路径,不写路径默认就是login.jsp,也可以指定其他页面为默认路径
// shiroFilterFactoryBean.setLoginUrl("/login.jsp");
return shiroFilterFactoryBean;
}
// 2.创建shiroFilter需要的安全管理器
@Bean //是工厂中的一个对象
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
// 3.创建自定义realm
@Bean //是工厂中的一个对象
public Realm getRealm(){
return new CustomerRealm();
}
}
10、创建salt工具类
public class SaltUtils {
/**
* 生成salt的静态方法
* TODO 学完 vueblog项目后,看看能不能用 hutool工具类做这个工作,下面的开发效率太低了
* @param n 随机盐的位数
* @return
*/
public static String getSalt(int n){
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()".toCharArray();
StringBuilder sb = new StringBuilder();
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) {
String salt = getSalt(4);
System.out.println(salt);
}
}
11、测试
访问:http://localhost:8888/shiro/register.jsp
6.6.2 开发数据库认证
1、开发mapper层
@Mapper
public interface TUserMapper extends BaseMapper<TUser> {
void save(TUser tUser); // 保存用户
TUser findByUsername(String username); // 根据身份信息认证的方法
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.prannt.springboot_jsp_shiro.mapper.TUserMapper">
<insert id="save" parameterType="TUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user values (#{id},#{username},#{password},#{salt})
</insert>
<select id="findByUsername" parameterType="String" resultType="TUser">
select * from t_user where username = #{username}
</select>
</mapper>
2、service层
public interface TUserService extends IService<TUser> {
// 注册用户
void register(TUser tUser);
// 根据用户名查询用户信息
TUser findByUsername(String username);
}
@Service("userService")
@Transactional
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements TUserService {
@Autowired
private TUserMapper tUserMapper;
// 根据用户名查询用户信息
@Override
public TUser findByUsername(String username) {
return tUserMapper.findByUsername(username);
}
}
3、修改自定义realm
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
TUser user = tUserService.findByUsername(principal); // 返回的是数据库中的user对象
if (user != null) {
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
}
return null;
}
4、修改ShiroConfig中realm
// 3.创建自定义realm
@Bean //是工厂中的一个对象
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("md5");
// 设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
5、测试
输入:http://localhost:8888/shiro/login.jsp
参数为:xiaochen 123
6.7 授权实现
6.7.1 没有数据库
1、页面资源授权
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>系统主页v1.0(受限资源)</h1>
<a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
<ul>
<!--具有admin权限才能看到-->
<shiro:hasRole name="admin">
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">物流管理</a></li>
</shiro:hasRole>
<!--普通用户也能看到-->
<shiro:hasRole name="user">
<li><a href="">用户管理</a></li>
</shiro:hasRole>
<!--用户是user或者admin权限都能看到用户管理-->
<shiro:hasAnyRoles name="user,admin">
<li><a href="">用户管理</a>
<ul>
<shiro:hasPermission name="user:add:*">
<li><a href="">添加</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:delete:*">
<li><a href="">删除</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:update:*">
<li><a href="">修改</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:find:*">
<li><a href="">查询</a></li>
</shiro:hasPermission>
</ul>
</li>
</shiro:hasAnyRoles>
</ul>
</body>
</html>
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principles) {
// 获取身份信息
String primaryPrincipal = (String) principles.getPrimaryPrincipal();
// 根据主身份信息获取角色信息和权限信息
if ("xiaochen".equals(primaryPrincipal)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("user");
simpleAuthorizationInfo.addStringPermission("user:find:*");
simpleAuthorizationInfo.addStringPermission("user:update:*");
return simpleAuthorizationInfo;
}
return null;
}
2、代码方式授权
@Controller
@RequestMapping("order")
public class OrderController {
// 订单保存
@RequestMapping("save")
public String save(){
// 代码授权(基于角色)
// 获取主体对象
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {
System.out.println("保存订单");
} else {
System.out.println("无权访问");
}
return "redirect:/index.jsp";
// 代码授权(基于权限字符串)
}
}
测试:
输入:http://localhost:8888/shiro/login.jsp
进行认证
然后访问:http://localhost:8888/shiro/order/save
,将在后台打印无权访问
3、注解方式授权
- 在
CustomerRealm
中修改simpleAuthorizationInfo.addRole("admin");
- 在
OrderController
的save()
方法上添加注解@RequiresRoles("admin")
- 访问
http://localhost:8888/shiro/login.jsp
先认证,然后访问http://localhost:8888/shiro/order/save
,将在后台打印`保存订单`` - ``@RequiresRoles
注解的参数也可以传入一个数组,例如:
@RequiresRoles(value={“admin”,”user”),表示同时具有
admin权限和
user`权限才可以访问 @RequiresPermissions()
注解用于判断权限字符串,例如:@RequiresPermissions("user:update:01")
6.7.2 连接数据库
6.7.2.1 数据库设计思路
用户表和角色表之间是多对多的关系(一个用户可能对应多个角色,一个角色可能同时被多个用户绑定),角色表和权限表之间也是多对多的关系(一个角色可能有多个权限,一个权限可能被多个角色使用)。数据库在处理多对多关系的时候,需要转换成两个一对多去处理。所以真正在做处理的时候,需要引入中间表(用户角色表和角色权限表),中间表是多的一方。具体流程如下图所示:
6.7.2.2 sql脚本
根据以上分析,设计表的sql语句如下:
-- ----------------------------
-- Table structure for t_user 用户表
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`username` varchar(40) DEFAULT NULL,
`password` varchar(40) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_role 角色表
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`name` varchar(60) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_perms 权限表
-- ----------------------------
DROP TABLE IF EXISTS `t_perms`;
CREATE TABLE `t_pers` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`name` varchar(80) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_user_role 用户角色表
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`id` int(6) NOT NULL,
`userid` int(6) DEFAULT NULL,
`roleid` int(6) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_role_perms 角色权限表
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
`id` int(6) NOT NULL,
`roleid` int(6) DEFAULT NULL,
`permsid` int(6) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
6.7.2.3 获取角色信息
①在数据库中插入数据
②实体类
@Data
public class TRole {
private Integer id;
private String name;
}
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class TUser implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private String salt;
private List<TRole> roles; // 角色集合
}
③service层
// 查询一个用户的多个角色
TUser findRolesByUsername(String username);
@Override
public TUser findRolesByUsername(String username) {
return tUserMapper.findRolesByUsername(username);
}
④Mapper层
TUser findRolesByUsername(String username); // 查询一个用户的多个角色
<resultMap id="userMap" type="TUser">
<id column="uid" property="id"></id>
<result column="username" property="username"></result>
<!--角色信息-->
<collection property="roles" javaType="list" ofType="TRole">
<id column="id" property="id"></id>
<result column="rname" property="name"></result>
</collection>
</resultMap>
<select id="findRolesByUsername" parameterType="String" resultMap="userMap">
select
u.id uid,u.username,r.id,r.name rname
from
t_user u
left join t_user_role ur
on
u.id = ur.userid
left join t_role r
on
ur.roleid = r.id
where
u.username = #{username}
</select>
⑤自定义realm
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principles) {
// 获取身份信息
String primaryPrincipal = (String) principles.getPrimaryPrincipal();
TUser user = tUserService.findRolesByUsername(primaryPrincipal);
// 授权角色信息
if (!CollectionUtils.isEmpty(user.getRoles())) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role -> {
simpleAuthorizationInfo.addRole(role.getName()); // 添加角色信息
});
return simpleAuthorizationInfo;
}
return null;
}
⑥测试
输入:http://localhost:8888/shiro/login.jsp
,参数为xiaochen 123
输入:http://localhost:8888/shiro/login.jsp
,参数为lucy 456
6.7.2.4 获取权限信息
①在数据库中插入数据
略
②实体类
@Data
public class TRole {
private Integer id;
private String name;
private List<TPers> pers; // 权限的集合
}
@Data
public class TPers {
private Integer id;
private String name;
private String url;
}
③service层
// 根据角色id查询权限集合
List<TPers> findPersByRoleId(Integer id);
@Override
public List<TPers> findPersByRoleId(Integer id) {
return tUserMapper.findPersByRoleId(id);
}
④Mapper层
List<TPers> findPersByRoleId(Integer id); // 根据角色id查询权限集合
<select id="findPersByRoleId" parameterType="Integer" resultType="TPers">
select
p.id,p.name,p.url,r.name
from
t_role r
left join
t_role_perms rp
on
r.id = rp.roleid
left join
t_pers p
on
rp.permsid = p.id
where
r.id = #{id}
</select>
⑤自定义realm
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principles) {
// 获取身份信息
String primaryPrincipal = (String) principles.getPrimaryPrincipal();
TUser user = tUserService.findRolesByUsername(primaryPrincipal);
// 授权角色信息
if (!CollectionUtils.isEmpty(user.getRoles())) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role -> {
simpleAuthorizationInfo.addRole(role.getName()); // 添加角色信息
// 权限信息
List<TPers> pers = tUserService.findPersByRoleId(role.getId());
if (!CollectionUtils.isEmpty(pers)){
pers.forEach(perm -> {
simpleAuthorizationInfo.addStringPermission(perm.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
⑥测试