shiro入门笔记
第一章 Shiro简介
1.1 什么是Shiro?
Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多。
1.2 Shiro框架结构
shiro是一个独立的安全框架,不跟任何的框架或者容器捆绑,可以独立运行,完整的框架结构如下
1.2.1 Shiro主要三大核心注件
三个核心组件:Subject, SecurityManager 和 Realms。
Subject:即“当前操作用户”。不仅仅是指人,可以是第三方的应用交互,比如,后台进程自己访问应用。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager:它是Shiro框架的核心,SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
1.2.2 主要功能
Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web 支持,可以非常容易的集成到 Web 环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我功能,开启默认下次不用登录。
第二章 认证
2.1 新建一个springboot项目
- 完整的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>
<groupId>com.lyf.shiro</groupId>
<artifactId>shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<shiro.version>1.5.1</shiro.version>
<thymeleaf.extras.shiro.version>2.1.0</thymeleaf.extras.shiro.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>${thymeleaf.extras.shiro.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- Shiro+JWT start -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- Shiro end -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.lyf.shiro.ShiroApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
- 创建ShiroConfig.java
package com.lyf.shiro.config;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Author: lyf
* @CreateTime: 2022-11-17
* @description: Shiro配置
*/
@Configuration
public class ShiroConfig {
/**
* 定义一个默认的安全管理器,这个管理器将管理所有的Shiro组件实例,
* @param realm 用于访问数据源的域
* @return
*/
@Bean
public SecurityManager securityManager(Realm realm){
//创建一个默认的实例放入到ioc容器当中
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
/**
* 定义一个数据源域,这个域Realm需要自己去实现
* @return
*/
@Bean
public MyRealm realm(){
MyRealm myRealm = new MyRealm();
return myRealm;
}
/**
* 定义一个过滤器,过滤所有的请求
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setLoginUrl("/");
shiroFilter.setSuccessUrl("/success");
shiroFilter.setUnauthorizedUrl("/noPermission");
Map<String,String> filterMap = new LinkedHashMap<>();
/**
/admin/** 表示一个请求名字的通配, 以admin开头的任意子路径下的所有请求
* anon: 表示该路径不需要进行认证,已访客身份访问
authc 表示该路径需要进行认证(登录),只有认证(登录)通过才能访问
** 表示任意子路径
* 表示任意的一个路径
? 表示 任意的一个字符
*/
filterMap.put("/login","anon");
filterMap.put("admin/**","authc");
filterMap.put("user/**","authc");
//注释表示拦截所有路径的请求,如果换成anon,则放行所有的请求路径
filterMap.put("/**","authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}
- 创建MyRealm.java
package com.lyf.shiro.config;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.Realm;
/**
* @Author: lyf
* @CreateTime: 2022-11-17
* @description:
*/
public class MyRealm implements Realm {
/**
* 返回这个域的名称
* @return
*/
@Override
public String getName() {
return "MyRealm";
}
/**
* 表示是否开启这个域的验证,为true表示开启,为false表示不开启这个域的验证
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return true;
}
/**
* 登录验证需要执行的方法,在主题调用subject.login(token),具体的验证就会有SecurityManager调用到这个方法进行验证;
* @param token
* @return
* @throws AuthenticationException
*/
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
char[] password = usernamePasswordToken.getPassword();
if ("admin".equals(username)&&"123".equals(password)){
//返回验证通过的信息到授权的处理器,授权处理器根据传人的参数设置角色与权限
return new SimpleAuthenticationInfo(username,password,getName());
}else {
throw new AuthenticationException("登录失败");
}
}
}
- 定义 login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="login" method="post">
username:<input name="username" type="text"><br>
password:<input type="password" name="password"><br>
<input type="submit" value="submit">
</form>
</body>
</html>
- 定义 success.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录成功</h1>
</body>
</html>
- 定义 noPermission.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>对不起!您没有权限操作!</h1>
</body>
</html>
- 定义 loginController.java
@Controller
public class loginController {
/**
* 跳转到登录页面
* @return
*/
@RequestMapping("/")
public String toLogin(){
return "login";
}
/**
* 登录控制器
* @param username 用户名
* @param password 密码
* @param response 客户端响应,输入错误信息
* @return
* @throws IOException
*/
@PostMapping("/login")
public String login(String username, String password, HttpServletResponse response) throws IOException {
PrintWriter writer = response.getWriter();
// response.setCharacterEncoding("UTF-8");
//shiro 提供的一个用户与密码的实现类,可以把用户提供的凭证信息封装成token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
//获取当前的主体(当前登录的对象)
Subject subject = SecurityUtils.getSubject();
//判断当前对象是否登录
if (!subject.isAuthenticated()) {
try {
//每次登录前,执行登出操作,清空缓存
subject.logout();
//主体调用这个登录方法会跳转到realm中去执行验证
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
writer.write("账号不存在!");
writer.close();
return "login";
} catch (LockedAccountException e) {
writer.write("账号被锁定");
writer.close();
return "login";
}
}
return "redirect:success";
}
/**
* 登录成功跳转页面
* @return
*/
@RequestMapping("/success")
public String success(){
return "success";
}
/**
*无权限页面
* @return
*/
@RequestMapping("/noPermission")
public String noPermiss(){
return "noPermission";
}
/**
* user/** 下的所有路径
* @return
*/
@RequestMapping("/user/add")
public String user(){
return "";
}
/**
* 访问admin/** 下所有路径
* @return
*/
@RequestMapping("/admin/add")
public String admin(){
return "";
}
}
2.2 测试接口
访问login
登录成功
访问user/add 或者admin/add 会跳转到登录页面。
**总结:**到这里,shiro的简单认证就完成了
2.3 登录流程总结
principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。
credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。
最常见的 principals 和 credentials 组合就是用户名 / 密码了。
另外两个相关的概念是之前提到的 Subject 及 Realm,分别是主体及验证主体的数据源。
登录流程:
1.首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
2.SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
3.Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
4.Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
5.Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证成功了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访。
2.3.1 登录认证策略
FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;
AtLeastOneSuccessfulStrategy:只要有一个 Realm 验证成功即可,和 FirstSuccessfulStrategy 不同,返回所有 Realm 身份验证成功的认证信息;
AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。ModularRealmAuthenticator 默认使用 AtLeastOneSuccessfulStrategy 策略。
Shiro支持的各种数据域结构图:
AuthorizingRealm授权域继承了AuthenticatingRealm认证域的类,所以我们只需要继承AuthorizingRealm域就可以实现自己的认证与授权操作。
package com.lyf.shiro.config;
import com.lyf.shiro.jwt.JwtToken;
import com.lyf.shiro.user.domain.Role;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @Author: lyf
* @CreateTime: 2022-11-17
* @description:
*/
public class MyAuthenticatingRealm extends AuthorizingRealm {
@Override
public String getName() {
return super.getName();
}
@Override
public boolean supports(AuthenticationToken token) {
return super.supports(token);
}
/**
* 登录认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
}
/**
* 授权,设置对应主体的角色与权限信息
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
第三章 授权
3.1授权方式
1.通过代码授权,使用subject主体实例去实现
2.通过注解的方式
@RequiresAuthentication:使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在已经过认证。
@RequiresGuest:使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是gust身份,不需要经过认证或者在原先的session中存在记录。
@RequiresPermissions:当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。
@RequiresRoles:当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。
@RequiresUser:当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。
@RequiresGuest
@RequiresUser
@RequiresAuthentication
@RequiresRoles("admin")
@RequiresPermissions("sys:user:view")
public String test(){
return "null";
}
- 通过页面方式
3.2 权限通配符
规则:“资源标识符:操作:对象实例 ID” 即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串,“:”表示资源/操作/实例的分割;“,”表示操作的分割;“*”表示任意资源/操作/实例
1.操作单个权限 @RequiresPermissions(“system:user:update”)
2.操作多个权限 @RequiresPermissions({“system:user:update,system:user:delete”})如果拥有两个权限可以简写@RequiresPermissions({“system:user:update,delete”})
3.拥有全部权限 @RequiresPermissions(“system:user:")或者@RequiresPermissions(“system:user”)
4.单个权限 @RequiresPermissions(:view*”)
5.单个实例的单个权限 @RequiresPermission(“user:view:1”) 表示实例1对当前资源有访问权限。
6.Shiro 对权限字符串缺失部分的处理。
前缀匹配原则:如(“system:delete:”)等价于(“system:delete:*”),而(“system”)等价于(“system: * ”)与(“system: * : *”);这样就完成了前缀匹配。
后缀匹配原则:如(“ :update”)不能匹配(“system:user:update”)需要知道全部匹配规则,如(“ * : :update”)才能匹配到相应的资源。
3.3 代码授权演示
这个继承了AuthorizingRealm 类,而该类又继承了认证类AuthenticatingRealm,所以我们在一个realm中实现自己的认证与授权方法。
package com.lyf.shiro.config;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import java.util.HashSet;
import java.util.Set;
/**
* @Author: lyf
* @CreateTime: 2022-11-17
* @description:
*/
public class MyRealm extends AuthorizingRealm {
/**
* 返回这个域的名称
* @return
*/
@Override
public String getName() {
return super.getName();
}
/**
* 表示是否开启这个域的验证,为true表示开启,为false表示不开启这个域的验证
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return super.supports(token);
}
/**
* 授权
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取主要的身份信息这里获取到的是认证SimpleAuthenticationInfo(username,password,getName());传过来的用户名
//通过用户名去数据库查询角色权限信息,这个做一个简单的角色权限封装
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
//角色集合
Set<String> role = new HashSet<>();
//permission集合
Set<String> permission = new HashSet<>();
if ("admin".equals(primaryPrincipal)){
role.add("admin");
role.add("user");
permission.add("system:user:*");
permission.add("system:admin:add");
}else {
role.add("user");
permission.add("system:user:add");
}
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(role);
simpleAuthorizationInfo.setStringPermissions(permission);
return simpleAuthorizationInfo;
}
/**
* 登录验证需要执行的方法,在主题调用subject.login(token),
* 具体的验证就会有SecurityManager调用到这个方法进行验证;
* @param token
* @return
* @throws AuthenticationException
*/
@Override
public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
String password = usernamePasswordToken.getPassword().toString();
/**
* 这个的用户名与密码应该从数据库拿到然后做对比的,我这里做了简单地点封装
*/
if ("admin".equals(username)||"user".equals(username)){
//返回验证通过的信息到授权的处理器,授权处理器根据传人的参数设置角色与权限
return new SimpleAuthenticationInfo(username,password,getName());
}else {
throw new AuthenticationException("登录失败");
}
}
}
CredentialsMatcher类是拥有密码匹配认证,如果正确返回true;
public class MyCredentialsMatcher implements CredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
System.out.println(usernamePasswordToken.getUsername());
return true;
}
}
修改ShiroConfig类的MyRealm类方法
@Bean
public MyCredentialsMatcher credentialsMatcher(){
return new MyCredentialsMatcher();
}
@Bean
public MyRealm realm(){
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(credentialsMatcher());
return myRealm;
}
第四章 过滤器
4.1过滤器类关系
1、NameableFilter
NameableFilter 给 Filter 起个名字,如果没有设置默认就是 FilterName;还记得之前的如 authc 吗?当我们组装拦截器链时会根据这个名字找到相应的拦截器实例;
2、OncePerRequestFilter
OncePerRequestFilter 用于防止多次执行 Filter 的;也就是说一次请求只会走一次拦截器链;另外提供 enabled 属性,表示是否开启该拦截器实例,默认 enabled=true 表示开启,如果不想让某个拦截器工作,可以设置为 false 即可。
3、ShiroFilter
ShiroFilter 是整个 Shiro 的入口点,用于拦截需要安全控制的请求进行处理,这个之前已经用过了。
4、AdviceFilter
AdviceFilter 提供了 AOP 风格的支持,类似于 SpringMVC 中的 Interceptor
5、PathMatchingFilter
PathMatchingFilter 提供了基于 Ant 风格的请求路径匹配功能及拦截器参数解析的功能,如“roles[admin,user]”自动根据“,”分割解析到一个路径参数配置并绑定到相应的路径:
6、AccessControlFilter
AccessControlFilter 提供了访问控制的基础功能;比如是否允许访问/当访问拒绝时如何处理等
4.2 自定义过滤器
public class MyFilter extends AuthenticatingFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
return false;
}
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
return null;
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
return super.onLoginFailure(token, e, request, response);
}
}
修改ShiroConfig类,设置过滤器
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//自定义过滤器
Map<String, Filter> myFilterMap = new HashMap<>();
myFilterMap.put("FILTER",new MyFilter());
shiroFilter.setFilters(myFilterMap);
......
第五章 加密&解密
5.1 常用加密算法
Base64 加密&解密
/**
* shiro提供的Base64编码与解码
*/
@Test
public void test(){
String str = "hello world";
String encode = Base64.encodeToString(str.getBytes());
System.out.println(encode);
String decode = Base64.decodeToString(encode.getBytes());
System.out.println(decode);
}
MD5Hash加密
@Test
public void test1(){
String source = "hello,world";
String salt = "123";
String password = new Md5Hash(source,salt).toString();
System.out.println(password);
}
Sha256Hash加密算法
String source = "hello,world";
String salt = "123";
String sha1 = new Sha256Hash(source , salt).toString();
SimpleHash加密算法
String str = "hello";
String salt = "123";
//内部使用MessageDigest
String simpleHash = new SimpleHash("SHA-1", str, salt).toString();
AesCipherService加密&解密算法
@Test
public void test2(){
AesCipherService aesCipherService = new AesCipherService();
aesCipherService.setKeySize(128);//设置key的大小
Key key = aesCipherService.generateNewKey();//生成一个key
String password = "aesCipherService";//加密信息
//加密
String encryptPassword = aesCipherService.encrypt(password.getBytes(), key.getEncoded()).toHex();
//解密
String str= new String(aesCipherService.decrypt(Hex.decode(encryptPassword),key.getEncoded()).getBytes());
//输出原加密密文
System.out.println("str"+str);
}
注入PasswordService服务类,即可调用加密算法
public interface PasswordService {
//输入明文密码得到密文密码
String encryptPassword(Object plaintextPassword) throws IllegalArgumentException;
}
第六章 会话管理
6.1shiro会话
shiro提供的会话不依赖任何底层容器,提供了会话管理、会话事件监听、会话存储 / 持久化、容器无关的集群、失效 / 过期支持、对 Web 的透明支持、SSO 单点登录的支持等特性。使用shiro的会话管理可以代替web的会话管理。
6.2会话获取
//获取当前的主体(当前登录的对象)
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();//通过主体获取当前会话
session.setAttribute("name",subject.getPrincipal());//设置会话属性
session.setTimeout(120L);//设置会话超时时间
System.out.println("name:"+session.getAttribute("name"))//打印会话属性值
subject.getSession(true)如果为true,表示如果没有会话就创建一个会话,为false表示没有返回空。
//获取会话唯一id
session.getId();
//获取当前的主机
session.getHost();
//设置会话过期时间
session.setTimeout(120L);
//获取会话过期时间
session.getTimeout();
获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间;如果是 Web 应用,每次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。当 Subject.logout() 时会自动调用 stop 方法来销毁会话。如果在 web 中,调用 javax.servlet.http.HttpSession. invalidate() 也会自动调用 Shiro Session.stop 方法进行销毁 Shiro 的会话
session.getLastAccessTime();
session.getStartTimestamp();
//更新会话最后访问的时间
session.touch();
//销毁会话
session.stop();
6.3 会话管理
Session start(SessionContext context); //启动会话
Session getSession(SessionKey key) throws SessionException; //根据会话Key获取会话
Shiro 提供了三个默认实现:
DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web 环境,其直接使用 Servlet 容器的会话;
DefaultWebSessionManager:用于 Web 环境的实现,可以替代 ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。
第七章 RememberMe
7.1 开启记住我功能
UsernamePasswordToken实现了记住我功能,直接传人值即可
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
usernamePasswordToken.setRememberMe(true);
第八章 缓存
8.1 开启缓存
开启shiro自带的缓存。
导入shiro缓存依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
修改ShiroConfig配置类的Realm。添加缓存的实现,当缓存开启,第一次请求会去访问数据库的数据,后面的请求就不会去访问数据库,而是直接访问缓存当中的数据。
//设置缓存管理器
myRealm.setCacheManager(new EhCacheManager());
//开启全局缓存
myRealm.setCachingEnabled(true);
//开启认证缓存
myRealm.setAuthenticationCachingEnabled(true);
//开启授权认证缓存
myRealm.setAuthorizationCachingEnabled(true);
return myRealm;
第八章 Shiro连接Mybatis
8.1 数据库脚本
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50628
Source Host : localhost:3306
Source Schema : test
Target Server Type : MySQL
Target Server Version : 50628
File Encoding : 65001
Date: 22/11/2022 15:58:03
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` bigint(20) NOT NULL COMMENT 'id',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限名称',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, 'system:user:update');
INSERT INTO `sys_permission` VALUES (2, 'system:user:insert');
INSERT INTO `sys_permission` VALUES (3, 'system:user:view');
INSERT INTO `sys_permission` VALUES (4, 'system:user:delete');
INSERT INTO `sys_permission` VALUES (5, 'system:admin:update');
INSERT INTO `sys_permission` VALUES (6, 'system:admin:insert');
INSERT INTO `sys_permission` VALUES (7, 'system:admin:view');
INSERT INTO `sys_permission` VALUES (8, 'system:*:*');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色编号',
`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
`desc` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'admin', '用户权限');
INSERT INTO `sys_role` VALUES (2, 'guest', '管理权限');
INSERT INTO `sys_role` VALUES (3, 'user', '产品权限');
INSERT INTO `sys_role` VALUES (4, 'user1', '订单权限');
-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`id` bigint(20) NOT NULL COMMENT '主键',
`role_id` bigint(16) NULL DEFAULT NULL COMMENT '角色id',
`permission_id` bigint(20) NULL DEFAULT NULL COMMENT '权限id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES (1, 1, 1);
INSERT INTO `sys_role_permission` VALUES (2, 1, 5);
INSERT INTO `sys_role_permission` VALUES (3, 1, 6);
INSERT INTO `sys_role_permission` VALUES (4, 1, 7);
INSERT INTO `sys_role_permission` VALUES (5, 1, 8);
INSERT INTO `sys_role_permission` VALUES (6, 2, 1);
INSERT INTO `sys_role_permission` VALUES (7, 2, 2);
INSERT INTO `sys_role_permission` VALUES (8, 2, 3);
INSERT INTO `sys_role_permission` VALUES (9, 2, 4);
INSERT INTO `sys_role_permission` VALUES (10, 1, 2);
INSERT INTO `sys_role_permission` VALUES (11, 1, 3);
INSERT INTO `sys_role_permission` VALUES (12, 1, 4);
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名称',
`password` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码',
`status` int(1) NOT NULL DEFAULT 1 COMMENT '用户状态(0:关闭、1:开启)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'zhangsan', '1e191d851b3b49a248f4ea62f6b06410', 0);
INSERT INTO `sys_user` VALUES (2, 'lisi', '1e191d851b3b49a248f4ea62f6b06410', 1);
INSERT INTO `sys_user` VALUES (3, 'wangwu', '1e191d851b3b49a248f4ea62f6b06410', 2);
INSERT INTO `sys_user` VALUES (4, 'zhaoliu', '1e191d851b3b49a248f4ea62f6b06410', 3);
INSERT INTO `sys_user` VALUES (5, 'xiaoqi', '1e191d851b3b49a248f4ea62f6b06410', 4);
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`uid` int(11) NOT NULL COMMENT '用户编号',
`rid` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`uid`, `rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);
INSERT INTO `sys_user_role` VALUES (1, 3);
INSERT INTO `sys_user_role` VALUES (2, 1);
INSERT INTO `sys_user_role` VALUES (2, 4);
INSERT INTO `sys_user_role` VALUES (3, 1);
INSERT INTO `sys_user_role` VALUES (3, 2);
INSERT INTO `sys_user_role` VALUES (3, 3);
INSERT INTO `sys_user_role` VALUES (3, 4);
SET FOREIGN_KEY_CHECKS = 1;
实体类 User
@Data
public class User implements Serializable {
private static final long serialVersionUID = 2951987436867980763L;
/**
* 主键
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 状态
*/
private Integer status;
}
Role
@Data
public class Role implements Serializable {
private static final long serialVersionUID = -8250617019976873581L;
/**
* 角色id
*/
private Long id;
/**
* 角色名称
*/
private String name;
/**
* 备注
*/
private String desc;
}
Permission
@Data
public class Permission implements Serializable {
private static final long serialVersionUID = -3561812452603746297L;
/**
* 权限id
*/
private Long id;
/**
* 权限名称
*/
private String name;
}
mapper包UserMapper
@Repository
public interface UserMapper {
@Select("select * from sys_user where username =#{username}")
@Results({
@Result(id = true,column = "id",property = "id"),
@Result(column = "username",property = "username"),
@Result(column = "password",property = "password"),
@Result(column = "id",property = "roleName",many = @Many(select = "com.lyf.shiro.user.dao.RoleMapper.getByUserId",fetchType = FetchType.EAGER)),
@Result(column = "id",property = "permissionCodes",many = @Many(select = "com.lyf.shiro.user.dao.PermissionMapper.getByUserId",fetchType = FetchType.EAGER))
})
LoginUserVo getByUsername(String username);
}
RolerMapper
@Repository
public interface RoleMapper {
@Select("select r.name from sys_user_role ur inner join sys_user u on u.id = ur.uid inner join sys_role r on r.id= ur.rid where uid = #{id}")
Set<String> getByUserId(Long id);
}
PermissionMapper
@Repository
public interface PermissionMapper {
@Select("SELECT p.`name` AS permission_name\n" +
"FROM `sys_user` u\n" +
"\n" +
"INNER JOIN sys_user_role ur ON u.id = ur.uid\n" +
"INNER JOIN sys_role r ON ur.rid = ur.uid\n" +
"INNER JOIN sys_role_permission rp ON r.id = rp.role_id\n" +
"INNER JOIN sys_permission p ON rp.id = p.id\n" +
"WHERE u.id = #{id}")
Set<String> getByUserId(Long id);
}
service包下LoginUserService
public interface LoginUserService {
/**
* 登录
* @param username
* @param password
* @return
*/
LoginUserVo login(String username,String password) throws Exception;
/**
* 根据用户名
* @param username
* @return
*/
LoginUserVo getByUser(String username);
}
LoginUserServiceImpl
@Service
public class LoginServiceImpl implements LoginUserService {
@Autowired
private UserMapper userMapper;
@Override
public LoginUserVo login(String username, String password) throws Exception {
LoginUserVo loginUserVo = getByUser(username);
//加密密码
String CodePassword = new Md5Hash(password, "123").toString();
if (loginUserVo == null) {
throw new AuthenticationException("用户不存在");
}
if (!CodePassword .equals(loginUserVo.getPassword())) {
throw new AuthenticationException("用户名或密码错误");
}
Subject subject = SecurityUtils.getSubject();
subject.logout();
if (!subject.isAuthenticated()) {
try {
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
subject.login(token);
} catch (AuthenticationException e) {
System.out.println(e.getMessage());
}
}
return loginUserVo;
}
@Override
public LoginUserVo getByUser(String username) {
return userMapper.getByUsername(username);
}
}
controller包
package com.lyf.shiro.user.controller;
import com.lyf.shiro.user.service.LoginUserService;
import com.lyf.shiro.user.vo.LoginUserTokenVo;
import com.lyf.shiro.user.vo.LoginUserVo;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Controller
public class loginController {
@Autowired
private LoginUserService loginUserService;
/**
* 跳转到登录页面
* @return
*/
@RequestMapping("/")
public String toLogin(){
return "login";
}
/**
* 登录控制器
* @param username 用户名
* @param password 密码
* @return
* @throws IOException
*/
@RequestMapping("/login")
@ResponseBody
public Map<String,LoginUserVo> login(String username, String password) throws Exception {
LoginUserVo login = loginUserService.login(username, password);
Map<String,LoginUserVo> map = new HashMap<>() ;
map.put("操作成功",login);
return map;
}
/**
* 登录成功跳转页面
* @return
*/
@RequestMapping("/success")
public String success(){
return "success";
}
/**
*无权限页面
* @return
*/
@RequestMapping("/noPermission")
public String noPermiss(){
return "noPermission";
}
/**
* user/** 下的所有路径
* @return
*/
@RequiresPermissions("system:user:add")
@RequestMapping("/user/add")
@ResponseBody
public String user(){
return "/user/add 添加路径";
}
/**
* 访问admin/** 下所有路径
* @return
*/
@RequiresPermissions("system:admin:add")
@RequestMapping("/admin/add")
@ResponseBody
public String admin(){
return "/admin/add 添加路径";
}
}
config包
package com.lyf.shiro.config;
import com.lyf.shiro.user.dao.UserMapper;
import com.lyf.shiro.user.vo.LoginUserVo;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.PasswordService;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Set;
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
/**
* 返回这个域的名称
* @return
*/
@Override
public String getName() {
super.setName("shiro");
return super.getName();
}
/**
* 表示是否开启这个域的验证,为true表示开启,为false表示不开启这个域的验证
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return true;
}
/**
* 授权
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取主要的身份信息这里获取到的是认证SimpleAuthenticationInfo(username,password,getName());传过来的用户名
//通过用户名去数据库查询角色权限信息,这个做一个简单的角色权限封装
String username = (String) principals.getPrimaryPrincipal();
LoginUserVo byUsername = userMapper.getByUsername(username);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(byUsername.getRoleName());
simpleAuthorizationInfo.setStringPermissions(byUsername.getPermissionCodes());
return simpleAuthorizationInfo;
}
/**
* 登录验证需要执行的方法,在主题调用subject.login(token),
* 具体的验证就会有SecurityManager调用到这个方法进行验证;
* @param token
* @return
* @throws AuthenticationException
*/
@Override
public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
String password = (String) usernamePasswordToken.getCredentials();
//返回验证通过的信息到授权的处理器,授权处理器根据传人的参数设置角色与权限
return new SimpleAuthenticationInfo(username,password, ByteSource.Util.bytes(token.getCredentials()),getName());
}
}
ShiroConfig
package com.lyf.shiro.config;
import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.ehcache.EhCache;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* 定义一个默认的安全管理器,这个管理器将管理所有的Shiro组件实例,
* @param realm 用于访问数据源的域
* @return
*/
@Bean
public SecurityManager securityManager(Realm realm){
//创建一个默认的实例放入到ioc容器当中
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
/**
* 定义一个数据源域,这个域Realm需要自己去实现
* @return
*/
@Bean
public MyRealm realm(){
MyRealm myRealm = new MyRealm();
//设置缓存管理器
myRealm.setCacheManager(new EhCacheManager());
//开启全局缓存
myRealm.setCachingEnabled(true);
//开启认证缓存
myRealm.setAuthenticationCachingEnabled(true);
//开启授权认证缓存
myRealm.setAuthorizationCachingEnabled(true);
return myRealm;
}
/**
* 定义一个过滤器,过滤所有的请求
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//自定义过滤器
shiroFilter.setLoginUrl("/");
shiroFilter.setSuccessUrl("/success");
shiroFilter.setUnauthorizedUrl("/noPermission");
Map<String,String> filterMap = new LinkedHashMap<>();
/**
/admin/** 表示一个请求名字的通配, 以admin开头的任意子路径下的所有请求
* anon: 表示该路径不需要进行认证,已访客身份访问
authc 表示该路径需要进行认证(登录),只有认证(登录)通过才能访问
** 表示任意子路径
* 表示任意的一个路径
? 表示 任意的一个字符
*/
filterMap.put("/login","anon");
// filterMap.put("admin/**","perms[system:admin:*]");
// filterMap.put("user/**","perms[system:user:add]");
//注释表示拦截所有路径的请求,如果换成anon,则放行所有的请求路径
filterMap.put("/**","authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
//开启注解
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
//开启注解
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
attributeSourceAdvisor.setSecurityManager(securityManager);
return attributeSourceAdvisor;
}
}
配置文件
# 应用名称
spring:
application:
name: shiro
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 链接池初始化大小
initial-size: 8
# 最大活跃数
max-active: 16
# 最小空闲数
min-idle: 1
# 最大等待时间
max-wait: 60000
mybatis:
type-aliases-package: com.lyf.shiro.user.domain.* // 换成自己实际的路径
configuration:
map-underscore-to-camel-case: true
启动类
@SpringBootApplication
@MapperScan("com.lyf.shiro.user.dao")//换上自己实际的路径
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class, args);
}
}
8.2 测试
账号: zhangsan
密码:123456