1. 整合 SpringBoot
1.1 创建项目
1.2 添加依赖
Shiro 官方提供了和 SpringBoot 集成的启动器,使用官方提供的启动器,只需要极少的配置和代码就可以顺利整合SpringBoot。
官方文档地址:http://shiro.apache.org/spring-boot.html
在项目的 pom.xml 中添加如下坐标。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>
完整 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/>
</parent>
<groupId>com.imcode</groupId>
<artifactId>shiro-boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro-boot</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
1.3 配置信息
配置文件
application.properties 文件添加如下配置:
shiro.loginUrl=/login # 系统登录的链接 GET 请求 默认是login.jsp
配置类
只需要配置Realm和过滤规则就可以,安全管理器、会话管理器会使用默认配置。
@Configuration
public class ShiroConfig {
@Bean
public Realm realm() {
TextConfigurationRealm realm = new TextConfigurationRealm();
realm.setUserDefinitions("jack=123456,user \n rose=111111,admin,user");
realm.setRoleDefinitions("admin=dept:view,dept:add \n user=dept:view");
return realm;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition =
new DefaultShiroFilterChainDefinition();
// 不拦截登录
chainDefinition.addPathDefinition("/login", "anon");
// 用户退出
chainDefinition.addPathDefinition("/logout", "logout");
// 除登录和退出外,其它所有资源都需要认证通过才能访问
chainDefinition.addPathDefinition("/**", "user");
return chainDefinition;
}
}
1.4 控制器和页面
登录控制器
@Controller
public class LoginController {
/**
* 跳转到登录页面
* @return
*/
@GetMapping("/login")
public String login() {
return "login";
}
/**
* 登录系统
* @return
*/
@PostMapping("/login")
public String login(String username, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token =
new UsernamePasswordToken(username,password);
subject.login(token);
return "redirect:index";
}
/**
* 跳转到系统主页
* @return
*/
@GetMapping("/index")
public String main() {
return "index";
}
}
页面
login.html 内容如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" >
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form th:action="@{/login}" method="post" shiro:guest>
<input name="username" type="text"/><br>
<input name="password" type="password"/><br>
<input type="submit" value="登录"/>
</form>
</body>
</html>
index.html 内容如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" >
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Hello Shiro
<a th:href="@{/logout}">退出</a>
<hr>
<a th:href="@{/dept/add}">新增部门</a> | <a th:href="@{/dept/list}">部门列表</a>
</body>
</html>
1.5 认证
认证验证
- 启动应用,访问 http://localhsot:8080, 未登录的时候访问根目录,跳转到了登录页面,访问其它链接也会跳转到登录页面。
- 用户名和密码都输入正确,会进入系统首页。
- 如果用户名输入错误,抛出
org.apache.shiro.authc.UnknownAccountException
异常。 - 如果用户名正确,密码输入错误,抛出
org.apache.shiro.authc.IncorrectCredentialsException
异常。 通过查看源码,以上两个异常类都是
org.apache.shiro.ShiroException.AuthenticationException
的子类。统一处理认证异常
新建统一异常处理类:
内容如下:@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(AuthenticationException.class) public Map<String, Object> handleUnknownAccountException(AuthenticationException e) { Map<String, Object> map = new HashMap<>(); map.put("code", HttpStatus.UNAUTHORIZED.value()); map.put("msg", "用户名或密码错误"); return map; } }
1.6 授权
权限注解
Shiro 提供了一些注解用于权限控制,使用方式非常简单,在需要权限控制的方法上添加相关注解就可以了。Shiro 提供的权限注解如下:
@RequiresAuthentication
- 表示当前 Subject 已经通过认证;即 subject.isAuthenticated() 返回 true。
@RequiresUser
- 表示当前 Subject 已经通过认证或者通过记住我登录成功。
@RequiresGuest
- 表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。
@RequiresRoles(value={"admin", "user"}, logical= Logical.OR)
- 表示当前 Subject 需要角色 admin 或 user。
@RequiresPermissions (value={"dept:view","dept:add"}, logical= Logical.AND)
表示当前 Subject 需要权限
dept:view
和dept:add
。编写部门控制器
@RestController @RequestMapping("/dept") public class DeptController { @GetMapping("/list") // @RequiresPermissions("dept:view") @RequiresRoles(value = {"admin","user"},logical= Logical.OR) public String list() { return "dept-view"; } @GetMapping("/add") // @RequiresPermissions("dept:add") @RequiresRoles({"admin"}) public String add() { return "dept-add"; } }
验证权限注解是否生效。如果Subject没有权限注解配置的角色和权限,会抛出
org.apache.shiro.authz.AuthorizationException
异常。
权限标签
Shiro 官方提供了用于JSP页面的权限标签,如果使用JSP作为试图,请参考:http://shiro.apache.org/web.html#Web-JSP%2FGSPTagLibrary 。
我们使用 thymeleaf 模板,thymeleaf 官方没有提供对应的 shiro 权限标签,我们使用第三方扩展的标签库。
该标签库的地址:https://github.com/theborakompanioni/thymeleaf-extras-shiro。引入标签
在pom.xml 中添加 thymeleaf-extras-shiro 相关依赖:
<dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
引入依赖后,需要在
ShiroConfig
中配置该方言标签:@Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); }
标签说明
参考官方文档:https://github.com/theborakompanioni/thymeleaf-extras-shiro
使用示例
login.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" > <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <shiro:guest> <form th:action="@{/login}" method="post"> <input name="username" type="text"/><br> <input name="password" type="password"/><br> <input type="submit" value="登录"/> </form> </shiro:guest> <shiro:authenticated> 您好, <shiro:principal/> <br> <a href="/index">系统首页</a> </shiro:authenticated> </body> </html>
index.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" > <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> Hello <shiro:principal/> <a th:href="@{/logout}">退出</a> <br> <hr> 您的角色是: <p shiro:hasRole="admin">管理员</p> <p shiro:hasRole="user">普通用户</p> <hr> <a th:href="@{/dept/add}" shiro:hasPermission="dept:add">新增部门</a> <a th:href="@{/dept/list}" shiro:hasPermission="dept:view">部门列表</a> </body> </html>
统一处理授权异常
在 GlobalExceptionHandler 类中添加授权异常处理方法:
@ExceptionHandler(AuthorizationException.class) public Map<String, Object> handleAuthorizationException(AuthorizationException e) { Map<String, Object> map = new HashMap<>(); map.put("code", HttpStatus.FORBIDDEN.value()); map.put("msg", "您没有访问该资源的权限"); return map; }
2. 自定义 Realm
在前面我们使用 TextConfigurationRealm,将 Subject 的认证和授权信息写到了该Realm的对象里,实际开发中,用户的认证和授权信息是存储在数据库的。我们需要自定义Realm从数据库来读取授权和认证信息,我们先来实现用户的业务逻辑。
2.1 User
引入lombok:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
```java package com.imcode.entity;
import lombok.AllArgsConstructor; import lombok.Data;
@Data @AllArgsConstructor public class User { private Long userId; private String username; private String password; }
<a name="qSVMA"></a>
## 2.2 UserService
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1429839/1615605868531-54391ace-d541-440b-af87-f33987001394.png#align=left&display=inline&height=281&margin=%5Bobject%20Object%5D&name=image.png&originHeight=281&originWidth=303&size=9128&status=done&style=none&width=303)<br />UserService 接口:
```java
public interface UserService {
User queryByUsername(String username);
}
UserServiceImpl 实现类:
@Service
public class UserServiceImpl implements UserService {
@Override
public User queryByUsername(String username) {
if ("jack".equals(username)) {
// 实际开发中用户信息从数据库查询获得
Long dbUserId = 1007L;
String dbUsername = username;
String dbpassword = "123456";
return new User(dbUserId, dbUsername, dbpassword);
}
return null;
}
}
2.3 Realm
Realm 接口
Realm 接口和类的关系:
最基础的是Realm接口,CachingRealm
负责缓存处理,AuthenticatingRealm
负责认证,AuthorizingRealm
负责授权,通常自定义的realm 继承 AuthorizingRealm
。
编写 Realm
自定义的 ShiroRealm
继承 AuthorizingRealm
,需要实现父类两个抽象方法:
- doGetAuthenticationInfo(AuthenticationToken token)
- 该方法实现认证
doGetAuthorizationInfo(PrincipalCollection principals)
该方法实现授权
public class ShiroRealm extends AuthorizingRealm { /** * 认证 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("认证..."); return null; } /** * 授权 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("授权..."); return null; } }
配置 Realm
在ShiroConfig中配置该Realm:
@Bean public Realm realm() { // TextConfigurationRealm realm = new TextConfigurationRealm(); // realm.setUserDefinitions("jack=123456,user \n rose=111111,admin,user"); // realm.setRoleDefinitions("admin=dept:view,dept:add \n user=dept:view"); ShiroRealm realm = new ShiroRealm(); return realm; }
2.4 认证
public class ShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 认证 * 调用 subject.login(token) 会调用该方法并传入token * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("认证..."); UsernamePasswordToken userToken = (UsernamePasswordToken) token; // 根据用户名从数据库查询用户 User user = userService.queryByUsername(userToken.getUsername()); // 用户名不存在 if (user == null) { throw new UnknownAccountException("用户名或密码错误"); } // 创建简单认证信息对象并返回 // 第二个参数是数据库中存储的用户密码 return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); } /** * 授权 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } }
2.5 授权
/** * 授权 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("授权..."); // 创建简单授权信息对象 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRole("user"); info.addStringPermission("dept:view"); return info; }
3. 密码加密
3.1 MD5
简介
MD5(Message-Digest Algorithm 5)是一种被广泛使用的消息摘要算法,也称为哈希算法、散列算法,可以产生出一个定长的128位(16字节)的散列值(Hash Value),一般用于数字签名以确保信息传输完整性与密码的加密存储。
简单的说,使用MD5可以将任意长度的明文字符串生产为128位的哈希值。MD5生产的hash值是不可逆的,因为MD5算法里面有很多不可逆的运算,会丢失很多原文的信息,无法找回,所以该算法是不可逆的。
密码的存储方案:
明文存储到数据库 绝对不可以
- MD5 散列后存储 暴力枚举攻击(时间长)、字典攻击(空间大)、彩虹表攻击(折中)
- MD5 加盐散列后存储 有效避免彩虹表攻击
解决方案:让相同的明文密码散列后的HASH值都不一样。
参考博客:
https://blog.csdn.net/qq_44970883/article/details/111112681
https://www.sohu.com/a/197288583_684445
Md5Hash
Shiro 的 Md5Hash 类实现集成了该算法,具体使用方式如下:
@Test
public void test() {
/**
* 只使用散列算法
*/
String password1 = new Md5Hash("123456").toHex();
System.out.println(password1);
/**
* 散列算法 + 随机盐
*/
String password2 = new Md5Hash("123456", UUID.randomUUID().toString()).toHex();
System.out.println(password2);
/**
* 散列算法 + 随机盐 + 多次散列
* MD5(MD5(MD5(m)))
*/
String password3 = new Md5Hash("123456", UUID.randomUUID().toString(),3).toHex();
System.out.println(password3);
}
在数据库保存用户密码的时候,可以使用 散列算法 + 随机盐 + 多次散列 后的密文存储到数据库。
3.2 Shiro 配置
配置凭证匹配器
添加凭证匹配器:
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 使用MD5散列算法
matcher.setHashAlgorithmName("MD5");
// 设置散列次数
matcher.setHashIterations(3);
return matcher;
}
修改认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
System.out.println("认证...");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// 根据用户名从数据库查询用户
User user = userService.queryByUsername(userToken.getUsername());
// 用户名不存在
if (user == null) {
throw new UnknownAccountException("用户名或密码错误");
}
// 创建简单认证信息对象并返回
// 第二个参数是数据库中存储的用户密码
// 第三个参数是盐
ByteSource salt = ByteSource.Util.bytes(user.getUsername());
return new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName());
}
模拟数据库存储密文
使用
ByteSource salt = ByteSource.Util.bytes(“jack”);
String password = new Md5Hash(“123456”, salt ,3).toHex();
对密码123456 进行MD5 哈希,哈希的次数是3。该次数和ShiroConfig中的次数要保持一致。
HASH后的密文是:4ebb57a4e260f12e429c469bd6d77bd0
修改UserService的 queryByUsername() 方法:
测试验证
4. 缓存管理
目前的方案中,用户每次访问系统资源的时候,都需要调用 ShiroRealm 中的授权方法doGetAuthorizationInfo(PrincipalCollection principals),该方法每次都会访问数据库获取用户的授权信息,为了提高性能,我们可以使用缓存来缓存用户的授权信息,避免每次都去访问数据库。
Shiro 提供和基于内存的缓存方案,以及 ehcache 缓存框架的整合。
4.1 内存缓存
启用内存缓存非常简单,只需要在 ShiroConfig 中配置缓存管理器即可,配置好的缓存管理器会自动注入安全管理器。
@Bean
public CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
4.2 ehcache 缓存
pom.xml
引入 shiro 对 ehcache 的支持包和 ehcache 包:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.8.0</version>
</dependency>
配置缓存管理器
5. 分布式缓存和集群会话
5.1 介绍
在集群环境中,我们需要集群中的多台服务器能够共享缓存和会话,目前流行的方案是使用Redis数据库来作为缓存服务器。Shiro 官方没有提供对 Redis 做缓存的集成支持,在官方提供的第三方扩展库中有对 Redis的支持。
参考地址:http://shiro.apache.org/integration.html
该项目的名称叫 shiro-redis
该项目文档地址:https://github.com/alexxiyang/shiro-redis/tree/master/docs。
5.2 集成 shiro-redis
shiro-redis 提供了和SpringBoot 整合的 starter,让整合变得非常简单。shiro-redis 可以用来缓存用户的会话信息和认证信息。
pom.xml
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.3.1</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
shiro-redis 目前最新版本是 3.3.1,该版本以来的 shiro 版本是1.6.0。我们项目使用shiro 1.7.1,要排除掉 shiro 1.6.0的依赖。
application.properties
添加如下配置:shiro-redis.cache-manager.principal-id-field-name=userId
userId 是User 类中用户id属性的名称。给配置用来针对不同的主体(用户) 生成唯一的缓存key。
User 对象序列化
User 对象是作为 principal 的,所以User 对象的信息如果缓存到Redis,则该类需要实现序列化接口。
验证效果:
将上一小节配置的缓存管理器注释掉
用户登录成功以后,访问系统功能,查看Redis中数据的变化:
5.3 会话管理器
shiro 提供了三种会话管理器:
- DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;
- ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于Web环境,直接使用 Servlet 容器的会话;
- DefaultWebSessionManager:用于Web环境,Shiro 维护和管理会话,不使用Servlet容器的会话。
- 在 配置文件添加如下配置:
shiro.userNativeSessionManager=true
- 在 配置文件添加如下配置:
可实现 Shiro 维护和管理会话。**
WEB环境会话管理总结:
- HttpServletRequest 对象被 Shiro 包装成 ShiroHttpServletRequest对象 。
- 通过 request.getSession() 获取 session,该session是Servlet容器的还是Shiro的,由使用的安全管理器SecurityManager 和会话管理器 SessionManager 决定。
- 不管是通过 request.getSession() 或者 subject.getSession() 获取到session对象,两者都是等价的。操作 subject.getSession() 等价于操作 request.getSession()。