1.写在前面
我跳槽几家公司,发现公司都是使用shiro来做权限认证的,但是我发现自己从来没有用过,同时也看不懂,但是这个shiro框架用的也很多,于是我不得不去学习shiro,今天我带领大家一篇博客搞定shiro。
2.本篇博客的概述
3.shiro简介
我们学习的第一手段当然是去官网找对应的例子,这里提供了shiro的官网,那么shiro是个什么东西呢?我们先来看官网的介绍。
笔者这儿做一个简单的翻译吧。Apache Shiro是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。通过Shiro易于理解的API,您可以快速轻松地保护任何应用程序,从最小的移动应用程序到最大的web和企业应用程序。
那么shiro能为我们做什么呢?
- 验证用户身份
- 用户访问权限控制
- 在非 Web 或 EJB 容器的环境下可以任意使用Session API
- 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
- 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
- 支持单点登录(SSO)功能
- 支持提供“Remember Me”服务,获取用户关联信息而无需登录
- …
不单单是上面介绍的这些功能shiro能为我们做,还有很多很多,主要是与shiro的四大特性有关。
4.shiro的四大特性
- 认证 - 用户身份识别,常被称为用户“登录”;
- 授权 - 访问控制;
- 密码加密 - 保护或隐藏数据防止被偷窥;
- 会话管理 - 每用户相关的时间敏感的状态。
5.shiro的历史
对于一个框架来讲,使其有存在价值的最好例证就是有让你去用它的原因,它应该能完成一些别人无法做到的事情。要理解这一点,需要了解 Shiro 的历史以及创建它时的其他替代方法。
在 2008 年加入 Apache 软件基金会之前,Shiro 已经 5 岁了,之前它被称为 JSecurity 项目,始于 2003 年初。当时,对于 Java 应用开发人员而言,没有太多的通用安全替代方案 - 我们被 Java 认证 / 授权服务(或称为 JAAS)紧紧套牢了。JAAS 有太多的缺点 - 尽管它的认证功能尚可忍受,但授权方面却显得拙劣,用起来令人沮丧。此外,JAAS 跟虚拟机层面的安全问题关系非常紧密,如判断 JVM 中是否允许装入一个类。作为应用开发者,我更关心应用最终用户能做什么,而不是我的代码在 JVM 中能做什么。
由于当时正从事应用开发,我也需要一个干净、容器无关的会话机制。在当时,“这场游戏”中唯一可用的会话是 HttpSessions,它需要 Web 容器;或是 EJB 2.1 里的有状态会话 Bean,这又要 EJB 容器。而我想要的一个与容器脱钩、可用于任何我选择的环境中的会话。
最后就是加密问题。有时,我们需要保证数据安全,但是 Java 密码架构(Java Cryptography Architecture)让人难以理解,除非你是密码学专家。API 里到处都是 Checked Exception,用起来很麻烦。我需要一个干净、开箱即用的解决方案,可以在需要时方便地对数据加密 / 解密。
于是,纵观 2003 年初的安全状况,你会很快意识到还没有一个大一统的框架满足所有上述需求。有鉴于此,JSecurity(即之后的 Apache Shiro)诞生了。
6.shiro的优点
- 易于使用 - 易用性是这个项目的最终目标。应用安全有可能会非常让人糊涂,令人沮丧,并被认为是“必要之恶”【译注:比喻应用安全方面的编程。】。若是能让它简化到新手都能很快上手,那它将不再是一种痛苦了。
- 广泛性 - 没有其他安全框架可以达到 Apache Shiro 宣称的广度,它可以为你的安全需求提供“一站式”服务。
- 灵活性 - Apache Shiro 可以工作在任何应用环境中。虽然它工作在 Web、EJB 和 IoC 环境中,但它并不依赖这些环境。Shiro 既不强加任何规范,也无需过多依赖。
- Web 能力 - Apache Shiro 对 Web 应用的支持很神奇,允许你基于应用 URL 和 Web 协议(如 REST)创建灵活的安全策略,同时还提供了一套控制页面输出的 JSP 标签库。
- 可插拔 - Shiro 干净的 API 和设计模式使它可以方便地与许多的其他框架和应用进行集成。你将看到 Shiro 可以与诸如 Spring、Grails、Wicket、Tapestry、Mule、Apache Camel、Vaadin 这类第三方框架无缝集成。
- 支持 - Apache Shiro 是 Apache 软件基金会成员,这是一个公认为了社区利益最大化而行动的组织。项目开发和用户组都有随时愿意提供帮助的友善成员。像 Katasoft 这类商业公司,还可以给你提供需要的专业支持和服务。
7.shiro功能结构图
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:
- **Authentication(认证):**用户身份识别,通常被称为用户“登录”
- **Authorization(授权):**访问控制。比如某个用户是否具有某个操作的使用权限。
- **Session Management(会话管理):**特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
- **Cryptography(加密):**在对数据源使用加密算法加密的同时,保证易于使用。
还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。特别是对以下的功能支持:
- **Web Support:**Shiro的Web支持API有助于保护Web应用程序。
- **Caching:**缓存是Apache Shiro API中的第一级,以确保安全操作保持快速和高效。
- **Concurrency:**Apache Shiro支持具有并发功能的多线程应用程序。
- **Testing:**存在测试支持,可帮助您编写单元测试和集成测试,并确保代码按预期得到保障。
- **Run As:**允许用户承担另一个用户的身份(如果允许)的功能,有时在管理方案中很有用。
- **Remember Me:**记住用户在会话中的身份,所以用户只需要强制登录即可。
注意: Shiro不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给Shiro
8.shiro高级概述
在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。
那么这三个概念是什么意思呢?具体的如下:
Subject
一词是一个安全术语,其基本意思是“当前的操作用户”。称之为“用户”并不准确,因为“用户”一词通常跟人相关。在安全领域,术语“Subject”可以是人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是 Shiro 的“用户”概念。在代码的任何地方,你都能轻易的获得 Shiro Subject,参见如下代码:
import org.apache.shiro.subject.Subject;
import org.apache.shiro.SecurityUtils;
...
Subject currentUser = SecurityUtils.getSubject();
一旦获得 Subject,你就可以立即获得你希望用 Shiro 为当前用户做的 90% 的事情,如登录、登出、访问会话、执行授权检查等 - 稍后还会看到更多。这里的关键点是 Shiro 的 API 非常直观,因为它反映了开发者以‘每个用户’思考安全控制的自然趋势。同时,在代码的任何地方都能很轻松地访问 Subject,允许在任何需要的地方进行安全操作。
SecurityManager
Subject 的“幕后”推手是 SecurityManager。Subject 代表了当前用户的安全操作,SecurityManager 则管理所有用户的安全操作。它是 Shiro 框架的核心,充当“保护伞”,引用了多个内部嵌套安全组件,它们形成了对象图。但是,一旦 SecurityManager 及其内部对象图配置好,它就会退居幕后,应用开发人员几乎把他们的所有时间都花在 Subject API 调用上。
Realms
Shiro 的第三个也是最后一个概念是Realm。Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当切实与像用户帐户这类安全相关数据进行交互,执行认证(登录)和授权(访问控制)时,Shiro 会从应用配置的 Realm 中查找很多内容。
从这个意义上讲,Realm 实质上是一个安全相关的 DAO :它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro。当配置 Shiro 时,你必须至少指定一个 Realm,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。
我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
9.Shiro 认证过程
上图展示了 Shiro 认证的一个重要的过程,为了加深我们的印象,我们来自己动手来写一个例子,来验证一下,首先我们新建一个Maven工程,然后在pom.xml中引入相关依赖:
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
新建一个AuthenticationTest
测试类:
package com.ys;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
public class AuthenticationTest {
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
@Before // 在方法开始前添加一个用户
public void addUser() {
simpleAccountRealm.addAccount("12345", "123456");
}
@Test
public void testAuthentication() {
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);
// 2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
Subject subject = SecurityUtils.getSubject(); // 获取当前主体
UsernamePasswordToken token = new UsernamePasswordToken("12345", "123456");
subject.login(token); // 登录
// subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
subject.logout(); // 登出
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出false
}
}
运行结果如下:
运行之后可以看到预想中的效果,先输出isAuthenticated:true
表示登录认证成功,然后再输出isAuthenticated:false
流程如下:
- 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
- SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
- Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
- Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
- Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。
10.Shiro授权过程
跟认证过程大致相似,下面我们仍然通过代码来熟悉一下过程,创建AuthenticationTest1
,具体的代码如下:
package com.ys;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
public class AuthenticationTest1 {
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
@Before // 在方法开始前添加一个用户,让它具备admin和user两个角色
public void addUser() {
simpleAccountRealm.addAccount("12345", "123456", "admin", "user");
}
@Test
public void testAuthentication() {
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);
// 2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
Subject subject = SecurityUtils.getSubject(); // 获取当前主体
UsernamePasswordToken token = new UsernamePasswordToken("12345", "123456");
subject.login(token); // 登录
// subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
// 判断subject是否具有admin和user两个角色权限,如没有则会报错
subject.checkRoles("admin","user");
// subject.checkRole("xxx"); // 报错
}
}
运行的结果如下:
11.自定义Realm
从上面我们了解到实际进行权限信息验证的是我们的 Realm,Shiro 框架内部默认提供了两种实现,一种是查询.ini
文件的IniRealm
,另一种是查询数据库的JdbcRealm
,这两种来说都相对简单,感兴趣的可以去【这里】瞄两眼,我们着重就来介绍介绍自定义实现的 Realm 吧。
有了上面的对认证和授权的理解,我们先在合适的包下创建一个MyRealm
类,继承 Shirot 框架的 AuthorizingRealm 类,并实现默认的两个方法:
package com.ys;
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 java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class MyRealm extends AuthorizingRealm {
/**
* 模拟数据库数据
*/
Map<String, String> userMap = new HashMap<>(16);
{
userMap.put("12345", "123456");
super.setName("myRealm"); // 设置自定义Realm的名称,取什么无所谓..
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 1.从主体传过来的认证信息中,获得用户名
String userName = (String) principalCollection.getPrimaryPrincipal();
// 从数据库获取角色和权限数据
Set<String> roles = getRolesByUserName(userName);
Set<String> permissions = getPermissionsByUserName(userName);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(permissions);
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
/**
* 模拟从数据库中获取权限数据
*/
private Set<String> getPermissionsByUserName(String userName) {
Set<String> permissions = new HashSet<>();
permissions.add("user:delete");
permissions.add("user:add");
return permissions;
}
/**
* 模拟从数据库中获取角色数据
*/
private Set<String> getRolesByUserName(String userName) {
Set<String> roles = new HashSet<>();
roles.add("admin");
roles.add("user");
return roles;
}
/**
* 认证
*
* @param authenticationToken 主体传过来的认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1.从主体传过来的认证信息中,获得用户名
String userName = (String) authenticationToken.getPrincipal();
// 2.通过用户名到数据库中获取凭证
String password = getPasswordByUserName(userName);
if (password == null) {
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("12345", password, "myRealm");
return authenticationInfo;
}
/**
* 模拟从数据库取凭证的过程
*/
private String getPasswordByUserName(String userName) {
return userMap.get(userName);
}
}
然后我们编写测试类,来验证是否正确:
package com.ys;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
public class AuthenticationTest2 {
@Test
public void testAuthentication() {
MyRealm myRealm = new MyRealm(); // 实现自己的 Realm 实例
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(myRealm);
// 2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
Subject subject = SecurityUtils.getSubject(); // 获取当前主体
UsernamePasswordToken token = new UsernamePasswordToken("12345", "123456");
subject.login(token); // 登录
// subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
// 判断subject是否具有admin和user两个角色权限,如没有则会报错
subject.checkRoles("admin", "user");
//subject.checkRole("xxx"); // 报错
// 判断subject是否具有user:add权限
subject.checkPermission("user:add");
}
}
12.Shiro加密
在最早的时候,我们存入数据库的都是明文密码,这样就很不安全,于是有了加密的手段,而Shiro也可以用来做加密,具体的例子如下:
package com.ys;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.junit.Test;
public class Test1{
@Test
public void test(){
String password = "123456";
System.out.println(new Md5Hash(password).toString());
}
}
运行结果如下:
这样我们就对“123456”进行了MD5加密,这种加密的手段是不可逆的。虽然是不可逆,但是我们对一些常用的密码的MD5加密的字符串给记录下来,这样MD5加密就不是很安全了,于是加盐的操作,既然相同的密码的MD5是一样的,那么我们就让我们原石密码再加上一个随机数,然后进行Md5加密,这个随机数就是通常我们说的盐,这样处理下来就会得到不同的MD5,当然我们需要将这个随机数也保存到数据库中,以便我们验证。于是就有了如下的方式:
package com.ys;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.junit.Test;
public class Test1 {
@Test
public void test() {
String password = "123456";
System.out.println(new Md5Hash(password).toString());
}
@Test
public void test2() {
String password = "123456";
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
int times = 2; // 加密次数:2
String name = "md5"; // 加密算法
String encodePassword = new SimpleHash(name, password, salt, times).toString();
System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ", password, salt, times, encodePassword);
}
}
14.SpringBoot简单实例
通过上面的学习,我们现在来搭建一个使用shiro进行权限验证授权的简单系统。
1.搭建基础环境
具体的pom.xml如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.6.0</version>
</dependency>
</dependencies>
Application.properties文件
#thymeleaf 配置
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html
#缓存设置为false, 这样修改之后马上生效,便于调试
spring.thymeleaf.cache=false
#数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=5201314ysX@
#mysql 8.0以前
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#mysql 8.0以后
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
#显示SQL语句
spring.jpa.show-sql=true
#不加下面这句则不会默认创建MyISAM引擎的数据库
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#自己重写的配置类,默认使用utf8编码
spring.jpa.properties.hibernate.dialect=com.wmyskxz.demo.shiro.config.MySQLConfig
2.创建实体类
新建一个entity
包,在下面创建以下实体类:
用户信息:
@Entity
@ToString
public class UserInfo {
@Id
@GeneratedValue
private Long id; // 主键.
@Column(unique = true)
private String username; // 登录账户,唯一.
private String name; // 名称(匿名或真实姓名),用于UI显示
private String password; // 密码.
private String salt; // 加密密码的盐
@JsonIgnoreProperties(value = {"userInfos"})
@ManyToMany(fetch = FetchType.EAGER) // 立即从数据库中进行加载数据
@JoinTable(name = "SysUserRole", joinColumns = @JoinColumn(name = "uid"), inverseJoinColumns = @JoinColumn(name = "roleId"))
private List<SysRole> roles; // 一个用户具有多个角色
//自动生成get set方法
}
角色信息:
@Entity
@ToString
public class SysRole {
@Id
@GeneratedValue
private Long id; // 主键.
private String name; // 角色名称,如 admin/user
private String description; // 角色描述,用于UI显示
// 角色 -- 权限关系:多对多
@JsonIgnoreProperties(value = {"roles"})
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "permissionId")})
private List<SysPermission> permissions;
// 用户 -- 角色关系:多对多
@JsonIgnoreProperties(value = {"roles"})
@ManyToMany
@JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})
private List<UserInfo> userInfos;// 一个角色对应多个用户
//自动生成get set方法
}
权限信息:
@Entity
@ToString
public class SysPermission {
@Id
@GeneratedValue
private Long id; // 主键.
private String name; // 权限名称,如 user:select
private String description; // 权限描述,用于UI显示
private String url; // 权限地址.
@JsonIgnoreProperties(value = {"permissions"})
@ManyToMany
@JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "permissionId")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})
private List<SysRole> roles; // 一个权限可以被多个角色使用
//自动生成get set方法
}
运行之前记得书写MySQLConfig
类,后面会有代码展示出来,启动springBoot项目会自动生成user_info(用户信息表)、sys_role(角色表)、sys_permission(权限表)、sys_user_role(用户角色表)、sys_role_permission(角色权限表)这五张表。具体的如下:
这个时候虽然数据库中的表已经创建好了,但是还是要往表中插入一些数据,具体执行的SQL语句如下:
INSERT INTO `user_info` (`id`,`name`,`password`,`salt`,`username`) VALUES (1, '管理员','951cd60dec2104024949d2e0b2af45ae', 'xbNIxrQfn6COSYn1/GdloA==', '12345');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (1,'查询用户','userInfo:view','/userList');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (2,'增加用户','userInfo:add','/userAdd');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (3,'删除用户','userInfo:delete','/userDelete');
INSERT INTO `sys_role` (`id`,`description`,`name`) VALUES (1,'管理员','admin');
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);
这个时候我们的数据就算导入完成了。
3.配置 Shiro
先创建一个config包,然后创建MySQLConfig类,具体的代码如下:
package com.example.springbootshiro.config;
import org.hibernate.dialect.MySQL8Dialect;
//这儿继承类也是和你的数据库版本有关系的,如果是5.X的版本就继承MySQL5Dialect类,如果是8.X的版本就继承MySQL8Dialect类
public class MySQLConfig extends MySQL8Dialect {
@Override
public String getTableTypeString() {
return "ENGINE=InnoDB DEFAULT CHARSET=utf8";
}
}
这个文件关联的是配置文件中最后一个配置,是让 Hibernate 默认创建 InnoDB 引擎并默认使用 utf-8 编码。接下来就是创建我们的Realm。我们先创建一个shiro包,放一些自定义Realm,然后创建MyShiroRealm类,具体的代码如下:
package com.example.springbootshiro.shiro;
import com.example.springbootshiro.entity.SysPermission;
import com.example.springbootshiro.entity.SysRole;
import com.example.springbootshiro.entity.UserInfo;
import com.example.springbootshiro.service.UserInfoService;
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;
import javax.annotation.Resource;
public class MyShiroRealm extends AuthorizingRealm {
@Resource
private UserInfoService userInfoService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 能进入这里说明用户已经通过验证了
UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (SysRole role : userInfo.getRoles()) {
simpleAuthorizationInfo.addRole(role.getName());
for (SysPermission permission : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permission.getName());
}
}
return simpleAuthorizationInfo;
}
//验证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户输入的账户
String username = (String) authenticationToken.getPrincipal();
System.out.println("authenticationToken: " + authenticationToken);
System.out.println("principal: " + authenticationToken.getPrincipal());
// 通过username从数据库中查找 UserInfo 对象
// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
UserInfo userInfo = userInfoService.findByUsername(username);
if (null == userInfo) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
userInfo, // 用户名
userInfo.getPassword(), // 密码
ByteSource.Util.bytes(userInfo.getSalt()), // salt=username+salt
getName() // realm name
);
return simpleAuthenticationInfo;
}
}
自定义的 Realm ,方法跟上面的认证授权过程一致,接下来就是shiro的配置文件,在config包下创建ShiroConfig类,具体的代码如下:
package com.example.springbootshiro.config;
import com.example.springbootshiro.shiro.MyShiroRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
// 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/**", "authc");
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2); // 散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean(name = "simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver
createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError"); // 数据库异常处理
mappings.setProperty("UnauthorizedException", "403");
r.setExceptionMappings(mappings); // None by default
r.setDefaultErrorView("error"); // No default
r.setExceptionAttribute("ex"); // Default is "exception"
//r.setWarnLogCategory("example.MvcLogger"); // No default
return r;
}
}
Apache Shiro 的核心通过 Filter 来实现,就好像 SpringMVC 通过 DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。
Filter Chain定义说明:
- 1、一个URL可以配置多个Filter,使用逗号分隔
- 2、当设置多个过滤器时,全部验证通过,才视为通过
- 3、部分过滤器可指定参数,如perms,roles
Filter Name | Class |
---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
- aauthc :需要认证登录才能访问
- user:用户拦截器,表示必须存在用户。
- anon:匿名拦截器,不需要登录即可访问的资源,匿名用户或游客,一般用于过滤静态资源。
- roles:角色授权拦截器,验证用户是或否拥有角色。参数可写多个,表示某些角色才能通过,多个参数时写 roles[“admin,user”],当有多个参数时必须每个参数都通过才算通过
- perms:权限授权拦截器,验证用户是否拥有权限参数可写多个,表示需要某些权限才能通过,多个参数时写 perms[“user, admin”],当有多个参数时必须每个参数都通过才算可以
- authcBasic:httpBasic 身份验证拦截器。
- logout:退出拦截器,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
- port:端口拦截器, 可通过的端口。
- ssl:ssl拦截器,只有请求协议是https才能通过。
4.准备Dao层和Service层
新建Dao包,在Dao包下创建UserInfoDao接口,具体的代码如下:
package com.example.springbootshiro.dao;
import com.example.springbootshiro.entity.UserInfo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserInfoDao extends JpaRepository<UserInfo, Long> {
/** 通过username查找用户信息*/
public UserInfo findByUsername(String username);
}
新建Service包,在Service包下创建UserInfoService接口,具体的代码如下:
package com.example.springbootshiro.service;
import com.example.springbootshiro.entity.UserInfo;
public interface UserInfoService {
public UserInfo findByUsername(String username);
}
并在该包下再新建一个impl包,新建UserInfoServiceImpl实现类:
package com.example.springbootshiro.service.impl;
import com.example.springbootshiro.dao.UserInfoDao;
import com.example.springbootshiro.entity.UserInfo;
import com.example.springbootshiro.service.UserInfoService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Resource
UserInfoDao userInfoDao;
@Override
public UserInfo findByUsername(String username) {
return userInfoDao.findByUsername(username);
}
}
5.准备Controller层
新建controller包,创建一个HomeController类,具体的代码如下:
package com.example.springbootshiro.controller;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Controller
public class HomeController {
@RequestMapping({"/","/index"})
public String index(){
return"/index";
}
@RequestMapping("/login")
public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
System.out.println("HomeController.login()");
// 登录失败从request中获取shiro处理的异常信息。
// shiroLoginFailure:就是shiro异常类的全类名.
String exception = (String) request.getAttribute("shiroLoginFailure");
System.out.println("exception=" + exception);
String msg = "";
if (exception != null) {
if (UnknownAccountException.class.getName().equals(exception)) {
System.out.println("UnknownAccountException -- > 账号不存在:");
msg = "UnknownAccountException -- > 账号不存在:";
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
System.out.println("IncorrectCredentialsException -- > 密码不正确:");
msg = "IncorrectCredentialsException -- > 密码不正确:";
} else if ("kaptchaValidateFailed".equals(exception)) {
System.out.println("kaptchaValidateFailed -- > 验证码错误");
msg = "kaptchaValidateFailed -- > 验证码错误";
} else {
msg = "else >> "+exception;
System.out.println("else -- >" + exception);
}
}
map.put("msg", msg);
// 此方法不处理登录成功,由shiro进行处理
return "/login";
}
@RequestMapping("/403")
public String unauthorizedRole(){
System.out.println("------没有权限-------");
return "403";
}
}
接下来创建UserInfoController类,具体的代码如下:
package com.example.springbootshiro.controller;
import com.example.springbootshiro.entity.UserInfo;
import com.example.springbootshiro.service.UserInfoService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class UserInfoController {
@Resource
UserInfoService userInfoService;
/**
* 按username账户从数据库中取出用户信息
*
* @param username 账户
* @return
*/
@GetMapping("/userList")
@RequiresPermissions("userInfo:view") // 权限管理.
public UserInfo findUserInfoByUsername(@RequestParam String username) {
return userInfoService.findByUsername(username);
}
/**
* 简单模拟从数据库添加用户信息成功
*
* @return
*/
@GetMapping("/userAdd")
@RequiresPermissions("userInfo:add")
public String addUserInfo() {
return "addUserInfo success!";
}
/**
* 简单模拟从数据库删除用户成功
*
* @return
*/
@GetMapping("/userDelete")
@RequiresPermissions("userInfo:delete")
public String deleteUserInfo() {
return "deleteUserInfo success!";
}
}
6.准备页面
新建三个页面用来测试:
index.html
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
index - 首页
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
错误信息:<h4 th:text="${msg}"></h4>
<form action="" method="post">
<p>账号:<input type="text" name="username" value="12345"/></p>
<p>密码:<input type="text" name="password" value="123456"/></p>
<p><input type="submit" value="登录"/></p>
</form>
</body>
</html>
403.html
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>403错误页</title>
</head>
<body>
错误页面
</body>
</html>
7.测试
1.编写好程序后就可以启动,首先访问http://localhost:8080/userList?username=12345
页面,由于没有登录就会跳转到我们配置好的http://localhost:8080/login
页面。登陆之后就会跳转到index.html,上面这些操作时候触发MyShiroRealm.doGetAuthenticationInfo()
这个方法,也就是登录认证的方法。
2.登录之后,我们还能访问http://localhost:8080/userAdd
页面,因为我们在数据库中提前配置好了权限,能够看到正确返回的数据,但是我们访问http://localhost:8080/userDelete
时,就会返回错误页面。
13.写在最后
本篇博客大概的带大家对shiro有个简单的认识,同时也带着大家入了门。如果觉得不错,可以一键三连支持一下博主。