Shiro 入门笔记
视频地址:https://www.bilibili.com/video/BV1uz4y197Zm
感谢编程不良人的教程
1. 权限管理
权限管理包括用户 身份认证
和 授权
两部分,简称 认证授权
。对于需要访问控制的资源用户首先经过身份认证,认证通过后的用户,且具有该资源的访问权限才可以访问。
身份认证
就是判断一个用户是否是一个合法用户的过程。最常用的简单身份认证方式是通过核对用户输入的用户名和密码,看是否与系统中存储的该用户的用户名密码一致,来判断用户身份是否正确。
授权
即访问控制,控制已经登录的用户能访问那些资源。主体进行身份认证后需要分配权限才可以访问系统的资源,对于某些资源,没有权限是不能访问的。
2. Shiro 简介
apacheshiro(发音为“shee-roh”,日语中是“castle”)是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理,可用于保护任何应用程序,从命令行应用程序,移动应用程序到最大的web和企业应用程序。
- Apache shiro 是 java 的一个安全(权限)框架
- Shiro 可以非常容易地开发出足够好的应用环境,其不仅可以用在 JavaSE 环境,也可以在 JavaEE 环境
- Shiro 可以实现:认证、授权、加密、会话管理、与 Web 集成、缓存等
- 官网地址:https://shiro.apache.org/
功能简介
- Authentication:身份认证/登录,验证用户是否拥有相应的的身份。
- Authorization:授权,即权限验证,验证某个已经认证的用户,是否拥有某个权限;即判断用户是否能进行相关操作,如:验证某个用户是否拥有某个角色,或者细粒度地验证某个用户对某个资源是否具有某个权限。
- Session Manger:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是 JavaSE 环境,也可以是 Web 环境的。
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
- Web Support:Web 支持,可以非常容易集成到 Web 环境;
- Cacheing:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次都去查询,这样可以提高效率。
- Concurrency:Shiro 支持多线程应用的并发验证,如在一个线程中开启另一个线程,能把权限自动传播过去。
- Testing:提供测试支持。
- Run As:允许一个用户假装另一个用户(如果允许)的身份进行访问;
- Remember Me:记住我,这是非常常见的功能,即一次登录后,下次再访问的话,
不用再次登录。
Shiro 架构
-
从外部来看 Shiro,也就是应用程序角度的看,如何使用 Shiro 完成工作
-
从 Shiro 内部来看
-
Subject:任何可以与应用交互的 “用户”
-
SecurityManager:相当于 SpringMVC 中的 DispatcherServlet,是 Shiro 的心脏,所有的具体交互都通过 SecurityManager 进行控制,它管理着所有的 Subject,并且负责进行认证、授权、会话及缓存的管理
-
Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现,可以使用认证策略(Authentication Strategy),即什么情况下算是用户认证通过了
-
Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能
-
Realm:可以有一个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的,可以是 JDBC 实现,也可以是内存实现等等,由用户提供,所以在一般应用中都需要实现自己的 Realm
-
SessionManager:管理 Session 声明周期的组件,而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境
-
CacheManager:缓存控制器,来管理如用户、角色,权限等的缓存的,因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能
-
Cryptography:密码模块,Shiro 提高了一下常见的加密组件,用于如密码的加密/解密
3. Shiro 认证
身份认证,就是判断一个用户是否为合法用户的处理过程
关键对象
-
Subject:主体
访问系统的用户,主体可以是用户、程序等,进行认证的都成为主体
-
Principal:身份信息
是主体(subject)进行身份认证的标识,标识必须具有
唯一性
,如用户名、手机号、邮箱地址等。一个主体可以有多个身份,但必须有一个主身份(Primary Principal) -
credential:凭证信息
只有主体自己知道的安全信息,如密码、证书等。
认证流程
认证案例
案例仓库地址:https://gitee.com/Crater/hello-shiro.git
-
引入 Maven 依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency>
-
resources 目录下新建 shiro.ini ,就是 shiro 的配置文件(整合 SpringBoot 和数据库后不需要这个文件)
用来学习 shiro 的时候,书写系统中相关权限的数据
[users] crater=123456 ton=123 jerry=456
-
测试代码
// 1.创建安全管理器对象 DefaultSecurityManager securityManager = new DefaultSecurityManager(); // 2.给安全管理器设置 Realm // 因为信息保存在 ini 文件里,所以使用 IniRealm,认证时,去 shiro.ini 读取数据 securityManager.setRealm(new IniRealm("classpath:shiro.ini")); // 3.给全局安全工具类设置安全管理器 // SecurityUtils 全局安全工具类 SecurityUtils.setSecurityManager(securityManager); // 4.关键对象 Subject 主体 Subject subject = SecurityUtils.getSubject(); // 5.创建令牌 UsernamePasswordToken token = new UsernamePasswordToken("crater", "123456"); // 6.用户认证 System.out.println("认证前——认证状态 >> " + subject.isAuthenticated()); try { subject.login(token); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("认证失败 >> 用户名不存在~"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("认证失败 >> 密码错误~"); } System.out.println("认证后——认证状态 >> " + subject.isAuthenticated());
认证流程源码
-
根据认证源码,认证使用的是
SimpleAccountRealm
-
最终执行用户比较在
SimpleAccountRealm
中,在doGetAuthenticationInfo
方法中完成用户名校验。需要将用户信息存储在数据库中实现认证,可以模仿
SimpleAccountRealm
继承AuthorizingRealm
,重写doGetAuthenticationInfo
方法,自定义实现 Realm。 -
最终密码校验是在
AuthenticatingRealm
中的assertCredentialsMatch
方法中实现。
总结
AuthenticatingRealm
: 认证realmdoGetAuthenticationInfo
方法AuthorizingRealm
: 授权realmdoGetAuthorizationInfo
方法
自定义 Realm
-
自定义Realm
/** * 自定义 Realm,将认证/授权数据的来源,改为数据库 */ public class CustomRealm extends AuthorizingRealm { /** * 授权方法 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } /** * 认证方法 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 在 token中获取用户名 String principal = (String) token.getPrincipal(); System.out.println("token 中用户名 >> " + principal); // 在数据库中获取身份信息 String username = "crater"; String password = "123456"; if (username.equals(principal)) { // 认证信息 // 参数1:数据库中用户名 // 参数2:数据库中密码 // 参数3:当前 Realm 名字 return new SimpleAuthenticationInfo(principal, password, this.getName()); } // 返回null:用户名错误 return null; } }
MD5 和 Salt
MD5 一般用来加密、签名(校验和),算法不可逆,相同的文本无论执行多少次 MD5 加密,生成结果始终一致,一些 MD5 解密工具网站就是把常用的密码加密后,进行穷举。
MD5 生成结果始终是一个 16进制的 32位字符串。
在实际开发中使用 MD5,注册过程需要在业务层使用 MD5 对密码加密。
单独使用 MD5 还有潜在而风险,如被穷举破解,所以需要使用 Salt(盐)配合 MD5减小风险:
- 在注册时,对用户的密码加盐,即在密码的任意位置按照加盐规则,添加随机字符,如
123456
加盐后变成123456X*oq
,然后再使用 MD5 加密。 - 在登录时,根据对用户从前台传入的账号密码,再按照相同的加盐规则,对密码加盐,MD5 加密,再与数据库中注册信息对比,进而降低了风险。
MD5+Salt 实现
-
测试 Md5
// 使用Md5 Md5Hash md5Hash = new Md5Hash("123456"); System.out.println(md5Hash.toHex()); // e10adc3949ba59abbe56e057f20f883e // 使用Md5 + salt,默认加盐到后面 Md5Hash md5Hash1 = new Md5Hash("123456", "X0*7ps"); System.out.println(md5Hash1.toHex()); // e99a0dee78d3c1f71609cead42047675 // 使用Md5 + salt + hash散列,第三个参数为散列程度 Md5Hash md5Hash2 = new Md5Hash("123456", "X0*7ps", 1024); System.out.println(md5Hash2.toHex()); // 955224a95d4161ad8bd84f7ede979c02
-
在 Shiro 中使用
在认证方法模拟,注册时使用了加密
/** * 认证方法 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 获取用户名 String principal = (String) token.getPrincipal(); // 用户名、据用户名查询到的密码(Md5) String username = "crater"; String password01 = "e10adc3949ba59abbe56e057f20f883e"; // MD5 String password02 = "e99a0dee78d3c1f71609cead42047675"; // MD5 + salt String password03 = "955224a95d4161ad8bd84f7ede979c02"; // MD5 + salt + hash if (username.equals(principal)) { // 认证信息 return new SimpleAuthenticationInfo( principal, password03, ByteSource.Util.bytes("X0*7ps"), // MD5 + salt 需要指明注册时生成密码的随机盐 this.getName() ); } // 返回null:用户名错误 return null; }
给安全管理器设置 Realm 时,Realm使用hash凭证匹配器
// 2.给安全管理器设置 Realm CustomMd5Realm realm = new CustomMd5Realm(); /* * 设置Realm使用hash的凭证匹配器 */ HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // 凭证匹配器策略 credentialsMatcher.setHashAlgorithmName("md5"); // MD5 + salt + hash 模式需要设置散列数 credentialsMatcher.setHashIterations(1024); realm.setCredentialsMatcher(credentialsMatcher); securityManager.setRealm(realm);
4. Shiro 授权
授权
授权,即访问控制,控制谁能访问那些资源。主体进行身份认证后需要分配权限才可以访问系统的资源,对于某些资源没有权限是不能访问的
关键对象
授权可以简单理解为 who 对 what 进行 how 操作:
- who:主体(Subject),主体需要访问系统中的资源。
- what:资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括
资源类型
和资源实例
,比如商品信息为资源类型,类型为 t01的商品为资源实例,编号为 001 的 商品也属于资源实例。 - how:权限/许可(Permission),规定了主体对资源的操作许可, 权限离开资源没有意义 ,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为 001 用户的修改权限等,通过权限可知主体对哪些资源都有哪些许可。
授权流程
授权方式
-
基于角色的访问控制
-
RBAC 基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
if (subject.hasRole("admin")) { // 操作什么资源 }
-
-
基于资源的访问控制
-
RBAC 基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
if (subject.hasPermission("user:update:01")) { // 资源实例 // 对用户01进行修改 } if (subject.hasPermission("user:update:*")) { // 资源类型 // 对用户进行修改 }
-
权限字符串
权限字符串的规则是: 资源标识符 : 操作 : 资源实例标识符 ,意思是对哪个资源的哪个实例具有什么操作," : "是资源/操作/实例的分隔符,权限字符串也可以使用 * 通配符。
例子:
- 用户创建权限:user:create,或 user:create:*
- 用户修改实例 001 的权限:user:update:001
- 用户实例 001 的所有权限:user:*:001
Shiro 授权实现
-
编程式:
Subject subject = SecurityUtils.getSubject(); if (subject.hasRole("admin")) { // 有权限 } else { // 无权限 }
-
注解式
@RequiresRoles("admin") public void hello() { // 有权限 }
-
标签式
JSP/GSP 标签:在 JSP/GSP 页面通过相应的标签完成: <shiro:hasRole name="admin"> <!-- 有权限 --> </shiro:hasRole> 注意:Thymeleaf 中使用 shiro 需要额外集成
代码实例
-
改写自定义 Realm 中的 doGetAuthorizationInfo 方法
/** * 授权方法 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String principal = (String)principals.getPrimaryPrincipal(); System.out.println("授权身份信息 >> " + principal); // 根据身份信息(用户名),获取当前用户的角色信息,以及权限信息 // 假设 crater 拥有权限 admin、user SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 将数据库查询的【角色信息】赋予权限对象 authorizationInfo.addRole("admin"); authorizationInfo.addRole("user"); // 将数据库查询的【资源信息】赋予权限对象 authorizationInfo.addStringPermission("user:*:01"); authorizationInfo.addStringPermission("product:create"); return authorizationInfo; }
-
认证后测试授权
if (subject.isAuthenticated()) { System.out.println("===================== 基于角色 ======================"); // 基于角色权限控制 System.out.println("是否拥有admin权限 >> " +subject.hasRole("admin")); // 基于多角色权限控制 boolean allRoles = subject.hasAllRoles(Arrays.asList("admin", "user")); System.out.println("是否拥有admin、user权限 >> " + allRoles); // 是否拥有其中一个角色 boolean[] hasRoles = subject.hasRoles(Arrays.asList("admin", "user")); System.out.println("是否拥有admin、user其中一个权限 >> " + Arrays.toString(hasRoles)); System.out.println("===================== 基于资源 ======================"); // 基于资源权限控制 System.out.println("是否拥有user:*:01权限 >> " + subject.isPermitted("user:*:01")); // 基于多资源权限控制 boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create"); System.out.println("是否拥有user:*:01、product:create权限 >> " + permittedAll); // 是否拥有其中一个资源 boolean[] permitted = subject.isPermitted("user:*:01", "product:create:01"); System.out.println("是否拥有user:*:01、product:create其中一个权限 >> " + Arrays.toString(permitted)); }
-
输出
登录成功~ ===================== 基于角色 ====================== 授权身份信息 >> crater 是否拥有admin权限 >> true 授权身份信息 >> crater 授权身份信息 >> crater 是否拥有admin、user权限 >> true 授权身份信息 >> crater 授权身份信息 >> crater 是否拥有admin、user其中一个权限 >> [true, true] ===================== 基于资源 ====================== 授权身份信息 >> crater 是否拥有user:*:01权限 >> true 授权身份信息 >> crater 授权身份信息 >> crater 是否拥有user:*:01、product:create权限 >> true 授权身份信息 >> crater 授权身份信息 >> crater 是否拥有user:*:01、product:create其中一个权限 >> [true, true] Process finished with exit code 0
5. 整合 SpringBoot
整合思路
配置 Shiro 环境
-
搭建 SpringBoot,并引入 shiro 依赖
仓库地址:https://gitee.com/Crater/hello-shiro.git
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.5.3</version> </dependency>
访问 http://127.0.0.1:8080/shiro/index.jsp,可以看到主页
-
搭建 shiro 配置和自定义 Realm
@Configuration public class ShiroConfig { // 1.创建shiroFilter @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 给Filter设置安全管理器 shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); // 配置系统受限资源 Map<String, String> map = new HashMap<>(); map.put("/index.jsp", "authc"); // authc:请求这个资源需要认证和授权 shiroFilterFactoryBean.setFilterChainDefinitionMap(map); // 默认认证界面,就算是不设置,未授权的请求也会重定向到 login.jsp shiroFilterFactoryBean.setLoginUrl("/login.jsp"); return shiroFilterFactoryBean; } // 2.创建Web安全管理器 @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) { DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager(); // 给安全管理器设置 Realm defaultSecurityManager.setRealm(realm); return defaultSecurityManager; } // 3.创建自定义realm @Bean(name = "realm") public Realm getRealm() { return new CustomRealm(); } }
public class CustomRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { return null; } }
-
再次访问 http://127.0.0.1:8080/shiro/login.jsp,因为未经授权,重定向到 login.jsp 页面
常见过滤器
shiro 提供多个默认的过滤器,可以使用这些过滤器来配置控制指定url的权限:
前两个用的比较多。
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名访问 |
authc | FromAuthenticationFilter | 指定url需要from表单登录,默认会从请求中获取username、password , rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做,录不了就会跳转到loginUrl配置的路径.我们也可以用这个过滤器做默认的登录逻辑,但是-般都是我们自己在控制器写登录逻辑的,默认的登录逻辑,但是-般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 |
logout | LogoutFilter | 登出过滤器,配置指定url,就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionAuthenticationFilter | 需要指定权限才能访问 |
port | PortFilter | 需要指定端口才能访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构建一个权限字符,感觉这个意义不大,有兴趣自己看源码 |
roles | RolesAuthenticationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要https请求才能访问 |
user | UserFilter | 需要以登录或“记住我”用户才能访问 |
认证
表单认证退出
实现登录认证。过滤除登录之外所有请求,并重定向到登录页面。
-
登录页面
<body> <h1>登录页面</h1> <form action="${pageContext.request.contextPath}/user/login" method="post"> 账号:<input type="text" name="username"><br/> 密码:<input type="text" name="password"><br/> <input type="submit" value="登录"> </form> </body>
-
系统主页
<body> <h1>系统主页V1.0</h1> <a href="${pageContext.request.contextPath}/user/logout">退出用户</a> <ul> <li><a href="">用户管理</a></li> <li><a href="">商品管理</a></li> <li><a href="">订单管理</a></li> <li><a href="">物流管理</a></li> </ul> </body>
-
shiro 配置类
@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 给Filter设置安全管理器 shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); // 配置认证和授权规则 // 配置系统公共资源 // 配置系统受限资源 Map<String, String> map = new HashMap<>(); map.put("/user/login", "anon"); // anon:设置为公共资源 map.put("/**", "authc"); // authc:请求这个资源需要认证和授权,使用通配符过滤所有 shiroFilterFactoryBean.setFilterChainDefinitionMap(map); // 默认认证界面,就是不设置,未授权的请求也会重定向到 login.jsp shiroFilterFactoryBean.setLoginUrl("/login.jsp"); return shiroFilterFactoryBean; }
注册(连接数据库)
-
注册页面
<body> <h1>注册页面</h1> <form action="${pageContext.request.contextPath}/user/register" method="post"> 账号:<input type="text" name="username"><br/> 密码:<input type="text" name="password"><br/> <input type="submit" value="立即注册"> </form> </body>
-
新建数据库
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_user -- ---------------------------- DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` int(6) NOT NULL AUTO_INCREMENT, `username` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
-
引入依赖
<!--druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.22</version> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.2</version> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency>
-
修改配置文件
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8&serverTimezone=GMT%2b8&useSSL=false&failOverReadOnly=false spring.datasource.username=root spring.datasource.password=123456 mybatis.type-aliases-package=com.crater.entity mybatis.mapper-locations=classpath:/mapper/*.xml
-
随机盐工具类
/** * 生成salt */ public static String getSalt(int n){ char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()".toCharArray(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { char c = chars[new Random().nextInt(chars.length)]; sb.append(c); } return sb.toString(); }
-
编写业务层
@Override public void register(User user) { // 1.生成随机盐并保存 String salt = SaltUtils.getSalt(8); user.setSalt(salt); // 2.明文密码进行 md5 + salt + hash散列 Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024); user.setPassword(md5Hash.toHex()); userDao.save(user); }
-
编写 controller
/** * 注册 */ @RequestMapping("/register") public String register(User user) { try { userService.register(user); return "redirect:/login.jsp"; } catch (Exception e) { e.printStackTrace(); return "redirect:/register.jsp"; } }
-
测试注册,注册成功,跳转到登录页面,数据库也存储了用户信息
认证(连接数据库)
-
修改自定义 CustomRealm 中的认证方法
// 获取身份信息 String principal = (String) authenticationToken.getPrincipal(); // 获取service对象 UserService userService = SpringUtils.getBean("userService", UserService.class); User user = userService.findByUserName(principal); if (!ObjectUtils.isEmpty(user)) { return new SimpleAuthenticationInfo( user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName()); } return null;
-
修改 ShiroConfig 中 getRealm 的规则
// 3.创建自定义realm @Bean(name = "realm") public Realm getRealm() { CustomRealm customRealm = new CustomRealm(); // 修改凭证校验匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // 设置加密算法为MD5 credentialsMatcher.setHashAlgorithmName("MD5"); credentialsMatcher.setHashIterations(1024); customRealm.setCredentialsMatcher(credentialsMatcher); return customRealm; }
-
测试成功,实现了基于MD5 + salt 认证!
授权
角色授权
-
改写自定义 Realm 下的 doGetAuthorizationInfo 方法
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal(); System.out.println("调用授权验证 >> " + primaryPrincipal); if ("crater".equals(primaryPrincipal)) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRole("user"); return simpleAuthorizationInfo; } return null; }
-
改写 index.jsp 页面
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> ... ... <body> <h1>系统主页V1.0</h1> <a href="${pageContext.request.contextPath}/user/logout">退出用户</a> <ul> <shiro:hasAnyRoles name="user,admin"> <li><a href="">用户管理</a></li> </shiro:hasAnyRoles> <shiro:hasRole name="admin"> <li><a href="">商品管理</a></li> <li><a href="">订单管理</a></li> <li><a href="">物流管理</a></li> </shiro:hasRole> </ul> </body>
-
测试,只有拥有相应的角色,才能看到相应的资源,user 角色只能看到用户管理
权限授权
-
改写自定义 Realm 下的 doGetAuthorizationInfo 方法,赋予权限
if ("crater".equals(primaryPrincipal)) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRole("user"); simpleAuthorizationInfo.addStringPermission("user:find:*"); simpleAuthorizationInfo.addStringPermission("user:update:*"); return simpleAuthorizationInfo; }
-
改写 index.jsp 页面,CURD都需要不同的权限才可以看到
<shiro:hasAnyRoles name="user,admin"> <li><a href="">用户管理</a> <ul> <shiro:hasPermission name="user:add:*"> <li>添加</li> </shiro:hasPermission> <shiro:hasPermission name="user:delete:*"> <li>删除</li> </shiro:hasPermission> <shiro:hasPermission name="user:update:*"> <li>修改</li> </shiro:hasPermission> <shiro:hasPermission name="user:find:*"> <li>查询</li> </shiro:hasPermission> </ul> </li> </shiro:hasAnyRoles>
-
测试
-
在后端判断当前主体的权限,有两种方式
// 1. 获取主体,进行判断 Subject subject = SecurityUtils.getSubject(); boolean hasRole = subject.hasRole("admin"); boolean permitted = subject.isPermitted("user:find:*"); // 2. 注解的方式 @RequiresRoles("admin") @RequiresPermissions("user:find:*")
角色授权(连接数据库)
-
数据库设计:用户 >> 角色 >> 权限 >> 资源
搭建数据库并初始化
-- ---------------------------- -- Table structure for t_perms -- ---------------------------- DROP TABLE IF EXISTS `t_perms`; CREATE TABLE `t_perms` ( `id` int(6) NOT NULL AUTO_INCREMENT, `name` varchar(80) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for t_role -- ---------------------------- DROP TABLE IF EXISTS `t_role`; CREATE TABLE `t_role` ( `id` int(6) NOT NULL AUTO_INCREMENT, `name` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for t_role_perms -- ---------------------------- DROP TABLE IF EXISTS `t_role_perms`; CREATE TABLE `t_role_perms` ( `id` int(6) NOT NULL AUTO_INCREMENT, `roleid` int(6) NULL DEFAULT NULL, `permsid` int(6) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for t_user_role -- ---------------------------- DROP TABLE IF EXISTS `t_user_role`; CREATE TABLE `t_user_role` ( `id` int(6) NOT NULL AUTO_INCREMENT, `userid` int(6) NULL DEFAULT NULL, `roleid` int(6) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-
编写 t_role 和 t_perms 的数据库查询
public class Role { private Integer id; private String name; }
改写 User 实体类
public class User { private String id; private String username; private String password; private String salt; // 角色集合 private List<Role> roles; }
编写sql
<resultMap id="userMap" type="User"> <id column="uid" property="id"/> <result column="username" property="username"/> <collection property="roles" javaType="list" ofType="Role"> <id column="id" property="id"/> <result column="name" property="name"/> </collection> </resultMap> <select id="findRolesByUsername" parameterType="String" resultMap="userMap"> select u.id as uid,u.username,r.id,r.name from t_user u left join t_user_role ur on u.id = ur.userid left join t_role r on ur.roleid = r.id where u.username=#{username} </select>
-
改写授权过程
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 获取身份信息 String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal(); System.out.println("调用授权验证 >> " + primaryPrincipal); // 获取角色 UserService userService = SpringUtils.getBean("userService", UserService.class); List<Role> roles = userService.findRolesByUsername(primaryPrincipal).getRoles(); System.out.println(roles); if (!CollectionUtils.isEmpty(roles)) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); roles.forEach(role -> { simpleAuthorizationInfo.addRole(role.getName()); }); return simpleAuthorizationInfo; } return null; }
-
测试结果,具有 admin 角色可以看到所有资源,具有 user 角色只可以看到 用户管理
权限授权(连接数据库)
-
新建 Perms 实体
@Data @Accessors(chain = true) @AllArgsConstructor @NoArgsConstructor public class Perms { private Integer id; private String name; private String url; }
-
改写Role实体
@Data @Accessors(chain = true) @AllArgsConstructor @NoArgsConstructor public class Role { private Integer id; private String name; private List<Perms> perms; }
-
权限数据库初始化
数据库中一共3个权限:user:*:*,product:*:01,order:*.* admin角色拥有所有权限 user角色拥有user:*:* product角色拥有order:*.*
-
编写SQL
<select id="findPermsByRoleId" parameterType="java.lang.Integer" resultType="Perms"> select p.* from t_role r left join t_role_perms rp on r.id = rp.roleid left join t_perms p on rp.permsid = p.id where r.id = #{id} </select>
-
改写认证,添加对权限的操作
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 获取身份信息 String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal(); System.out.println("调用授权验证 >> " + primaryPrincipal); // 获取角色 UserService userService = SpringUtils.getBean("userService", UserService.class); List<Role> roles = userService.findRolesByUsername(primaryPrincipal).getRoles(); // 授权角色信息 if (!CollectionUtils.isEmpty(roles)) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); roles.forEach(role -> { simpleAuthorizationInfo.addRole(role.getName()); // 权限信息 List<Perms> perms = userService.findPermsByRoleId(role.getId()); if (!CollectionUtils.isEmpty(perms)) { perms.forEach(perm -> { simpleAuthorizationInfo.addStringPermission(perm.getName()); }); } }); return simpleAuthorizationInfo; } return null; }
-
测试,只有拥有admin角色的admin用户可以看到所有资源,其他用户只能看到部分资源
6. CacheManager
-
作用:用来减轻 DB 的访问压力,提高系统的查询效率
-
流程:
EhCache实现缓存
使用shiro中默认的EhCache实现缓存
-
引入依赖
<!-- EnCache --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.5.3</version> </dependency>
-
在自定义 Realm中开启缓存管理
// 3.创建自定义realm @Bean(name = "realm") public Realm getRealm() { CustomRealm customRealm = new CustomRealm(); // 修改凭证校验匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // 设置加密算法为MD5 credentialsMatcher.setHashAlgorithmName("MD5"); credentialsMatcher.setHashIterations(1024); customRealm.setCredentialsMatcher(credentialsMatcher); // 开启缓存管理 customRealm.setCacheManager(new EhCacheManager()); customRealm.setCachingEnabled(true); // 开启全局缓存 customRealm.setAuthenticationCachingEnabled(true); // 开启认证缓存 customRealm.setAuthenticationCacheName("authenticationCache"); customRealm.setAuthorizationCachingEnabled(true); // 开启授权缓存 customRealm.setAuthorizationCacheName("authorizationCache"); return customRealm; }
-
测试,登录后刷新,除了第一次请求数据库,之后不再请求
Redis实现缓存
-
引入依赖
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置 redis 连接
spring.redis.port=6379 spring.redis.host=localhost spring.redis.database=0
-
自定义shiro缓存管理器
/** * 自定义shiro缓存管理器 */ public class RedisCacheManager implements CacheManager { /** * @param s 缓存统一名称 * @return Redis缓存 */ @Override public <K, V> Cache<K, V> getCache(String s) throws CacheException { return new RedisCache<K, V>(); } }
-
自定义redis缓存的实现
public class RedisCache<K, V> implements Cache<K, V> { private String cacheName; public RedisCache() { } public RedisCache(String cacheName) { this.cacheName = cacheName; } @Override public V get(K k) throws CacheException { System.out.println("get key:" + k); RedisTemplate redisTemplate = getRedisTemplate(); redisTemplate.setStringSerializer(new StringRedisSerializer()); return (V) redisTemplate.opsForHash().get(this.cacheName, k.toString()); } @Override public V put(K k, V v) throws CacheException { System.out.println("put key:" + k); System.out.println("put val:" + v); RedisTemplate redisTemplate = getRedisTemplate(); redisTemplate.setStringSerializer(new StringRedisSerializer()); redisTemplate.opsForHash().put(this.cacheName, k.toString(), v); return null; } ... ... private RedisTemplate getRedisTemplate(){ RedisTemplate redisTemplate = SpringUtils.getBean("redisTemplate", RedisTemplate.class); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); return redisTemplate; } }
-
测试报错
由于shiro中提供的simpleByteSource实现没有实现序列化,所有在认证时出现错误信息,需要自动salt实现序列化
-
自动salt实现序列化
package com.crater.shiro.salt; import org.apache.shiro.codec.Base64; import org.apache.shiro.codec.CodecSupport; import org.apache.shiro.codec.Hex; import org.apache.shiro.util.ByteSource; import java.io.File; import java.io.InputStream; import java.io.Serializable; import java.util.Arrays; //自定义salt实现 实现序列化接口 public class MyByteSource implements ByteSource, Serializable { private byte[] bytes; private String cachedHex; private String cachedBase64; public MyByteSource() { } public MyByteSource(byte[] bytes) { this.bytes = bytes; } public MyByteSource(char[] chars) { this.bytes = CodecSupport.toBytes(chars); } public MyByteSource(String string) { this.bytes = CodecSupport.toBytes(string); } public MyByteSource(ByteSource source) { this.bytes = source.getBytes(); } public MyByteSource(File file) { this.bytes = (new com.crater.shiro.salt.MyByteSource.BytesHelper()).getBytes(file); } public MyByteSource(InputStream stream) { this.bytes = (new com.crater.shiro.salt.MyByteSource.BytesHelper()).getBytes(stream); } public static boolean isCompatible(Object o) { return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream; } public byte[] getBytes() { return this.bytes; } public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; } public String toHex() { if (this.cachedHex == null) { this.cachedHex = Hex.encodeToString(this.getBytes()); } return this.cachedHex; } public String toBase64() { if (this.cachedBase64 == null) { this.cachedBase64 = Base64.encodeToString(this.getBytes()); } return this.cachedBase64; } public String toString() { return this.toBase64(); } public int hashCode() { return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0; } public boolean equals(Object o) { if (o == this) { return true; } else if (o instanceof ByteSource) { ByteSource bs = (ByteSource) o; return Arrays.equals(this.getBytes(), bs.getBytes()); } else { return false; } } private static final class BytesHelper extends CodecSupport { private BytesHelper() { } public byte[] getBytes(File file) { return this.toBytes(file); } public byte[] getBytes(InputStream stream) { return this.toBytes(stream); } } }
-
在realm中使用自定义salt
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 获取身份信息 String principal = (String) authenticationToken.getPrincipal(); // 获取service对象 UserService userService = SpringUtils.getBean("userService", UserService.class); User user = userService.findByUserName(principal); if (!ObjectUtils.isEmpty(user)) { return new SimpleAuthenticationInfo( user.getUsername(), user.getPassword(), new MyByteSource(user.getSalt()), this.getName()); } return null; }
-
测试又报错,看第6步骤
org.apache.shiro.authc.SimpleAuthenticationInfo cannot be cast to org.apache.shiro.authz.AuthorizationInfo AuthenticationInfo(认证)被转换成AuthorizationInfo(授权)了 根本原因是先往redis里面放入认证的数据,后来授权的数据把认证的数据给覆盖了(key相同),取出来的时候就会报这个转换的错误。 解决方案: redisTemplate.opsForHash().get(this.cacheName, k.toString()); redisTemplate.opsForHash().put(this.cacheName, k.toString(), v);
-
测试,成功
-
其他的方法
@Override public V remove(K k) throws CacheException { // 退出时调用 System.out.println("=============remove============="); return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString()); } @Override public void clear() throws CacheException { System.out.println("=============clear=============="); getRedisTemplate().delete(this.cacheName); } @Override public int size() { return getRedisTemplate().opsForHash().size(this.cacheName).intValue(); } @Override public Set<K> keys() { return getRedisTemplate().opsForHash().keys(this.cacheName); } @Override public Collection<V> values() { return getRedisTemplate().opsForHash().values(this.cacheName); }