相关概念
RBAC【了解】
RBAC 是基于角色的访问控制(Role-Based Access Control)在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
在 RBAC 模型里面,有3个基础组成部分,分别是:用户、角色和权限。
RBAC 通过定义角色的权限,并对用户授予某个角色从而来控制用户的权限,实现了用户和权限的逻辑分离,极大地方便了权限的管理。
-
User(用户):每个用户都有唯一的 UID 识别,并被授予不同的角色
-
Role(角色):不同角色具有不同的权限
-
Permission(权限):访问权限
-
用户-角色映射:用户和角色之间的映射关系
-
角色-权限映射:角色和权限之间的映射
权限管理
只要有用户参与的系统一般都要有权限管理,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户认证和授权两部分。
用户认证
用户认证,用户去访问系统,系统要验证用户身份的合法性。最常用的用户身份验证的方法:
1、用户名密码方式
2、指纹打卡机
3、基于证书验证方法。
系统验证用户身份合法,用户方可访问系统的资源。
用户认证流程
关键对象
subject:主体,理解为用户,可能是程序,都要去访问系统的资源,系统需要对 subject 进行身份认证。
principal:身份信息,通常是唯一的,一个主体还有多个身份信息,但是都有一个主身份信息(primary principal)
credential:凭证信息,可以是密码 、证书、指纹。
总结
主体在进行身份认证时需要提供身份信息和凭证信息。
用户授权
用户授权,简单理解为访问控制,在用户认证通过后,系统对用户访问资源进行控制,用户具有资源的访问权限方可访问。
用户授权流程
关键对象
授权的过程理解为:who 对 what(which) 进行 how 操作。
who:主体即 subject,subject 在认证通过后系统进行访问控制。
what(which):资源(Resource),subject 必须具备资源的访问权限才可访问该资源。资源比如:系统用户列表页面、商品修改菜单、商品id为001的商品信息,用户 id 为1的购物车列表。
资源分为资源类型和资源实例:
系统的用户信息就是资源类型,相当于 java 类。
系统中 id 为 001 的用户就是资源实例,相当于 new 的 java 对象。
how:权限/许可(permission) ,针对资源的权限或许可,subject 具有permission 访问资源,如何访问/操作需要定义permission,权限比如:用户添加、用户修改、商品删除。
权限模型
主体 subject(账号、密码)
资源 resources(资源名称、访问地址)
权限 permission(权限名称、资源id)
角色 role(角色名称)
角色和权限关系(角色id、权限id)
主体和角色关系(主体id、角色id)
通常企业开发中将资源和权限表合并为一张权限表,如下:
资源(资源名称、访问地址)
权限(权限名称、资源id)
合并为:
权限(权限名称、资源名称、资源访问地址)
分配权限
用户需要分配相应的权限才可访问相应的资源。权限是对于资源的操作许可,通常给用户分配资源权限需要将权限信息持久化,比如存储在关系数据库中,把用户信息、权限管理、用户分配的权限信息写到数据库(权限数据模型)。
Shiro
概述
Apache Shiro 是一个强大且易用的 Java 安全框架,执行身份验证、授权、密码和会话管理。且简单、灵活,不跟任何的框架或者容器绑定,可以独立运行,无论是最小的移动应用程序还是最大的网络和企业应用程序。
官方网址:https://shiro.apache.org/
官方文档:http://shiro.apache.org/reference.html
API文档:http://shiro.apache.org/static/1.7.1/apidocs/
GitHub 源码地址:https://github.com/apache/shiro
Shiro 特点
Shiro 是一个强大而灵活的开源安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密。
- 易于理解的 Java Security API
- 简单的身份认证(登录),支持多种数据源( LDAP,JDBC 等)
- 对角色的简单的授权(访问控制),也支持细粒度的鉴权
- 支持一级缓存,以提升应用程序的性能
- 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境
- 异构客户端会话访问
- 非常简单的加密 API
- 不跟任何的框架或者容器捆绑,可以独立运行
Shiro 架构
核心组件
三个核心组件:Subject,SecurityManager 和 Realms(域)。
Subject
即“当前操作用户”。但是,在 Shiro 中,Subject 这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject 代表了当前用户的安全操作,SecurityManager 则管理所有用户的安全操作。即所有的 Subject 实例都被绑定到一个 SecurityManager,如果你和一个 Subject 交互,所有的交互动作都会被转换成 Subject 与 SecurityManager 的交互。
SecurityManager
它是 Shiro 框架的核心,典型的 Facade 模式,Shiro 通过 SecurityManager 来管理内部组件实例,并通过它来提供安全管理的各种服务。不过我们一般不用太关心 SecurityManager,对于应用程序开发者来说,主要还是使用 Subject 的 API 来处理各种安全验证逻辑。分别通过 SecuritywManager 是通过 Authenticator 进行认证,通过 Authorizer 进行授权。通过sessionManager 进行会话管理等。SecurityManager 是一个接口,继承了 Authenticator,Authorizer,sessionManager 这三个接口。
Realm
Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro。当配置 Shiro 时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。
Shiro 内置了可以连接大量安全数据源(又名目录)的 Realm,如 LDAP、关系数据库(JDBC)、类似 INI 的文本配置资源以及属性文件等。如果缺省/默认的 Realm 不能满足需求,还可以插入代表自定义数据源的自己的 Realm 实现。
其他组件描述
Subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
SecurityManager:安全管理器,主体进行认证和授权都是通过securityManager进行。
Authenticator:认证器,主体进行认证最终通过authenticator进行的,用户登录认证
Authorizer:授权器,主体进行授权最终通过authorizer进行的,通过授权器判断登录成功的用户是否有操作权限。
SessionManager:会话管理,web 应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理。此特性可使它实现单点登录。
SessionDao:通过 SessionDao 管理 session 数据,针对个性化的 session 数据存储需要使用 sessionDao,提供了一些对 session 进行操作的方法,比如连接 JDBC 存储到数据库中,或者存储到 缓存服务器中。
cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager 进行缓存管理,和 ehcache 整合对缓存数据进行管理。
realm:域,领域,相当于数据源,通过 realm 存取认证、授权相关数据。
【注意】在realm中存储授权和认证的逻辑。
cryptography:密码管理,提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
使用流程
Shiro 入门
相关 API
// 安全管理器,用来处理所有的安全操作
interface SecurityManager {}
// 安全工具类,用来设置安全框架环境以及获取主体
class SecurityUtils {
// 设置安全管理器
static void setSecurityManager(SecurityManager securityManager);
// 获取主体
static Subject getSubject();
}
// 【重点】主体,包含了状态和安全相关的操作
interface Subject {
// 使用包含用户名和密码的token令牌登录
void login(AuthenticationToken token);
// 判断是否登录认证成功
boolean isAuthenticated();
// 退出登录,移除session和认证授权信息
void logout();
// 判断当前登录用户是否包含指定角色
boolean hasRole(String roleIdentifier);
// 判断当前登录用户是否包含list集合中的角色,返回值是一个包含结果的数组
boolean[] hasRoles(List<String> roleIdentifiers);
// 判断当前登录用户是否包含集合中的所有角色
boolean hasAllRoles(Collection<String> roleIdentifiers);
// 检查是否包含指定角色,如果没有就抛出异常
void checkRole(String roleIdentifier);
// 检查是否包含指定集合中的所有角色,如果没有就抛出异常
void checkRoles(Collection<String> roleIdentifiers);
// 检查是否包含指定集合中的所有角色,如果没有就抛出异常
void checkRoles(String... roleIdentifiers);
// 判断是否包含指定权限
boolean isPermitted(String permission);
// 判断是否包含集合中的权限,返回结果是一个包含结果的数组
boolean[] isPermitted(List<Permission> permissions);
// 判断是否包含多个权限,返回结果是一个包含结果的数组
boolean[] isPermitted(String... permissions);
// 判断是否包含集合中的所有权限
boolean isPermittedAll(Collection<Permission> permissions);
// 判断是否包含全部权限
boolean isPermittedAll(String... permissions);
// 如果没有指定权限,抛出异常
void checkPermission(String permission);
// 如果没有指定的所有权限,抛出异常
void checkPermissions(String... permissions);
// 如果没有指定的所有权限,抛出异常
void checkPermissions(Collection<Permission> permissions);
}
// 【重点】Realm核心类,自定义Realm需要继承此类并重写其中的方法
class AuthorizingRealm {
// 授权方法,参数为身份信息,即用户名,返回值为对应的授权信息
abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
// 认证方法,参数为包含用户名和密码的token令牌,返回值为对应的认证信息
abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token);
}
身份认证
概述
-
Shiro 把用户的数据封装成标识 token,token 一般封装着用户名,密码等信息
-
使用 Subject 门面获取到封装着用户的数据的标识token
-
Subject 把标识 token 交给 SecurityManager,在 SecurityManager 安全中心中,SecurityManager 把标识 token 委托给认证器 Authenticator 进行身份验证。认证器的作用一般是用来指定如何验证,它规定本次认证用到哪些 Realm
-
认证器 Authenticator 将传入的标识 token,与数据源 Realm 对比,验证 token 是否合法
案例代码
1、创建项目并配置 pom.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.fc</groupId>
<artifactId>09_Shiro_01_start</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--日志-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!--Shiro核心包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.7.1</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
</dependencies>
</project>
2、在 resources 目录下创建 shiro.ini 文件,IEDA 需要安装 ini 插件并重启方可生效
# 配置用户信息
[users]
tom = 123
jerry = 456
3、编写测试类
public class TestShiro {
@Test
public void useShiro() {
// 使用ini文件创建工厂对象
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// 根据工厂对象获取安全管理器
SecurityManager securityManager = factory.getInstance();
// 使用工具类配置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 通过工具类获取主体
Subject subject = SecurityUtils.getSubject();
// 创建token对象,这里使用UsernamePasswordToken,传入用户名和密码,也可以设置remember me
UsernamePasswordToken token = new UsernamePasswordToken("tom", "123");
// 使用token登录
subject.login(token);
// 判断是否登录成功
boolean authenticated = subject.isAuthenticated();
if (authenticated) {
System.out.println("认证成功");
// 退出登录
subject.logout();
System.out.println("退出");
System.out.println("是否认证成功:" + subject.isAuthenticated());
} else {
System.out.println("认证失败");
}
}
}
Realm
Realm 接口实现类
自定义 Realm
1、创建登录接口及实现类模拟查询数据库
public interface LoginService {
String getPasswordOnUsername(String username);
}
public class LoginServiceImpl implements LoginService {
@Override
public String getPasswordOnUsername(String username) {
return "123";
}
}
2、创建自定义 Realm 并继承自 AuthorizingRealm,重写认证和授权方法
/**
* 自定义Realm
*/
public class CustomRealm extends AuthorizingRealm {
private final String realmName = "customRealm";
/**
* 授权方法
* @param principals 用户名
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 认证方法
* @param token 认证令牌
* @return 认证信息对象
* @throws AuthenticationException 认证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 通过token获取用户名
String username = (String) token.getPrincipal();
// 获取业务层对象
LoginService loginService = new LoginServiceImpl();
// 通过用户名获取密码
String password = loginService.getPasswordOnUsername(username);
// 如果密码不存在
if (password == null || password.equals("") || !username.equals("jerry")) {
// 抛出异常
throw new UnknownAccountException("该用户不存在");
// 如果用户名和密码都正确
} else if (username.equals("jerry") && password.equals("123")) {
// 返回SimpleAuthenticationInfo对象并携带用户名、密码以及realmName
return new SimpleAuthenticationInfo(username, password, realmName);
} else {
throw new IncorrectCredentialsException("密码错误");
}
}
}
3、配置 shiro.ini 文件
# 配置自定义realm并指定安全管理器
[main]
customRealm = com.fc.realm.CustomRealm
securityManager.realms = $customRealm
4、编写测试类
public class TestRealm {
@Test
public void testRealm() {
// 通过shiro.ini文件获取工厂对象
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// 通过工厂对象获取安全管理器
SecurityManager securityManager = factory.getInstance();
// 通过工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 通过工具类获取主体
Subject subject = SecurityUtils.getSubject();
// 创建token
UsernamePasswordToken token = new UsernamePasswordToken("jerry1", "123");
// 使用token登录
subject.login(token);
// 判断是否认证成功
boolean authenticated = subject.isAuthenticated();
if (authenticated) {
System.out.println("认证成功");
} else {
System.out.println("认证失败");
}
}
}
Shiro 编码/解码
Shiro 提供了 Base64 和 16进制字符串编码/解码的 API 支持,方便一些编码解码操作。Shiro 内部的一些数据的【存储/表示】都使用了 base64 和 16 进制字符串。且这两种算法都是可逆的
Hex 编码/解码:https://www.107000.com/T-Hex
Base64 编码/解码:https://www.107000.com/T-Base64
编码/解码工具类
/**
* 编码解码工具类
*/
public class EncodingUtils {
/**
* hex编码
* @param src 源字节数组
* @return desc 目标字符串
*/
public static String hexEncoding(byte[] src) {
return Hex.encodeToString(src);
}
/**
* hex解码
* @param src 源字符串
* @return desc 目标字节数组
*/
public static byte[] hexDecoding(String src) {
return Hex.decode(src);
}
/**
* base64编码
* @param src 源字节数组
* @return desc 目标字符串
*/
public static String base64Encoding(byte[] src) {
return Base64.encodeToString(src);
}
/**
* base64解码
* @param src 源字符串
* @return desc 目标字节数组
*/
public static byte[] base64Decoding(String src) {
return Base64.decode(src);
}
}
案例代码
public class TestEncoding {
/**
* 测试Hex编码与解码
*/
@Test
public void testHex() {
// 声明源字符串
String src = "hello";
// 通过工具类使用Hex编码
String dest = EncodingUtils.hexEncoding(src.getBytes());
// 通过工具类解码
String valueOf = new String(EncodingUtils.hexDecoding(desc));
System.out.println(src);
System.out.println(desc);
System.out.println(valueOf);
}
/**
* 测试base64编码与解码
*/
@Test
public void testBase64() {
// 声明源字符串
String src = "hello";
// 通过工具类使用Hex编码
String desc = EncodingUtils.base64Encoding(src.getBytes());
// 通过工具类解码
String valueOf = new String(EncodingUtils.base64Decoding(desc));
System.out.println(src);
System.out.println(desc);
System.out.println(valueOf);
}
}
Shiro 散列算法
概述
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个 salt(盐),比如加密密码"admin",产生的散列值是"21232f297a57a5a743894a0e4a801fc3”,可以到一些 md5 解密网站很容易的通过散列值得到密码"admin" ,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如 salt(即盐)﹔这样散列的对象是"密码+salt”,这样生成的散列值相对来说更难破解。
正常使用时散列方法:
在程序中对原始密码 + 盐进行散列,将散列值存储到数据库中,并且还要将盐也要存储在数据库中。
如果进行密码对比时,使用相同方法,将原始密码+盐进行散列,进行比对。
shiro 支持的散列算法:
Md2Hash、Md5Hash、Sha1Hash、Sha256Hash、Sha384Hash、Sha512Hash
工具类
/**
* 散列工具类,用来对密码进行加密
*/
public class HashUtils {
// 加密方式:二选一
public static final String algorithmName = "SHA-1";
// public static final String algorithmName = "md5";
// 散列次数
public static final Integer hashIterations = 512;
/**
* 散列算法
* @param src 源字符串
* @param salt 盐
* @return 散列后的字符串
*/
public static String hash(String src, String salt) {
// 使用给定的盐创建特定源的算法的特定的加密值,总共散列指定的次数次
return new SimpleHash(algorithmName, src, salt, hashIterations).toString();
}
/**
* 获取盐
* @return 盐
*/
public static String generateSalt() {
// 获取随机数生成器
SecureRandomNumberGenerator generator = new SecureRandomNumberGenerator();
// 获取 Base 64 格式字符串表示形式的盐值
return generator.nextBytes().toBase64();
}
/**
* 获取凭证信息
* @param password 源密码
* @return 包含凭证信息的Map集合
*/
public static Map<String, String> getCredential(String password) {
String salt = generateSalt();
String dest = hash(password, salt);
Map<String, String> map = new HashMap<>();
map.put("password", dest);
map.put("salt", salt);
return map;
}
}
案例代码
public class TestHash {
@Test
public void testHash() {
Map<String, String> map = HashUtils.getCredential("123");
System.out.println(map.get("password"));
System.out.println(map.get("salt"));
}
Realm 使用散列
1、声明业务层接口及实现类
/**
* 登录接口
*/
public interface LoginService {
Map<String, String> getPasswordOnUsername(String username);
}
/**
* 登录接口实现类
*/
public class LoginServiceImpl implements LoginService {
@Override
public Map<String, String> getPasswordOnUsername(String username) {
return HashUtils.getCredential("123456");
}
}
2、自定义 Realm
/**
* 自定义Realm
*/
public class CustomRealm extends AuthorizingRealm {
// realm名称
private static final String realmName = "customRealm";
// 通过构造方法指定凭证匹配方式
public CustomRealm() {
// 指定密码匹配方式
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(HashUtils.ALGORITHM_NAME);
// 指定迭代次数
hashedCredentialsMatcher.setHashIterations(HashUtils.HASH_ITERATIONS);
// 设置使用自定义匹配方式
setCredentialsMatcher(hashedCredentialsMatcher);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 认证方法
* @param token 令牌(用户名和密码)
* @return 认证信息
* @throws AuthenticationException 认证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户名
String principal = (String) token.getPrincipal();
// 获取业务层对象
LoginService loginService = new LoginServiceImpl();
// 获取凭证
Map<String, String> credential = loginService.getPasswordOnUsername(principal);
// 判断凭证是否为空
if (credential.isEmpty() || !principal.equals("易烊千玺")) {
throw new UnknownAccountException("查无此人");
}
// 获取凭证中的密码和盐
String password = credential.get("password");
String salt = credential.get("salt");
// 返回认证信息,其中包括账号、密码、盐以及realm名称
return new SimpleAuthenticationInfo(principal, password, ByteSource.Util.bytes(salt), realmName);
}
}
3、shiro.ini
# 配置realm
customRealm = com.fc.realm.CustomRealm
securityManager.realms = $customRealm
4、测试类
public class TestRealm {
@Test
public void testRealm() {
// 通过ini文件获取工厂对象
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// 通过工厂获取安全管理器
SecurityManager securityManager = factory.getInstance();
// 获取token
UsernamePasswordToken token = new UsernamePasswordToken("易烊千玺", "123456");
// 通过工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 通过工具类获取主体
Subject subject = SecurityUtils.getSubject();
// 使用token登录
subject.login(token);
// 判断是否认证成功
boolean authenticated = subject.isAuthenticated();
if (authenticated) {
System.out.println("认证成功");
} else {
System.out.println("认证失败");
}
}
}
身份授权
流程
1、首先调用 Subject.isPermitted/hasRole 接口,其会委托给 SecurityManager。
2、SecurityManager 接着会委托给内部组件 Authorizer;
3、Authorizer 再将其请求委托给我们的 Realm 去做;Realm 才是真正干活的;
4、Realm 将用户请求的参数封装成权限对象。再从我们重写的 doGetAuthorizationInfo 方法中获取从数据库中查询到的权限集合。
5、Realm 将用户传入的权限对象,与从数据库中查出来的权限对象,进行一—对比。如果用户传入的权限对象在从数据库中查出来的权限对象中,则返回 true,否则返回 false。
进行授权操作的前提:用户必须通过认证。
案例代码
1、声明业务层接口及实现类
/**
* 登录接口
*/
public interface LoginService {
/**
* 根据用户名获取密码
*
* @param username 用户名
* @return 包含密码及盐的凭证
*/
Map<String, String> getPasswordOnUsername(String username);
/**
* 根据用户名查询角色
*
* @param username 用户名
* @return 返回包含角色的集合
*/
List<String> findRoleByUsername(String username);
/**
* 根据用户名查询权限
*
* @param username 用户名
* @return 返回包含权限的集合
*/
List<String> findPermissionByUsername(String username);
}
/**
* 登录接口实现类
*/
public class LoginServiceImpl implements LoginService {
@Override
public Map<String, String> getPasswordOnUsername(String username) {
return HashUtils.getCredential("123456");
}
@Override
public List<String> findRoleByUsername(String username) {
List<String> list = new ArrayList<>();
list.add("admin");
return list;
}
@Override
public List<String> findPermissionByUsername(String username) {
List<String> list = new ArrayList<>();
list.add("user:add");
list.add("user:delete");
list.add("user:modify");
return list;
}
}
2、散列工具类
3、自定义Realm
/**
* 自定义Realm
*/
public class CustomRealm extends AuthorizingRealm {
// realm名称
private static final String realmName = "customRealm";
// 获取业务层对象
private final LoginService loginService = new LoginServiceImpl();
// 通过构造方法指定凭证匹配方式
public CustomRealm() {
// 指定密码匹配方式
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(HashUtils.ALGORITHM_NAME);
// 指定迭代次数
hashedCredentialsMatcher.setHashIterations(HashUtils.HASH_ITERATIONS);
// 设置使用自定义匹配方式
setCredentialsMatcher(hashedCredentialsMatcher);
}
/**
* 授权方法
*
* @param principals 身份
* @return 授权信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取用户名
String principal = (String) principals.getPrimaryPrincipal();
// 获取用户角色以及权限
List<String> roles = loginService.findRoleByUsername(principal);
List<String> permissions = loginService.findPermissionByUsername(principal);
// 创建授权信息对象
SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();
// 给授权信息对象添加角色和权限
authenticationInfo.addRoles(roles);
authenticationInfo.addStringPermissions(permissions);
return authenticationInfo;
}
/**
* 认证方法
* @param token 令牌(用户名和密码)
* @return 认证信息
* @throws AuthenticationException 认证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户名
String principal = (String) token.getPrincipal();
// 获取凭证
Map<String, String> credential = loginService.getPasswordOnUsername(principal);
// 判断凭证是否为空
if (credential.isEmpty() || !principal.equals("易烊千玺")) {
throw new UnknownAccountException("查无此人");
}
// 获取凭证中的密码和盐
String password = credential.get("password");
String salt = credential.get("salt");
// 返回认证信息,其中包括账号、密码、盐以及realm名称
return new SimpleAuthenticationInfo(principal, password, ByteSource.Util.bytes(salt), realmName);
}
}
4、shiro.ini
# 配置realm
customRealm = com.fc.realm.CustomRealm
securityManager.realms = $customRealm
5、编写测试类
public class TestAuthorization {
@Test
public void testAuthorization() {
// 获取主体
Subject subject = getSubject();
// 判断主体是否为null
if (subject != null) {
// 判断是否含有指定角色
boolean isAdmin = subject.hasRole("admin");
System.out.println("是否是admin:" + isAdmin);
try {
// 判断是否含有指定角色
subject.checkRole("user");
} catch (Exception e) {
System.out.println("没有user角色");
}
// 判断是否包含指定权限
boolean[] hasPermitted = subject.isPermitted("user:add", "user:delete", "user:modify");
System.out.println("是否含有user:add以及user:delete权限:" + Arrays.toString(hasPermitted));
try {
// 判断是否包含指定权限
subject.checkPermission("user:query");
} catch (Exception e) {
System.out.println("没有user:query权限");
}
}
}
/**
* 获取主体
*
* @return 如果认证成功获取对应的subject,否则返回null
*/
public Subject getSubject() {
// 通过ini文件获取工厂对象
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("易烊千玺", "123456");
subject.login(token);
boolean authenticated = subject.isAuthenticated();
if (authenticated) {
System.out.println("认证成功");
return subject;
} else {
System.out.println("认证失败");
}
return null;
}
}
Java Web 整合 Shiro
核心配置文件
1、pom.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.fc</groupId>
<artifactId>09_Shiro_06_Web</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--日志包-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!--shiro核心包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.7.1</version>
</dependency>
<!--shiro-web-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.7.1</version>
</dependency>
<!--Java Web-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2、web.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--Shiro环境监听器,当服务器启动时创建Shiro的web环境-->
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<!--配置securityManager上下文对象-->
<context-param>
<param-name>shiroEnvironmentClass</param-name>
<param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value>
</context-param>
<!--加载shiro.ini文件-->
<context-param>
<param-name>shiroConfigLocations</param-name>
<param-value>classpath:shiro.ini</param-value>
</context-param>
<!--配置shiro过滤路径-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<!--过滤所有请求-->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
Shiro 默认过滤器
Shiro内置了很多默认的过滤器,比如身份验证、授权等相关的。默认过滤器可以参考org.apache.shiro.web.filter.mgt.DefaultFilter 中的枚举过滤器
认证相关
过滤器 | 过滤器类 | 描述 | 默认 |
---|---|---|---|
authc | FormAuthenticationFilter | 认证过滤器,必须登录,如果没有登录会跳到相应的登录页面登录。基于表单。如"/**=authc”" | 无 |
logout | LogoutFilter | 退出过滤器,主要属性: redirectUrl:退出成功后重定向的地址,如"/logout=logout" | / |
anon | AnonymousFilter | 匿名过滤器,即不需要登录即可访问。一般用于静态资源过滤:示例"lstatic/**=anon” | 无 |
user | UserFilter | 用户拦截器,用户已经身份验证 / 记住我登录的都可;示例 “/**=user” | 无 |
授权相关
过滤器 | 过滤器类 | |
---|---|---|
roles | RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例 “/admin/**=roles[admin]” |
perms | PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例 “/user/**=perms[“user:create”]” |
port | PortFilter | 端口拦截器,主要属性:port(80):可以通过的端口;示例 “/test= port[80]”,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径 / 参数等都一样 |
rest | HttpMethodPermissionFilter | rest 风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例 “/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete” 权限字符串进行权限匹配(所有都得匹配,isPermittedAll); |
ssl | SslFilter | SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样; |
通过 ini 配置文件获取授权【重点】
1、自定义 Realm
/**
* 自定义Realm
*/
public class CustomRealm extends AuthorizingRealm {
// realm名称
private static final String realmName = "customRealm";
// 获取业务层对象
private final SecurityService securityService = new SecurityServiceImpl();
// 通过构造方法指定凭证匹配方式
public CustomRealm() {
// 指定密码匹配方式
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(HashUtils.ALGORITHM_NAME);
// 指定迭代次数
hashedCredentialsMatcher.setHashIterations(HashUtils.HASH_ITERATIONS);
// 设置使用自定义匹配方式
setCredentialsMatcher(hashedCredentialsMatcher);
}
/**
* 授权方法
*
* @param principals 身份
* @return 授权信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取用户名
String principal = (String) principals.getPrimaryPrincipal();
// 获取用户角色以及权限
List<String> roles = securityService.findRoleByUsername(principal);
List<String> permissions = securityService.findPermissionByUsername(principal);
// 创建授权信息对象
SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();
// 给授权信息对象添加角色和权限
authenticationInfo.addRoles(roles);
authenticationInfo.addStringPermissions(permissions);
return authenticationInfo;
}
/**
* 认证方法
* @param token 令牌(用户名和密码)
* @return 认证信息
* @throws AuthenticationException 认证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户名
String principal = (String) token.getPrincipal();
// 获取凭证
Map<String, String> credential = securityService.getPasswordOnUsername(principal);
// 判断凭证是否为空
if (credential.isEmpty()) {
throw new UnknownAccountException("查无此人");
}
// 获取凭证中的密码和盐
String password = credential.get("password");
String salt = credential.get("salt");
// 返回认证信息,其中包括账号、密码、盐以及realm名称
return new SimpleAuthenticationInfo(principal, password, ByteSource.Util.bytes(salt), realmName);
}
}
2、散列工具类
3、业务层接口及实现类
/**
* 登录接口
*/
public interface LoginService {
/**
* 登录
* @param token 用户名和密码
* @return 登录成功返回true,否则返回false
*/
boolean login(UsernamePasswordToken token);
/**
* 退出登录
*/
void logout();
}
/**
* 登录接口实现类
*/
public class LoginServiceImpl implements LoginService {
@Override
public boolean login(UsernamePasswordToken token) {
// 通过工具类获取主体
Subject subject = SecurityUtils.getSubject();
// 使用token进行登录操作
subject.login(token);
// 判断是否认证成功
if (subject.isAuthenticated()) {
System.out.println("登录成功");
return true;
} else {
System.out.println("登录失败");
return false;
}
}
@Override
public void logout() {
// 通过工具类获取主体
Subject subject = SecurityUtils.getSubject();
// 退出登录
subject.logout();
}
}
/**
* 认证授权接口
*/
public interface SecurityService {
/**
* 根据用户名获取密码
*
* @param username 用户名
* @return 包含密码及盐的凭证
*/
Map<String, String> getPasswordOnUsername(String username);
/**
* 根据用户名查询角色
*
* @param username 用户名
* @return 返回包含角色的集合
*/
List<String> findRoleByUsername(String username);
/**
* 根据用户名查询权限
*
* @param username 用户名
* @return 返回包含权限的集合
*/
List<String> findPermissionByUsername(String username);
}
/**
* 认证授权接口实现类
*/
public class SecurityServiceImpl implements SecurityService {
@Override
public Map<String, String> getPasswordOnUsername(String username) {
return HashUtils.getCredential("123456");
}
@Override
public List<String> findRoleByUsername(String username) {
List<String> list = new ArrayList<>();
if (username.equals("admin")) {
list.add("admin");
}
list.add("user");
return list;
}
@Override
public List<String> findPermissionByUsername(String username) {
List<String> list = new ArrayList<>();
if (username.equals("admin")) {
list.add("order:add");
list.add("order:delete");
list.add("order:modify");
}
return list;
}
}
4、shiro.ini 配置文件
# 配置realm
[main]
customRealm = com.fc.realm.CustomRealm
securityManager.realms = $customRealm
# 用户退出后跳转至登录页面
logout.redirectUrl = /login.jsp
# 没有登录认证,跳转至登录页面
authc.loginUrl = /login.jsp
[urls]
# 登录页面允许匿名访问
/login.jsp = anon
# 主页需要登录
/home = authc
/home.jsp = authc
# 订单列表需要admin角色或者user角色
/order_list = roles[admin], roles[user]
# 添加订单需要订单添加权限
/order_add = perms["order:add"]
# 删除订单需要订单删除权限
/order_del = perms["order:delete"]
# 退出登录跳转至退出登录过滤器
/logout = logout
5、Servlet
/**
* 登录Servlet
*/
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 从前端获取用户名和密码
String username = req.getParameter("username");
String password = req.getParameter("password");
// 获取token
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 获取登录业务对象
LoginService loginService = new LoginServiceImpl();
// 登录
boolean isLogin = loginService.login(token);
// 判断是否登录成功
if (isLogin) {
// 登录成功跳转至主页
req.getRequestDispatcher("/home").forward(req, resp);
} else {
// 登录失败跳转至等领域页面
resp.sendRedirect("login.jsp");
}
}
}
/**
* 主页Servlet
*/
@WebServlet("/home")
public class HomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("home.jsp").forward(req, resp);
}
}
/**
* 退出登录
*/
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 登录业务层对象
LoginService loginService = new LoginServiceImpl();
// 退出登录
loginService.logout();
}
}
/**
* 添加订单Servlet
*/
@WebServlet("/order_add")
public class OrderAddServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("order_add.jsp").forward(req, resp);
}
}
/**
* 订单列表Servlet
*/
@WebServlet("/order_list")
public class OrderListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("order_list.jsp").forward(req, resp);
}
}
6、jsp页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录页面</title>
</head>
<body>
<form action="login" method="post">
<table align="center">
<caption><h1>登录</h1></caption>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密 码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td align="center" colspan="2">
<input type="reset" value="重置">
<input type="submit" value="提交">
</td>
</tr>
</table>
</form>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>主页</title>
</head>
<body>
<a href="order_list">列表</a>
<a href="order_add">添加</a>
<a href="logout">退出登录</a>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>添加订单</title>
</head>
<body>
添加订单
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>订单列表</title>
</head>
<body>
<table align="center">
<tr>
<th>订单编号</th>
<th>创建时间</th>
<th>订单状态</th>
<th>订单描述</th>
</tr>
<tr>
<td>001</td>
<td>2020.0202</td>
<td>已发货</td>
<td>货到付款</td>
</tr>
</table>
</body>
</html>
通过编程式代码获取授权
1、修改 shiro.ini 配置文件
# 配置realm
[main]
customRealm = com.fc.realm.CustomRealm
securityManager.realms = $customRealm
# 用户退出后跳转至登录页面
logout.redirectUrl = /login.jsp
# 没有登录认证,跳转至登录页面
authc.loginUrl = /login.jsp
[urls]
/login = anon
/logout = logout
2、修改对应的 Servlet
/**
* 主页Servlet
*/
@WebServlet("/home")
public class HomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取主体
Subject subject = SecurityUtils.getSubject();
// 判断是否登录成功
boolean authenticated = subject.isAuthenticated();
if (authenticated) {
// 登录成功,跳转至主页
req.getRequestDispatcher("home.jsp").forward(req, resp);
}
// 登录失败,跳转至登录页面
req.getRequestDispatcher("login.jsp").forward(req, resp);
}
}
/**
* 订单列表Servlet
*/
@WebServlet("/order_list")
public class OrderListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取当前主体
Subject subject = SecurityUtils.getSubject();
// 判断是否包含指定角色
if (subject.hasRole("admin")) {
// 包含角色,跳转至订单列表页面
req.getRequestDispatcher("order_list.jsp").forward(req, resp);
}
// 不包含角色跳转至错误页面
req.getRequestDispatcher("error.html").forward(req, resp);
}
}
/**
* 添加订单Servlet
*/
@WebServlet("/order_add")
public class OrderAddServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取主体
Subject subject = SecurityUtils.getSubject();
// 判断是否有指定权限
if (subject.isPermitted("order:add")) {
// 有权限,跳转到添加页面
req.getRequestDispatcher("order_add.jsp").forward(req, resp);
}
// 无权限,跳转至错误页面
req.getRequestDispatcher("error.html").forward(req, resp);
}
}
3、添加error.html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>错误页面</title>
</head>
<body>
<h1>当前用户权限不足</h1>
<a href="home.jsp">主页</a>
</body>
</html>
通过 JSP 页面获取授权
【注意】这种方式仅仅是没有展示的效果,如果知道了访问的 URL 依然可以访问到,所以需要配合其他方式才能保证安全地授权。
概述
Shiro 提供了一套 JSP 标签库来实现页面级的授权控制,在使用Shiro标签库前,首先需要在JSP 页面中引入 shiro标签
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags”%>
相关标签
标签 | 描述 |
---|---|
shiro:guest | 用户没有身份验证时显示相应信息,即游客访问信息。 |
shiro:user | 用户已经身份验证 / 记住我登录后显示相应的信息。 |
shiro:authenticated | 用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的。 |
shiro:notAuthenticated | 用户已经身份验证通过,即没有调用 Subject.login 进行登录,包括记住我自动登录的也属于未进行身份验证。 |
shiro: principal | 显示用户身份信息,默认调用 Subject.getPrincipal() 获取,即 Primary Principal。(单标签) |
shiro:hasRole【重点】 | 如果当前 Subject 有角色将显示 body 体内容。name属性为角色名称 |
shiro:hasAnyRoles | 如果当前 Subject 有任意一个角色(或的关系)将显示 body 体内容。name属性为角色名称,中间用逗号分隔 |
shiro:lacksRole | 如果当前 Subject 没有角色将显示 body 体内容。name属性为角色名称 |
shiro:hasPermission【重点】 | 如果当前 Subject 有权限将显示 body 体内容。name属性为权限名称 |
shiro:lacksPermission | 如果当前 Subject 没有权限将显示 body 体内容。name属性为权限名称 |
案例代码
1、修改 home.jsp 页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--引入shiro标签库--%>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<html>
<head>
<title>主页</title>
</head>
<body>
<%--判断是否包含admin角色--%>
<shiro:hasRole name="admin">
<a href="order_list">列表</a>
</shiro:hasRole>
<%--判断是否包含order:add权限--%>
<shiro:hasPermission name="order:add">
<a href="order_add">添加</a>
</shiro:hasPermission>
<%--判断是否不包含order:add权限--%>
<shiro:lacksPermission name="order:add">
<a aria-disabled="true" style="color: red">添加</a>
</shiro:lacksPermission>
<a href="logout">退出登录</a>
</body>
</html>
SpringBoot 整合 Shiro【重点】
1、创建 SpringBoot 项目并导入对应的依赖
<?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.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fc</groupId>
<artifactId>test_shiro_springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test_shiro_springboot</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>
<!--SpringBoot整合Shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>
<!--SpringBoot整合thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、application.yml 配置文件
server:
port: 8080
shiro:
enabled: true
spring:
thymeleaf:
cache: false
enabled: true
mode: HTML
prefix: classpath:/templates/
suffix: .html
3、控制层
@Controller
@RequestMapping("user")
public class UserController {
// 跳转至登录页面
@RequestMapping("toLogin")
public String toLogin() {
return "login";
}
// 登录逻辑
@RequestMapping("login")
public String login(String username, String password, Model model) {
// 获取token
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 获取主体
Subject subject = SecurityUtils.getSubject();
try {
// 登录
subject.login(token);
// 获取用户名
String principal = (String) subject.getPrincipal();
// 添加到session中
subject.getSession().setAttribute("username", principal);
// 跳转至主页
return "index";
// 捕获用户不存在的异常
} catch (UnknownAccountException e) {
model.addAttribute("msg", "用户不存在");
return "refuse";
// 捕获密码错误的异常
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg", "密码错误");
return "refuse";
}
}
// 跳转至主页
@RequestMapping("toIndex")
public String toIndex() {
return "index";
}
// 跳转至拒绝页面
@RequestMapping("toRefuse")
public String toRefuse() {
return "refuse";
}
// 退出登录
@RequestMapping("logout")
public void logout() {
Subject subject = SecurityUtils.getSubject();
// 退出登录
subject.logout();
}
// 添加
@RequestMapping("toAdd")
public String toAdd() {
return "add";
}
// 查询
@RequestMapping("toList")
public String toList() {
return "list";
}
}
4、Hash工具类
/**
* 散列工具类,用来对密码进行加密
*/
public class HashUtils {
// 加密方式
public static final String ALGORITHM_NAME = "SHA-1";
// 散列次数
public static final Integer HASH_ITERATIONS = 512;
/**
* 散列算法
* @param src 源字符串
* @param salt 盐
* @return 散列后的字符串
*/
public static String hash(String src, String salt) {
return new SimpleHash(ALGORITHM_NAME, src, salt, HASH_ITERATIONS).toString();
}
/**
* 获取盐
* @return 盐
*/
public static String generateSalt() {
SecureRandomNumberGenerator generator = new SecureRandomNumberGenerator();
return generator.nextBytes().toBase64();
}
/**
* 获取凭证信息
* @param password 源密码
* @return 包含凭证信息的Map集合
*/
public static Map<String, String> getCredential(String password) {
String salt = generateSalt();
String dest = hash(password, salt);
Map<String, String> map = new HashMap<>();
map.put("password", dest);
map.put("salt", salt);
return map;
}
}
5、自定义realm
/**
* 自定义核心Realm
*/
public class ShiroRealm extends AuthorizingRealm {
// realm名称
private static final String REALM_NAME = "shiroRealm";
// 构造方法执行后调用
@PostConstruct
public void initMatcher() {
// 指定密码匹配方式
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(HashUtils.ALGORITHM_NAME);
// 指定迭代次数
hashedCredentialsMatcher.setHashIterations(HashUtils.HASH_ITERATIONS);
// 设置使用自定义匹配方式
setCredentialsMatcher(hashedCredentialsMatcher);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String principal = (String) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (principal.equals("admin")) {
info.addStringPermission("user:add");
}
info.addStringPermission("user:query");
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户名
String username = (String) authenticationToken.getPrincipal();
// 通过工具类进行加密
Map<String, String> credential = HashUtils.getCredential("123");
// 获取加密过的密码以及盐
String password = credential.get("password");
String salt = credential.get("salt");
// 创建认证信息对象并返回
return new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes(salt), REALM_NAME);
}
}
6、shiroConfig
@Configuration
public class ShiroConfig {
// 自定义ShiroRealm
@Bean
public ShiroRealm getShiroRealm() {
return new ShiroRealm();
}
// cookie对象
@Bean
public SimpleCookie simpleCookie() {
return new SimpleCookie("sessionIdCookie");
}
// session会话管理器
@Bean
public DefaultWebSessionManager defaultWebSessionManager(SimpleCookie simpleCookie) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// cookie生效
sessionManager.setSessionIdCookieEnabled(true);
// 设置cookie
sessionManager.setSessionIdCookie(simpleCookie);
// 设置全局session过期时间
sessionManager.setGlobalSessionTimeout(3600000);
// 关闭绘画验证调度器,提高性能
sessionManager.setSessionValidationSchedulerEnabled(false);
return sessionManager;
}
// 配置安全管理器
@Bean
public DefaultWebSecurityManager getSecurityManager(ShiroRealm shiroRealm, DefaultWebSessionManager sessionManager) {
// 创建安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义realm
securityManager.setRealm(shiroRealm);
// 设置会话管理器
securityManager.setSessionManager(sessionManager);
return securityManager;
}
// 配置过滤器
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
factoryBean.setSecurityManager(securityManager);
// 设置登录路径
factoryBean.setLoginUrl("/user/toLogin");
// 有序map
Map<String, String> filterChainMap = new LinkedHashMap<>();
// 添加过滤条件
filterChainMap.put("/user/login", "anon");
filterChainMap.put("/user/toRefuse", "anon");
// 静态资源放行
filterChainMap.put("/static/**", "anon");
filterChainMap.put("/user/logout", "logout");
filterChainMap.put("/user/toAdd", "perms[user:add]");
filterChainMap.put("/user/toList", "perms[user:query]");
filterChainMap.put("/**", "authc");
// 设置过滤器链
factoryBean.setFilterChainDefinitionMap(filterChainMap);
// 设置无权限访问跳转路径
factoryBean.setUnauthorizedUrl("/user/toRefuse");
return factoryBean;
}
}
7、html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<form action="/user/login" method="post">
<table align="center">
<caption><h1>登录页面</h1></caption>
<tr>
<td>用户名:</td>
<td>
<input type="text" name="username"/>
</td>
</tr>
<tr>
<td>密 码:</td>
<td>
<input type="password" name="password"/>
</td>
</tr>
<tr>
<td align="center" colspan="2">
<input type="reset" value="重置">
<input type="submit" value="登录"/>
</td>
</tr>
</table>
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
欢迎<span th:text="${session.username}"></span>来到主页
<a href="/user/toAdd">添加用户</a>
<a href="/user/toList">查看用户</a>
<a href="/user/logout">退出登录</a>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>请求拒绝页面</title>
</head>
<body>
<p th:text="${msg}"></p>
<span th:if="${msg} eq null">请求被拒绝,没有权限</span>
<a th:href="@{/user/toIndex}">返回主页</a>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加用户</title>
</head>
<body>
<h1 align="center">添加用户</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>查询用户</title>
</head>
<body>
<h1 align="center">查询用户</h1>
</body>
</html>
自定义过滤器
1、自定义过滤器实现自 AuthorizationFilter 接口
// 自定义过滤器实现AuthorizationFilter接口
public class RolesAnyAuthorizationFilter extends AuthorizationFilter {
// 重写允许访问的方法
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
// 获取主体
Subject subject = getSubject(request, response);
// 获取角色数组
String[] rolesArray = (String[]) mappedValue;
// 判断是否为空
if (rolesArray == null || rolesArray.length == 0) {
return true;
}
// 将角色数组转为Set集合
Set<String> roles = CollectionUtils.asSet(rolesArray);
// for循环迭代
for (String role : roles) {
// 如果包含了指定的角色
if (subject.hasRole(role)) {
// 放行
return true;
}
}
// 过滤
return false;
}
}
2、修改自定义 Realm 添加角色
/**
* 自定义核心Realm
*/
public class ShiroRealm extends AuthorizingRealm {
// realm名称
private static final String REALM_NAME = "shiroRealm";
// 构造方法执行后调用
@PostConstruct
public void initMatcher() {
// 指定密码匹配方式
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(HashUtils.ALGORITHM_NAME);
// 指定迭代次数
hashedCredentialsMatcher.setHashIterations(HashUtils.HASH_ITERATIONS);
// 设置使用自定义匹配方式
setCredentialsMatcher(hashedCredentialsMatcher);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String principal = (String) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (principal.equals("admin")) {
// 添加指定角色和权限
info.addRole("admin");
info.addStringPermission("user:add");
}
// 添加指定角色和权限
info.addStringPermission("user:query");
info.addRole("user");
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户名
String username = (String) authenticationToken.getPrincipal();
// 通过工具类进行加密
Map<String, String> credential = HashUtils.getCredential("123");
// 获取加密过的密码以及盐
String password = credential.get("password");
String salt = credential.get("salt");
// 创建认证信息对象并返回
return new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes(salt), REALM_NAME);
}
}
3、修改 ShiroConfig 配置类
@Configuration
public class ShiroConfig {
// 自定义ShiroRealm
@Bean
public ShiroRealm getShiroRealm() {
return new ShiroRealm();
}
// cookie对象
@Bean
public SimpleCookie simpleCookie() {
return new SimpleCookie("sessionIdCookie");
}
// session会话管理器
@Bean
public DefaultWebSessionManager defaultWebSessionManager(SimpleCookie simpleCookie) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// cookie生效
sessionManager.setSessionIdCookieEnabled(true);
// 设置cookie
sessionManager.setSessionIdCookie(simpleCookie);
// 设置全局session过期时间
sessionManager.setGlobalSessionTimeout(3600000);
// 关闭绘画验证调度器,提高性能
sessionManager.setSessionValidationSchedulerEnabled(false);
return sessionManager;
}
// 配置安全管理器
@Bean
public DefaultWebSecurityManager getSecurityManager(ShiroRealm shiroRealm, DefaultWebSessionManager sessionManager) {
// 创建安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义realm
securityManager.setRealm(shiroRealm);
// 设置会话管理器
securityManager.setSessionManager(sessionManager);
return securityManager;
}
// 自定义过滤器配置类型
private Map<String, Filter> setFilters() {
Map<String, Filter> map = new HashMap<>();
map.put("role-any", new RolesAnyAuthorizationFilter());
return map;
}
// 配置过滤器
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
factoryBean.setSecurityManager(securityManager);
// 配置自定义的过滤器
factoryBean.setFilters(setFilters());
// 设置登录路径
factoryBean.setLoginUrl("/user/toLogin");
// 有序map
Map<String, String> filterChainMap = new LinkedHashMap<>();
// 添加过滤条件
filterChainMap.put("/user/login", "anon");
filterChainMap.put("/user/toRefuse", "anon");
// 静态资源放行
filterChainMap.put("/static/**", "anon");
filterChainMap.put("/user/logout", "logout");
// 添加多个条件
filterChainMap.put("/user/toAdd", "role-any[admin], perms[user:add]");
filterChainMap.put("/user/toList", "role-any[admin, user], perms[user:query]");
filterChainMap.put("/**", "authc");
// 设置过滤器链
factoryBean.setFilterChainDefinitionMap(filterChainMap);
// 设置无权限访问跳转路径
factoryBean.setUnauthorizedUrl("/user/toRefuse");
return factoryBean;
}
}
使用注解进行授权
概述
注解 | 描述 | 优先级 |
---|---|---|
@RequiresAuthentication | 表明当前用户必须是经过认证的用户 | 3 |
@RequiresGuest | 表明该用户需为"guest"宾客用户,它们不会从先前的会话中进行身份验证或被记住。 | 5 |
@RequiresPermissions | 当前用户需拥有指定权限,如果没有指定的权限, 会报错 AuthorizationException | 2 |
@RequiresRoles | 当前用户必须拥有指定角色,如果没有指定角色,则不会执行该方法并抛出 AuthorizationException。 | 1 |
@ RequiresUser | 当前用户需为已认证用户或已记住用户 | 4 |
注意:Shiro的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序与实际声明顺序无关
案例代码
1、修改 ShiroConfig 配置类
@Configuration
public class ShiroConfig {
// 自定义ShiroRealm
@Bean
public ShiroRealm getShiroRealm() {
return new ShiroRealm();
}
// cookie对象
@Bean
public SimpleCookie simpleCookie() {
return new SimpleCookie("sessionIdCookie");
}
// session会话管理器
@Bean
public DefaultWebSessionManager defaultWebSessionManager(SimpleCookie simpleCookie) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// cookie生效
sessionManager.setSessionIdCookieEnabled(true);
// 设置cookie
sessionManager.setSessionIdCookie(simpleCookie);
// 设置全局session过期时间
sessionManager.setGlobalSessionTimeout(3600000);
// 关闭绘画验证调度器,提高性能
sessionManager.setSessionValidationSchedulerEnabled(false);
return sessionManager;
}
// 配置安全管理器
@Bean
public DefaultWebSecurityManager getSecurityManager(ShiroRealm shiroRealm, DefaultWebSessionManager sessionManager) {
// 创建安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义realm
securityManager.setRealm(shiroRealm);
// 设置会话管理器
securityManager.setSessionManager(sessionManager);
return securityManager;
}
// 配置过滤器
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
factoryBean.setSecurityManager(securityManager);
// 设置登录路径
factoryBean.setLoginUrl("/user/toLogin");
// 有序map
Map<String, String> filterChainMap = new LinkedHashMap<>();
// 添加过滤条件
filterChainMap.put("/user/login", "anon");
filterChainMap.put("/user/toRefuse", "anon");
// 静态资源放行
filterChainMap.put("/static/**", "anon");
filterChainMap.put("/user/logout", "logout");
filterChainMap.put("/**", "authc");
// 设置过滤器链
factoryBean.setFilterChainDefinitionMap(filterChainMap);
// 设置无权限访问跳转路径
factoryBean.setUnauthorizedUrl("/user/toRefuse");
return factoryBean;
}
/**
* 使用注解进行AOP增强
* @return creator
*/
@Bean
// @DependsOn控制注解的注入顺序在lifecycleBeanPostProcessor之后
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
// 获取默认通知自动代理构建器
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
// 设置代理类型,默认false使用JDK进行代理
creator.setProxyTargetClass(true);
return creator;
}
/**
* 注解权限校验器
*
* @param securityManager 安全管理器
* @return 注解权限校验器
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
// 获取权限校验器
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
// 设置安全管理器
advisor.setSecurityManager(securityManager);
return advisor;
}
}
2、在 Controller 上添加对应的注解
@Controller
@RequestMapping("user")
public class UserController {
// 跳转至登录页面
@RequestMapping("toLogin")
public String toLogin() {
return "login";
}
// 登录逻辑
@RequestMapping("login")
public String login(String username, String password, Model model) {
// 获取token
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 获取主体
Subject subject = SecurityUtils.getSubject();
try {
// 登录
subject.login(token);
// 获取用户名
String principal = (String) subject.getPrincipal();
// 添加到session中
subject.getSession().setAttribute("username", principal);
// 跳转至主页
return "index";
// 捕获用户不存在的异常
} catch (UnknownAccountException e) {
model.addAttribute("msg", "用户不存在");
return "refuse";
// 捕获密码错误的异常
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg", "密码错误");
return "refuse";
}
}
// 跳转至主页
@RequestMapping("toIndex")
public String toIndex() {
return "index";
}
// 跳转至拒绝页面
@RequestMapping("toRefuse")
public String toRefuse() {
return "refuse";
}
// 退出登录
@RequestMapping("logout")
public void logout() {
Subject subject = SecurityUtils.getSubject();
// 退出登录
subject.logout();
}
// 添加方法
@RequestMapping("toAdd")
// 必须携带指定的权限
@RequiresPermissions("user:add")
// 必须经过认证才能访问
@RequiresAuthentication
// 必须携带指定的角色
@RequiresRoles(value = "admin")
public String toAdd() {
return "add";
}
// 查询
@RequestMapping("toList")
// 必须携带指定的权限
@RequiresPermissions("user:query")
// 必须经过认证
@RequiresAuthentication
// 必须携带指定的角色之一(logical为检查的逻辑操作,可以为and或者or。默认为and)
@RequiresRoles(value = {"admin", "user"}, logical = Logical.OR)
public String toList() {
return "list";
}
}
rememberMe 记住我
1、修改 login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<form action="/user/login" method="post">
<table align="center">
<caption><h1>登录页面</h1></caption>
<tr>
<td>用户名:</td>
<td>
<input type="text" name="username"/>
</td>
</tr>
<tr>
<td>密 码:</td>
<td>
<input type="password" name="password"/>
</td>
</tr>
<tr>
<td colspan="2">
<input type="checkbox" name="rememberMe"><span>记住我</span>
<input type="submit" value="登录"/>
</td>
</tr>
</table>
</form>
</body>
</html>
2、修改Controller
@Controller
@RequestMapping("user")
public class UserController {
// 跳转至登录页面
@RequestMapping("toLogin")
public String toLogin() {
return "login";
}
// 登录逻辑
@RequestMapping("login")
public String login(String username, String password, boolean rememberMe, Model model) {
// 获取token
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
// 获取主体
Subject subject = SecurityUtils.getSubject();
try {
// 登录
subject.login(token);
// 获取用户名
String principal = (String) subject.getPrincipal();
// 添加到session中
subject.getSession().setAttribute("username", principal);
// 跳转至主页
return "index";
// 捕获用户不存在的异常
} catch (UnknownAccountException e) {
model.addAttribute("msg", "用户不存在");
return "refuse";
// 捕获密码错误的异常
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg", "密码错误");
return "refuse";
}
}
// 跳转至主页
@RequestMapping("toIndex")
public String toIndex() {
return "index";
}
// 跳转至拒绝页面
@RequestMapping("toRefuse")
public String toRefuse() {
return "refuse";
}
// 退出登录
@RequestMapping("logout")
public void logout() {
Subject subject = SecurityUtils.getSubject();
// 退出登录
subject.logout();
}
// 添加
@RequestMapping("toAdd")
public String toAdd() {
return "add";
}
// 查询
@RequestMapping("toList")
public String toList() {
return "list";
}
}
3、修改自定义 realm
/**
* 自定义核心Realm
*/
public class ShiroRealm extends AuthorizingRealm {
// realm名称
private static final String REALM_NAME = "shiroRealm";
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String principal = (String) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (principal.equals("admin")) {
info.addStringPermission("user:add");
}
info.addStringPermission("user:query");
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户名
String username = (String) authenticationToken.getPrincipal();
// 获取登录密码
String password = new String((char[]) authenticationToken.getCredentials());
// 创建认证信息对象并返回
return new SimpleAuthenticationInfo(username, password, REALM_NAME);
}
}
4、修改 shiro 配置类
@Configuration
public class ShiroConfig {
// 自定义ShiroRealm
@Bean
public ShiroRealm getShiroRealm() {
return new ShiroRealm();
}
// 配置记住我管理器
@Bean
public CookieRememberMeManager getCookieRememberMeManager() {
// 创建简单Cookie对象
SimpleCookie cookie = new SimpleCookie("rememberMeCookie");
// 设置cookie过期时间
cookie.setMaxAge(60 * 60);
// 创建记住我管理器
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
// 添加cookie到管理器中
rememberMeManager.setCookie(cookie);
return rememberMeManager;
}
// 配置安全管理器
@Bean
public DefaultWebSecurityManager getSecurityManager(ShiroRealm shiroRealm, CookieRememberMeManager cookieRememberMeManager) {
// 创建安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义realm
securityManager.setRealm(shiroRealm);
// 设置cookie记住我管理器
securityManager.setRememberMeManager(cookieRememberMeManager);
return securityManager;
}
// 配置过滤器
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 设置登录路径
factoryBean.setLoginUrl("/user/toLogin");
// 有序map
Map<String, String> filterChainMap = new LinkedHashMap<>();
// 添加过滤条件
filterChainMap.put("/user/login", "anon");
filterChainMap.put("/user/toRefuse", "anon");
filterChainMap.put("/user/logout", "logout");
filterChainMap.put("/user/toAdd", "perms[user:add]");
filterChainMap.put("/user/toList", "perms[user:query]");
filterChainMap.put("/user/toIndex", "user");
filterChainMap.put("/**", "authc");
// 设置过滤器链
factoryBean.setFilterChainDefinitionMap(filterChainMap);
// 设置无权限访问跳转路径
factoryBean.setUnauthorizedUrl("/user/toRefuse");
return factoryBean;
}
}
Thymeleaf 整合 Shiro
GitHub 地址:https://github.com/theborakompanioni/thymeleaf-extras-shiro
可用标签
标签 | 描述 |
---|---|
shiro:anyTag | 任何情况下都可用 |
shiro:guest=“” | 用户没有身份验证时显示相应信息,即游客访问信息。 |
shiro:user=“” | 已认证后,或者记住我登录可用 |
shiro:authenticated=“” | 已认证后,且不是记住我认证,可用 |
shiro:notAuthenticated=“” | 用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证 |
< shiro:principal/> 或者 shiro:principal=“” | 展示当前用户凭证 |
shiro:hasRole=“角色” | 具有指定角色时可用 |
shiro:lacksRole=“角色” | 没有指定角色时可用 |
shiro:hasAllRoles=“角色1, 角色2” | 包含所有角色时可用 |
shiro:hasAnyRoles=“角色1, 角色2, 角色3” | 包含其中任一个角色时可用 |
shiro:hasPermission=“权限” | 具有指定权限时可用 |
shiro:lacksPermission=“权限” | 没有指定权限时可用 |
shiro:hasAllPermissions=“权限1, 权限2” | 包含所有权限时可用 |
shiro:hasAnyPermissions=“权限1, 权限2” | 包含其中任一个权限时可用 |
案例代码
1、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.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fc</groupId>
<artifactId>09_shiro_11_springboot_thymeleaf</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>09_shiro_11_springboot_thymeleaf</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>
<!--shiro整合SpringBoot-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>
<!--thymeleaf整合shiro-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!--SpringBoot整合Thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、修改 shiroConfig
@Configuration
public class ShiroConfig {
// 自定义ShiroRealm
@Bean
public ShiroRealm getShiroRealm() {
return new ShiroRealm();
}
// 配置Shiro方言
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
// 配置记住我管理器
@Bean
public CookieRememberMeManager getCookieRememberMeManager() {
// 创建简单Cookie对象
SimpleCookie cookie = new SimpleCookie("rememberMeCookie");
// 设置cookie过期时间
cookie.setMaxAge(60 * 60);
// 创建记住我管理器
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
// 添加cookie到管理器中
rememberMeManager.setCookie(cookie);
return rememberMeManager;
}
// 配置安全管理器
@Bean
public DefaultWebSecurityManager getSecurityManager(ShiroRealm shiroRealm, CookieRememberMeManager cookieRememberMeManager) {
// 创建安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义realm
securityManager.setRealm(shiroRealm);
// 设置cookie记住我管理器
securityManager.setRememberMeManager(cookieRememberMeManager);
return securityManager;
}
// 配置过滤器
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 设置登录路径
factoryBean.setLoginUrl("/user/toLogin");
// 有序map
Map<String, String> filterChainMap = new LinkedHashMap<>();
// 添加过滤条件
filterChainMap.put("/user/login", "anon");
filterChainMap.put("/user/toRefuse", "anon");
filterChainMap.put("/user/logout", "logout");
filterChainMap.put("/user/toAdd", "perms[user:add]");
filterChainMap.put("/user/toList", "perms[user:query]");
filterChainMap.put("/user/toIndex", "user");
filterChainMap.put("/**", "authc");
// 设置过滤器链
factoryBean.setFilterChainDefinitionMap(filterChainMap);
// 设置无权限访问跳转路径
factoryBean.setUnauthorizedUrl("/user/toRefuse");
return factoryBean;
}
}
3、修改 index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
欢迎<span th:text="${session.username}"></span>来到主页
<div shiro:hasPermission="user:add">
<a href="/user/toAdd">添加用户</a>
</div>
<div shiro:hasPermission="user:query">
<a href="/user/toList">查看用户</a>
</div>
<a href="/user/logout">退出登录</a>
</body>
</html>
SpringBoot 整合 Shiro、Mybatis
1、创建 SpringBoot 项目并导入对应的依赖
<?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.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fc</groupId>
<artifactId>09-shiro-09-springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>09-shiro-09-springboot</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>
<!--SpringBoot整合Shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>
<!--SpringBoot整合Thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.4.2</version>
</dependency>
<!--SpringBoot 整合MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<!--MySQL-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--逆向工程生成库-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
<!--log-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.7</version>
</dependency>
<!--log-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、使用逆向工程生成对应的 pojo、dao层接口、mapper 映射文件并添加对应的依赖注入注解
3、配置 application.yml 配置文件
server:
port: 8080
# 数据源配置
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/shiro?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# 模板引擎
thymeleaf:
suffix: .html
prefix: classpath:/templates/
mode: HTML
cache: false
encoding: UTF-8
# mybatis相关配置
mybatis:
# 配置别名需要扫描的包
type-aliases-package: com.fc.bean
# mapper映射文件
mapper-locations: classpath:com/fc/mapper/*.xml
configuration:
# 配置日志在控制台显示sql语句
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 开启二级缓存
cache-enabled: true
# shiro配置
shiro:
enabled: true
4、导入工具类 HashUtils
5、创建控制层对象
/**
* 控制层,用来完成对user的相关操作
*/
@Controller
@RequestMapping("user")
public class UserController {
// 注入业务层对象
@Autowired
private UserService userService;
// 跳转至登录页面
@RequestMapping("toLogin")
public String toLogin() {
return "login";
}
// 跳转至主页
@RequestMapping("toIndex")
public String toIndex() {
return "index";
}
/**
* 登录操作
*
* @param user 登录所需要的用户名和密码
* @param rememberMe 是否开启记住我
* @param model model
* @param request 请求对象
* @return 登录成功跳转至主页,否则跳转至拒绝页面
*/
@RequestMapping("login")
public String login(SysUser user, boolean rememberMe, Model model) {
// 获取token
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword(), rememberMe);
// 通过工具类获取主体
Subject subject = SecurityUtils.getSubject();
try {
// 使用token登录
subject.login(token);
// 获取登录的用户
SysUser principal = (SysUser) subject.getPrincipal();
// 获取用户是否已被锁定
String locked = principal.getLocked();
// 判断
if (locked.equals("1")) {
model.addAttribute("msg", "当前用户已被锁定");
return "refuse";
}
System.out.println("登录成功");
Session session = subject.getSession();
session.setAttribute("username", token.getUsername());
// 登录成功跳转至主页
return "index";
} catch (UnknownAccountException e) {
model.addAttribute("msg", "用户不存在");
return "refuse";
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg", "密码错误");
return "refuse";
}
}
// 退出登录
@RequestMapping("logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
// 跳转到拒绝页面
@RequestMapping("refuse")
public String refuse() {
return "refuse";
}
// 跳转到添加用户界面
@RequestMapping("toAdd")
public String toAdd() {
return "add";
}
// 添加用户
@RequestMapping("add")
public String add(SysUser user) {
// 添加用户获取是否添加成功
boolean flag = userService.addUser(user);
if (flag) {
// 添加成功跳转至主页
return "index";
}
return "refuse";
}
}
6、创建对应业务层接口以及实现类
public interface UserService {
// 根据用户名获取用户
SysUser findUserByUsername(String username);
// 根据用户名获取对应的角色
List<SysRole> findRoleByUsername(String username);
// 根据角色获取对应的权限
List<SysPermission> findPermissionByRoleName(String... roleNames);
// 添加用户
boolean addUser(SysUser user);
}
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private SysUserMapper userMapper;
@Override
public SysUser findUserByUsername(String username) {
SysUserExample userExample = new SysUserExample();
SysUserExample.Criteria criteria = userExample.createCriteria();
criteria.andUsernameEqualTo(username);
List<SysUser> list = userMapper.selectByExample(userExample);
if (list.size() > 0) {
return list.get(0);
}
return null;
}
@Override
public List<SysRole> findRoleByUsername(String username) {
return userMapper.findRoleByUsername(username);
}
@Override
public List<SysPermission> findPermissionByRoleName(String... roleNames) {
return userMapper.findPermissionByRoleName(roleNames);
}
@Override
public boolean addUser(SysUser user) {
// 对密码进行加密
Map<String, String> map = HashUtils.getCredential(user.getPassword());
// 设置加密过后的密码和盐
user.setPassword(map.get("password"));
user.setSalt(map.get("salt"));
// 获取受影响的行数
int affectedRows = userMapper.insertSelective(user);
return affectedRows > 0;
}
}
7、修改对应 dao 层接口以及 mapper 文件
@Repository("userMapper")
public interface SysUserMapper {
int insertSelective(SysUser record);
List<SysUser> selectByExample(SysUserExample example);
// 根据用户名获取对应的角色
List<SysRole> findRoleByUsername(String username);
// 根据角色获取对应的权限
List<SysPermission> findPermissionByRoleName(@Param("roleNames") String[] roleNames);
}
<?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.fc.dao.SysUserMapper">
<cache/>
<!--根据角色名获取权限-->
<select id="findPermissionByRoleName" resultType="com.fc.pojo.SysPermission">
select sp.* from sys_role
inner join sys_role_permission srp
on sys_role.id = srp.sys_role_id
inner join sys_permission sp
on srp.sys_permission_id = sp.id
where sys_role.name in
<foreach collection="roleNames" item="roleName" open="(" close=")" separator=",">
#{roleName}
</foreach>
</select>
<!--根据用户名查询角色-->
<select id="findRoleByUsername" parameterType="java.lang.String" resultType="com.fc.pojo.SysRole">
select sr.id, sr.name, sr.available from sys_user
inner join sys_user_role
on sys_user.id = sys_user_role.sys_user_id
inner join sys_role sr
on sys_user_role.sys_role_id = sr.id
where sys_user.username = #{username}
</select>
</mapper>
8、配置自定义 Realm
/**
* 核心自定义Realm
*/
public class ShiroRealm extends AuthorizingRealm {
// realm名称
private static final String realmName = "shiroRealm";
@Autowired
private UserService userService;
public ShiroRealm() {
// 指定密码匹配方式
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(HashUtils.ALGORITHM_NAME);
// 指定迭代次数
hashedCredentialsMatcher.setHashIterations(HashUtils.HASH_ITERATIONS);
// 存储散列后的密码是为16进制
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
// 设置使用自定义匹配方式
setCredentialsMatcher(hashedCredentialsMatcher);
}
// 授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 自定义授权信息对象
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 获取用户名
SysUser user = (SysUser) principals.getPrimaryPrincipal();
// 根据用户名获取角色
List<SysRole> roles = userService.findRoleByUsername(user.getUsername());
// 如果没有角色,直接返回
if (null == roles || roles.size() == 0) {
return authorizationInfo;
}
// 获取角色对象中的角色
Set<String> roleSet = new HashSet<>();
for (SysRole role : roles) {
roleSet.add(role.getName());
}
// 授权信息添加角色
authorizationInfo.addRoles(roleSet);
// set集合转字符串数组
String[] roleNames = roleSet.toArray(new String[roleSet.size()]);
// 获取角色对应的权限
List<SysPermission> permissions = userService.findPermissionByRoleName(roleNames);
// 没有权限直接返回
if (null == permissions || permissions.size() == 0) {
return authorizationInfo;
}
// 获取对应的权限
Set<String> permissionSet = new HashSet<>();
for (SysPermission permission : permissions) {
permissionSet.add(permission.getPercode());
}
// 添加权限到授权信息对象中
authorizationInfo.addStringPermissions(permissionSet);
return authorizationInfo;
}
// 认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取token中的用户名
String username = (String) token.getPrincipal();
// 通过用户名获取User对象
SysUser user = userService.findUserByUsername(username);
// 如果user为空
if (user == null) {
// 说明用户名不存在,抛出UnknownAccountException异常
throw new UnknownAccountException("用户不存在");
}
// 获取User对象中的密码和盐
String userPassword = user.getPassword();
String salt = user.getSalt();
// 返回认证信息,其中包括账号、密码、盐以及realm名称
return new SimpleAuthenticationInfo(user, userPassword, ByteSource.Util.bytes(salt), realmName);
}
}
9、配置 shiro 配置类
/**
* Shiro相关配置
*/
@Configuration
public class ShiroConfig {
// 配置自定义Realm
@Bean
public ShiroRealm getShiroRealm() {
return new ShiroRealm();
}
// 配置记住我管理器
@Bean
public CookieRememberMeManager getCookieRememberMeManager() {
// 创建简单Cookie对象
SimpleCookie cookie = new SimpleCookie("rememberMeCookie");
// 设置cookie过期时间
cookie.setMaxAge(60 * 60);
// 创建记住我管理器
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
// 添加cookie到管理器中
rememberMeManager.setCookie(cookie);
return rememberMeManager;
}
// 配置SecurityManager
@Bean("securityBean")
public DefaultWebSecurityManager getSecurityManager(ShiroRealm shiroRealm, CookieRememberMeManager cookieRememberMeManager) {
// 创建安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义Realm
securityManager.setRealm(shiroRealm);
// 设置cookie记住我管理器
securityManager.setRememberMeManager(cookieRememberMeManager);
return securityManager;
}
// 过滤器,用于配置过滤请求路径
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getFactoryBean(DefaultWebSecurityManager securityManager) {
// 创建工厂对象
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
factoryBean.setSecurityManager(securityManager);
// 设置登录路径
factoryBean.setLoginUrl("/user/toLogin");
// 创建有序的map
Map<String, String> map = new LinkedHashMap<>();
// 添加过滤规则
map.put("/js/**", "anon");
map.put("/css/**", "anon");
map.put("/img/**", "anon");
map.put("/login", "anon");
map.put("/user/login", "anon");
map.put("/refuse", "anon");
map.put("/user/toAdd", "perms[user:create]");
map.put("/user/**", "user");
map.put("/user/logout", "logout");
map.put("/**", "authc");
// 添加过滤规则到factoryBean中
factoryBean.setFilterChainDefinitionMap(map);
// 设置无权限访问跳转路径
factoryBean.setUnauthorizedUrl("/user/refuse");
return factoryBean;
}
}
10、配置启动类
@SpringBootApplication
// 扫描dao包下的mapper文件
@MapperScan("com.fc.dao")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 获取对应的权限
Set permissionSet = new HashSet<>();
for (SysPermission permission : permissions) {
permissionSet.add(permission.getPercode());
}
// 添加权限到授权信息对象中
authorizationInfo.addStringPermissions(permissionSet);
return authorizationInfo;
}
// 认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取token中的用户名
String username = (String) token.getPrincipal();
// 通过用户名获取User对象
SysUser user = userService.findUserByUsername(username);
// 如果user为空
if (user == null) {
// 说明用户名不存在,抛出UnknownAccountException异常
throw new UnknownAccountException("用户不存在");
}
// 获取User对象中的密码和盐
String userPassword = user.getPassword();
String salt = user.getSalt();
// 返回认证信息,其中包括账号、密码、盐以及realm名称
return new SimpleAuthenticationInfo(user, userPassword, ByteSource.Util.bytes(salt), realmName);
}
}
9、配置 shiro 配置类
```java
/**
* Shiro相关配置
*/
@Configuration
public class ShiroConfig {
// 配置自定义Realm
@Bean
public ShiroRealm getShiroRealm() {
return new ShiroRealm();
}
// 配置记住我管理器
@Bean
public CookieRememberMeManager getCookieRememberMeManager() {
// 创建简单Cookie对象
SimpleCookie cookie = new SimpleCookie("rememberMeCookie");
// 设置cookie过期时间
cookie.setMaxAge(60 * 60);
// 创建记住我管理器
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
// 添加cookie到管理器中
rememberMeManager.setCookie(cookie);
return rememberMeManager;
}
// 配置SecurityManager
@Bean("securityBean")
public DefaultWebSecurityManager getSecurityManager(ShiroRealm shiroRealm, CookieRememberMeManager cookieRememberMeManager) {
// 创建安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义Realm
securityManager.setRealm(shiroRealm);
// 设置cookie记住我管理器
securityManager.setRememberMeManager(cookieRememberMeManager);
return securityManager;
}
// 过滤器,用于配置过滤请求路径
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getFactoryBean(DefaultWebSecurityManager securityManager) {
// 创建工厂对象
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
factoryBean.setSecurityManager(securityManager);
// 设置登录路径
factoryBean.setLoginUrl("/user/toLogin");
// 创建有序的map
Map<String, String> map = new LinkedHashMap<>();
// 添加过滤规则
map.put("/js/**", "anon");
map.put("/css/**", "anon");
map.put("/img/**", "anon");
map.put("/login", "anon");
map.put("/user/login", "anon");
map.put("/refuse", "anon");
map.put("/user/toAdd", "perms[user:create]");
map.put("/user/**", "user");
map.put("/user/logout", "logout");
map.put("/**", "authc");
// 添加过滤规则到factoryBean中
factoryBean.setFilterChainDefinitionMap(map);
// 设置无权限访问跳转路径
factoryBean.setUnauthorizedUrl("/user/refuse");
return factoryBean;
}
}
10、配置启动类
@SpringBootApplication
// 扫描dao包下的mapper文件
@MapperScan("com.fc.dao")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}