🌽Shiro学习
Shiro
课程地址:
【【编程不良人】2020最新版Shiro教程,整合SpringBoot项目实战教程】 https://www.bilibili.com/video/BV1uz4y197Zm/?p=6&share_source=copy_web&vd_source=045f1e1c5f0609a50870e499d44940af
GitHub:
https://github.com/zhangzhixi0305/springboot-study-shiro
Gitee:
https://gitee.com/zhang-zhixi/springboot-study-shiro
一、Shiro
1.1、权限管理
什么是权限管理
权限管理包括用户身份认证和授权两部分,简称认证授权。对于 需要访问控制的资源用户首先经过身份认证,认证通过后用户 具有该资源的访问权限方可访问
什么是身份认证:Authenticator
身份认证,就是判断一个用户是否为合法用户的处理过程。 最常用的简单身份认证方式是系统通过核对用户输入的用 户名和口令,看其是否与系统中存储的该用户的用户名和 口令一致,来判断用户身份是否正确。对于采用指纹等 系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
什么是授权:Authorizer
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认 证后需要分配权限方可访问系统的资源,对于某些资源没有 权限是无法访问的
1.2、Shiro核心架构
Subject
Subject即主体,外部应用与subject进行交互,subject记录了当前 操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏 览器请求的用户,也可能是一个运行的程序。 Subject在shiro中 是一个接口,接口中定义了很多认证授相关的方法,外部程序通 过subject进行认证授,而subject是通过SecurityManager安全 管理器进行认证授权
SecurityManager
SecurityManager即安全管理器,对全部的subject进行安全管理, 它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等. 实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。 SecurityManager是一个接口,继承了Authenticator, Authorizer,SessionManager这三个接口。
Authenticator
Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口. shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
Authorizer
Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
Realm
Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据. 比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
SessionManager
sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session. 所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
SessionDAO
SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库
CacheManager
CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能
Cryptography
Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能
1.3、Shiro中的认证
1、创建maven项目,引入依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.11.0</version> </dependency>
2、创建shiro配置文件并加入如下配置:shiro.ini
[users] xiaochen=123 zhangsan=456
3、认证代码
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.subject.Subject; public class TestAuthenticator { public static void main(String[] args) { //1.创建安全管理器对象 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //2.给安全管理器设置realm securityManager.setRealm( new IniRealm( "classpath:shiro.ini" )); //3.SecurityUtils 给全局安全工具类设置安全管理器 SecurityUtils.setSecurityManager(securityManager); //4.关键对象 subject 主体 Subject subject = SecurityUtils.getSubject(); //5.创建令牌 UsernamePasswordToken token = new UsernamePasswordToken( "xiaochen1" , "123" ); try { System. out .println( "认证状态: " + subject.isAuthenticated()); //用户认证 subject.login(token); System. out .println( "认证状态: " + subject.isAuthenticated()); } catch (UnknownAccountException e){ e.printStackTrace(); System. out .println( "认证失败: 用户名不存在~" ); } catch (IncorrectCredentialsException e){ e.printStackTrace(); System. out .println( "认证失败: 密码错误~" ); } } }
4、异常
DisabledAccountException(帐号被禁用) LockedAccountException(帐号被锁定) ExcessiveAttemptsException(登录失败次数过多) ExpiredCredentialsException(凭证过期)等
1.3.1、自定义Realm
上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm。
Shiro提供的Realm
自定义Realm实现AuthorizingRealm:源码
public class SimpleAccountRealm extends AuthorizingRealm { //.......省略 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; SimpleAccount account = getUser(upToken.getUsername()); if (account != null ) { if (account.isLocked()) { throw new LockedAccountException( "Account [" + account + "] is locked." ); } if (account.isCredentialsExpired()) { String msg = "The credentials for account [" + account + "] are expired" ; throw new ExpiredCredentialsException(msg); } } return account; } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = getUsername(principals); USERS_LOCK.readLock(). lock (); try { return this .users. get (username); } finally { USERS_LOCK.readLock().unlock(); } } }
自定义Realm
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; /** * @ClassName ConsumerRealm * @Author zhangzhixi * @Description * @Date 2023-03-12 22:17 * @Version 1.0 */ public class ConsumerRealm extends AuthorizingRealm { // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null ; } /** * 认证 * * @param token 用户名密码token * @return 认证实例 * @throws AuthenticationException 身份验证异常 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 通过token获取用户名 String userName = token.getPrincipal().toString(); // 这里模拟从数据库中验证用户,之后可能从JDBC、Mybatis取 if ( "xiaochen" . equals (userName)) { // 参数1:返回数据库中正确的用户名 参数2:返回数据库中正确密码 参数3:提供当前realm的名字 this.getName(); return new SimpleAuthenticationInfo(userName, "1234" , this .getName()); } return null ; } }
测试
public class TestConsumerRealm { public static void main(String[] args) { //创建securityManager DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); //设置自定义realm defaultSecurityManager.setRealm( new ConsumerRealm()); //将安全工具类设置安全工具类 SecurityUtils.setSecurityManager(defaultSecurityManager); //通过安全工具类获取subject Subject subject = SecurityUtils.getSubject(); //创建token UsernamePasswordToken token = new UsernamePasswordToken( "xiaochen" , "123" ); //执行登录 try { subject.login(token); } catch (UnknownAccountException unknownAccountException) { unknownAccountException.printStackTrace(); System. out .println( "用户名验证错误" ); } catch (IncorrectCredentialsException incorrectCredentialsException) { incorrectCredentialsException.printStackTrace(); System. out .println( "密码验证错误" ); } } }
1.3.2、使用MD5+盐(Salt),实现认证
1、创建用户名和密码时候使用的加盐规则
import org.apache.shiro.crypto.hash.Md5Hash; /** * @ClassName ShiroMd5Utils * @Author zhangzhixi * @Description * @Date 2023-03-12 23:13 * @Version 1.0 */ public class ShiroMd5Utils { public static void main(String[] args) { // 使用用户名做盐值,为了让盐是动态的 String userName = "xiaochen" ; //使用md5 Md5Hash md5Hash = new Md5Hash( "123" ); System. out .println(md5Hash.toHex()); //使用MD5 + salt处理 Md5Hash md5Hash1 = new Md5Hash( "123" , userName); System. out .println(md5Hash1.toHex()); //使用md5 + salt + hash散列 Md5Hash md5Hash2 = new Md5Hash( "123" , userName, 1024); System. out .println(md5Hash2.toHex()); } }
2、设置自定义:MD5+盐,的认证代码
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; /** * 使用自定义realm 加入md5 + salt +hash */ public class CustomerMd5Realm extends AuthorizingRealm { //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null ; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取身份信息 String principal = token.getPrincipal().toString(); //根据用户名查询数据库 if ( "xiaochen" . equals (principal)) { // 参数1: 数据库用户名 // 参数2:数据库md5+salt之后的密码 // 参数3:注册时的随机盐,本例子中使用动态的,用户名作为随机盐 // 参数4:realm的名字 return new SimpleAuthenticationInfo( principal, "847b2f7a3705be61f6738b48bed195f8" , ByteSource.Util.bytes(principal), this .getName()); } return null ; } }
3、测试
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject; public class TestCustomerMd5RealmAuthenicator { public static void main(String[] args) { // 1、创建安全管理器 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); // 2、注入realm CustomerMd5Realm realm = new CustomerMd5Realm(); // 3、设置realm使用hash凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // 使用算法 credentialsMatcher.setHashAlgorithmName( "md5" ); // 散列次数 credentialsMatcher.setHashIterations(1024); // 4、设置凭据匹配器 realm.setCredentialsMatcher(credentialsMatcher); defaultSecurityManager.setRealm(realm); // 5、将安全管理器注入安全工具 SecurityUtils.setSecurityManager(defaultSecurityManager); // 6、通过安全工具类获取subject Subject subject = SecurityUtils.getSubject(); // 7、认证 UsernamePasswordToken token = new UsernamePasswordToken( "xiaochen" , "123" ); try { subject.login(token); System. out .println( "登录成功" ); } catch (UnknownAccountException e) { e.printStackTrace(); System. out .println( "用户名错误" ); } catch (IncorrectCredentialsException e){ e.printStackTrace(); System. out .println( "密码错误" ); } } }
1.4、Shiro中的授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
1.4.1、关键对象
Who,即主体(Subject),主体需要访问系统中的资源。 What,即资源(Resource),如系统菜单、页面、按钮、类方法、 系统商品信息等。资源包括资源类型和资源实例,比如商品信息 为资源类型,类型为t01的商品为资源实例,编号为001的商品信息 也属于资源实例。 How,权限/许可(Permission),规定了主体对资源的操作许可, 权限离开资源没有意义,如用户查询权限、用户添加权限、某个 类方法的调用权限、编号为001用户的修改权限等,通过权限可 知主体对哪些资源都有哪些操作许可。
1.4.2、授权流程
1.4.3、授权方式
基于角色的访问控制RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
if (subject.hasRole( "admin" )){ //操作什么资源 }
基于资源的访问控制RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
if (subject.isPermission( "user:update:01" )){ //资源实例 //对01用户进行修改 } if (subject.isPermission( "user:update:*" )){ //资源类型 //对01用户进行修改 }
1.4.4、权限字符串
权限字符串的规则是:
资源标识符:操作:资源实例标识符
意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。
例子: 用户创建权限:user:create,或user:create:* 用户修改实例001的权限:user:update:001 用户实例001的所有权限:user:*:001
1.4.5、自定义Realm重写授权
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import java.util.Arrays; /** * 使用自定义realm 加入md5 + salt +hash */ public class CustomerMd5Realm extends AuthorizingRealm { /** * 授权 * @param principals subject实体 * @return 授权对象 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 1、获取当前身份信息 String userName = principals.getPrimaryPrincipal().toString(); // 2、获取权限对象 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 3、添加角色(从数据库中获取) authorizationInfo.addRoles(Arrays.asList("admin", "user")); // 4、添加权限:userName这个用户在user模块具有所有权限 authorizationInfo.addStringPermissions(Arrays.asList("user:*:" + userName)); return authorizationInfo; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取身份信息 String principal = token.getPrincipal().toString(); //根据用户名查询数据库 if ("xiaochen".equals(principal)) { // 参数1: 数据库用户名 // 参数2:数据库md5+salt之后的密码 // 参数3:注册时的随机盐,本例子中使用动态的,用户名作为随机盐 // 参数4:realm的名字 return new SimpleAuthenticationInfo( principal, "847b2f7a3705be61f6738b48bed195f8", ByteSource.Util.bytes(principal), this.getName()); } return null; } }
1.4.6、测试授权
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject; import java.util.Arrays; public class TestCustomerMd5RealmAuthenicator { public static void main(String[] args) { // 1、创建安全管理器 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); // 2、注入realm CustomerMd5Realm realm = new CustomerMd5Realm(); // 3、设置realm使用hash凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // 使用算法 credentialsMatcher.setHashAlgorithmName("md5"); // 散列次数 credentialsMatcher.setHashIterations(1024); // 4、设置凭据匹配器 realm.setCredentialsMatcher(credentialsMatcher); defaultSecurityManager.setRealm(realm); // 5、将安全管理器注入安全工具 SecurityUtils.setSecurityManager(defaultSecurityManager); // 6、通过安全工具类获取subject Subject subject = SecurityUtils.getSubject(); // 7、认证 UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123"); try { subject.login(token); System.out.println("登录成功"); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("密码错误"); } // 判断是否已经认证 if (subject.isAuthenticated()) { System.out.println("************基于角色的控制************"); // 基于角色的权限控制,是否有admin角色 System.out.println(subject.hasRole("admin")); // 基于角色的权限控制,是否有其中的一种角色信息 System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("admin", "super")))); // 基于角色的权限控制,是否同时具有提供的所有权限 System.out.println(subject.hasAllRoles(Arrays.asList("admin", "super"))); try { System.out.println("************基于权限的控制************"); // 资源标识符:操作:资源类型,比如当前用户是否对user模块具有所有权限就可以这样写(user:*:xiaochen) System.out.println(subject.isPermitted("user:*:xiaochen")); // 分别具有哪些权限 System.out.println(Arrays.toString(subject.isPermitted("user:*:xiaochen", "user:update:xiaochen"))); // 同时具有哪些权限 System.out.println(subject.isPermittedAll("user:*:xiaochen", "commodity:update:xiaochen")); } catch (AuthorizationException e) { e.printStackTrace(); System.out.println("未经授权,无法访问"); } } } }
1.5、常见过滤器
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名访问 |
authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取username 、password ,rememberMe 等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 |
logout | LogoutFilter | 登出过滤器,配置指定url就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
port | PortFilter | 需要指定端口才能访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要https请求才能访问 |
user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
二、SpringBoot整合Shiro(JSP)
2.0、数据库表设计
在使用Apache Shiro框架时,可以设计以下几张表来处理用户权限相关的信息:
- 用户表(user)
- 角色表(role)
- 权限表(permission)
- 用户角色关联表(user_role)
- 角色权限关联表(role_permission)
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- 用户表 -- ---------------------------- DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` int(6) NOT NULL AUTO_INCREMENT, `username` varchar(40) DEFAULT NULL, `password` varchar(40) DEFAULT NULL, `salt` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -- ---------------------------- -- 角色表 -- ---------------------------- DROP TABLE IF EXISTS `t_role`; CREATE TABLE `t_role` ( `id` int(6) NOT NULL AUTO_INCREMENT, `name` varchar(60) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- 权限表 -- ---------------------------- DROP TABLE IF EXISTS `t_pers`; CREATE TABLE `t_pers` ( `id` int(6) NOT NULL AUTO_INCREMENT, `name` varchar(80) DEFAULT NULL, `url` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- 用户角色关联表 -- ---------------------------- DROP TABLE IF EXISTS `t_user_role`; CREATE TABLE `t_user_role` ( `id` int(6) NOT NULL, `userid` int(6) DEFAULT NULL, `roleid` int(6) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- 角色权限关联表 -- ---------------------------- DROP TABLE IF EXISTS `t_role_perms`; CREATE TABLE `t_role_perms` ( `id` int(6) NOT NULL, `roleid` int(6) DEFAULT NULL, `permsid` int(6) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS = 1;
-- 添加用户信息 INSERT INTO `shiro`.`t_user` (`id`, `username`, `password`, `salt`) VALUES (1, 'xiaochen', 'b81f9fd16871f518865c680de016ff5c', '%ut0ZmSH'); INSERT INTO `shiro`.`t_user` (`id`, `username`, `password`, `salt`) VALUES (2, 'zhangsan', 'a7c5d959a1a8ee1b983cd991bb32ad8e', '@MoM7X5G'); -- 添加角色信息 INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES (1, 'admin'); INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES (2, 'user'); INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES (3, 'product'); -- 添加用户角色信息 INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES (1, 1, 1); INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES (2, 2, 2); INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES (3, 2, 3);
2.1、环境配置
注意:
启动项目需要在IDEA的配置上加上工作目录:$MODULE_WORKING_DIR$
2.1.1、引入依赖
<dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <!--jsp依赖--> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <dependency> <groupId>javax.servlet.jsp.jstl</groupId> <artifactId>jstl-api</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2.1.2、配置项目
server.servlet.context-path=/ spring.mvc.view.prefix=/ spring.mvc.view.suffix=.jsp
2.1.3、编写jsp页面
├─resources │ │ application.properties │ │ rebel.xml │ │ │ ├─ static │ └─templates └─webapp index.jsp -- 受限资源 login.jsp -- 公共资源
2.1.4、自定义Realm
public class CustomerRealm extends AuthorizingRealm { //处理授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null ; } //处理认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { return null ; } }
2.1.5、配置Shiro
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.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * @ClassName ShiroConfig * @Author zhangzhixi * @Description * @Date 2023-03-15 16:35 * @Version 1.0 */ @Configuration public class ShiroConfig { /** * shiro过滤器工厂 * @param securityManager shiro安全管理器 * @return 过滤器 */ @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier( "securityManager" ) SecurityManager securityManager) { // 1、创建shiro的filter ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 2、注入安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); Map<string, string=""> authMap = new HashMap<>(); // 配置公共资源 // 配置受限资源 authMap.put( "/index.jsp" , "authc" ); // 如果是受限资源,默认会重定向login.jsp //shiroFilterFactoryBean.setLoginUrl("/login.jsp"); shiroFilterFactoryBean.setFilterChainDefinitionMap(authMap); return shiroFilterFactoryBean; } /** * shiro的安全管理器 * @param realm realm * @return shiro的安全管理器 */ @Bean( "securityManager" ) public DefaultWebSecurityManager getSecurityManager(@Qualifier( "realm" ) Realm realm) { DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); defaultWebSecurityManager.setRealm(realm); return defaultWebSecurityManager; } /** * 创建自定义realm * @return 自定义Realm */ @Bean( "realm" ) public Realm getRealm() { return new CustomerRealm(); } }</string,>
2.1.6、测试
2.2、认证与退出登录(不操作DB)
登录表单:login.jsp
<h1>登录页面</h1> <form action="${pageContext.request.contextPath}/user/login" method="post"> <label> 用户名: <input type="text" name="username"> </label> <br/> <label> 密码 : <input type="text" name="password"> </label> <br> <input type="submit" value="登录"> </form>
受限页面:index.jsp
<h2>系统主页v1.0(受限页面)</h2> <label> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> </label> <ul> <li>用户管理</li> <li>商品管理</li> <li>订单管理</li> <li>物流管理</li> </ul> </body> </html>
配置拦截路径:ShiroConfig
/** * shiro过滤器工厂 * @param securityManager shiro安全管理器 * @return 过滤器 */ @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) { // 1、创建shiro的filter ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 2、注入安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> authMap = new HashMap<>(); // 配置公共资源 authMap.put("/login.jsp", "anon"); // 配置受限资源 authMap.put("/index.jsp", "authc"); authMap.put("/", "authc"); // 如果是受限资源,默认会重定向login.jsp //shiroFilterFactoryBean.setLoginUrl("/login.jsp"); shiroFilterFactoryBean.setFilterChainDefinitionMap(authMap); return shiroFilterFactoryBean; }
自定义Realm处理认证:ConsumerRealm
public class CustomerRealm extends AuthorizingRealm { //处理授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } //处理认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("=========================="); String principal = (String) token.getPrincipal(); // 后序这里的用户名密码,从数据库中查 if ("xiaochen".equals(principal)) { return new SimpleAuthenticationInfo(principal, "123", this.getName()); } return null; } }
web请求:UserController
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("user") public class UserController { /** * 用来处理身份认证 * * @param username 用户名 * @param password 密码 * @return 登录成功返回页面 */ @RequestMapping(value = "login",method = RequestMethod.POST) public String login(String username, String password) { //获取主体对象 Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(username, password)); return "redirect:/index.jsp"; } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误!"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("密码错误!"); } return "redirect:/login.jsp"; } /** * 退出系统 */ @GetMapping("/logout") public String logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); return "redirect:/login.jsp"; } }
2.3、认证(MD5+Sale)(连接数据库,操作Mybatis)
1、注册页面
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <h1>注册页面</h1> <form action="${pageContext.request.contextPath}/user/register" method="post"> <!--指定实际发送的请求方式--> <input type="hidden" name="_method" value="PUT"> <label> 用户名: <input type="text" name="username"> </label> <br/> <label> 密码 : <input type="text" name="password"> </label> <br> <input type="submit" value="注册"> </form> </body> </html>
2、登录页面
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <h1>登录页面</h1> <form action="${pageContext.request.contextPath}/user/login" method="post"> <label> 用户名: <input type="text" name="username"> </label> <br/> <label> 密码 : <input type="text" name="password"> </label> <br> <input type="submit" value="登录"> <a href="/register.jsp">没有账号?点我注册</a> </form> </body> </html>
3、Controller
@Autowired private UserService userService; /** * 用来处理身份认证 * * @return 登录成功返回页面 */ @RequestMapping(value = "login", method = RequestMethod.POST) public String login(User user) { //获取主体对象 Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(user.getUsername(), user.getPassword())); return "redirect:/index.jsp"; } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误!"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("密码错误!"); } return "redirect:/login.jsp"; } /** * 退出系统 */ @GetMapping("/logout") public String logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); return "redirect:/login.jsp"; } /** * 用户注册 * @param user 用户实体 * @return 注册成功返回登录页面,注册失败返回注册页面 */ @PutMapping("/register") public String saveUser(User user) { int count = userService.saveUser(user); if (count == 1) { return "redirect:/login.jsp"; } return "redirect:/register.jsp"; } @RequestMapping("save") public String save() { //获取主体对象 Subject subject = SecurityUtils.getSubject(); // 基于角色权限控制 if (subject.hasRole("admin")) { System.out.println("保存订单!"); } else { System.out.println("无权访问!"); } //基于权限字符串判断是否有权限访问 subject.isPermitted("admin", "user"); return "redirect:/index.jsp"; }
4、mapper
<insert id="saveUser" parameterType="com.zhixi.pojo.User" useGeneratedKeys="true"> insert into shiro.t_user(username, password, salt) values (#{username}, #{password}, #{salt}); </insert>
5、service层,对密码字段进行md5+Sale+Hash
@Autowired private UserMapper userMapper; @Override public int saveUser(User user) { //处理业务调用dao //1.生成随机盐 String salt = SaltUtils.getSalt(8); //2.将随机盐保存到数据 user.setSalt(salt); //3.明文密码进行md5 + salt + hash散列 Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024); user.setPassword(md5Hash.toHex()); return userMapper.saveUser(user); }
6、自定义Realm处理登录
/** * 处理认证 * @param token 用户登录信息 * @return 身份验证信息 * @throws AuthenticationException 异常 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 获取当前登录的用户名 String principal = token.getPrincipal().toString(); // 查询数据库 User user = userService.selectUserByName(principal); return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName()); }
7、Shiro配置:配置请求过滤以及自定义Realm使用到的加密算法以及散列次数
import org.apache.shiro.authc.credential.HashedCredentialsMatcher; 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.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * @ClassName ShiroConfig * @Author zhangzhixi * @Description * @Date 2023-03-15 16:35 * @Version 1.0 */ @Configuration public class ShiroConfig { /** * shiro过滤器工厂 * @param securityManager shiro安全管理器 * @return 过滤器 */ @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) { // 1、创建shiro的filter ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 2、注入安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> authMap = new HashMap<>(); // 配置公共资源 authMap.put("/login.jsp", "anon"); authMap.put("/user/login", "anon"); authMap.put("/register.jsp", "anon"); authMap.put("/user/register", "anon"); // 配置受限资源 authMap.put("/**", "authc"); authMap.put("/index.jsp", "authc"); authMap.put("/", "authc"); // 如果是受限资源,默认会重定向login.jsp //shiroFilterFactoryBean.setLoginUrl("/login.jsp"); shiroFilterFactoryBean.setFilterChainDefinitionMap(authMap); return shiroFilterFactoryBean; } /** * shiro的安全管理器 * @param realm realm * @return shiro的安全管理器 */ @Bean("securityManager") public DefaultWebSecurityManager getSecurityManager(@Qualifier("realm") Realm realm) { DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); defaultWebSecurityManager.setRealm(realm); return defaultWebSecurityManager; } /** * 创建自定义realm * @return 自定义Realm */ @Bean("realm") public Realm getRealm(){ CustomerRealm customerRealm = new CustomerRealm(); //设置hashed凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //设置md5加密 credentialsMatcher.setHashAlgorithmName("md5"); //设置散列次数 credentialsMatcher.setHashIterations(1024); customerRealm.setCredentialsMatcher(credentialsMatcher); return customerRealm; } }
8、测试
添加用户:
ShiroConfig->register.jsp-->Controller->入库
用户登录:
ShiroConfig-->login.jsp-->Controller-->自定义Realm处理认证
2.4、授权(不操作DB)
2.4.1、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需要额外集成!
2.4.2、实现授权-自定义realm
/** * 处理授权 * @param principals 权限数据 * @return 返回的授权信息 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 获取登录用户名 String userName = principals.getPrimaryPrincipal().toString(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); if ("admin".equals(userName)) { // 添加角色 simpleAuthorizationInfo.addRoles(Arrays.asList("admin", "user")); // 添加资源 simpleAuthorizationInfo.addStringPermissions(Arrays.asList("user:*:*", "admin:*:*")); } return simpleAuthorizationInfo; }
2.4.3、实现授权-页面授权方式
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <h2>系统主页v1.0(受限页面)</h2> <label> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> </label> <shiro:hasAnyRoles name="user,admin"> <li><a href="">用户管理(满足user/admin权限)</a> <ul> <shiro:hasPermission name="user:add:*"> <li><a href="">添加</a></li> </shiro:hasPermission> <shiro:hasPermission name="user:delete:*"> <li><a href="">删除</a></li> </shiro:hasPermission> <shiro:hasPermission name="user:update:*"> <li><a href="">修改</a></li> </shiro:hasPermission> <shiro:hasPermission name="user:find:*"> <li><a href="">查询</a></li> </shiro:hasPermission> </ul> </li> </shiro:hasAnyRoles> <hr> <shiro:hasRole name="admin"> <li><a href="">用户管理(满足admin权限)</a> <ul> <li><a href="">商品管理</a></li> <li><a href="">订单管理</a></li> <li><a href="">物流管理</a></li> </ul> </shiro:hasRole> </body> </html>
登录后即可看到页面上面展示的列表数据。
2.4.4、实现授权-代码授权方式
@RequestMapping("save") public String save(){ //获取主体对象 Subject subject = SecurityUtils.getSubject(); // 基于角色权限控制 if (subject.hasRole("admin")) { System.out.println("保存订单!"); }else{ System.out.println("无权访问!"); } //基于权限字符串判断是否有权限访问 subject.isPermitted("admin", "user"); return "redirect:/index.jsp"; }
2.4.5、实现授权-注解授权方式
@RequiresRoles 用来基于角色进行授权
@RequiresPermissions 用来基于权限进行授权
// 用来判断角色:具有 admin/user @RequiresRoles(value = {"admin", "user"}, logical = Logical.OR) // 用来判断权限字符串 @RequiresPermissions("user:update:01") @RequestMapping("saveToPermissions") public String saveToPermissions() { return "redirect:/index.jsp"; }
2.5、授权-基于角色(连接数据库,操作Mybatis)
1、数据库表设计
2、实体
@Data public class User { private Integer id; private String username; private String password; private String salt; /** * 用户角色信息 */ private List<Role> roles; } @Data public class Role { /** * 主键 */ private Integer id; /** * 角色名称 */ private String name; } @Data public class Perms { private Integer id; /** * 权限字符串 */ private String name; /** * 权限url */ private String url; }
3、根据用户名查询角色信息
/** * 根据用户名查询角色信息 * @param username 用户名 * @return 用户角色集合 */ List<User> findRolesByUsernameRoles(String username);
4、mapper文件
<resultMap id="userRoleMap" type="com.zhixi.pojo.User"> <id column="id" property="id"/> <result column="username" property="username"/> <collection property="roles" javaType="list" ofType="com.zhixi.pojo.Role"> <result column="roleid" property="id"/> <result column="rname" property="name"/> </collection> </resultMap> <select id="findRolesByUsernameRoles" resultMap="userRoleMap"> SELECT t.id, t.username, ur.roleid, r.`name` rname FROM shiro.t_user t LEFT JOIN shiro.t_user_role ur ON t.id = ur.userid LEFT JOIN shiro.t_role r ON r.id = ur.roleid WHERE t.username = #{username} </select>
5、修改自定义Realm中自定义授权代码
import com.zhixi.pojo.User; import com.zhixi.sevice.UserService; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; import java.util.Optional; @Component public class CustomerRealm extends AuthorizingRealm { @Autowired UserService userService; /** * 处理授权 * * @param principals 权限数据 * @return 返回的授权信息 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 获取登录用户名(从认证那里传过来的) User userLogin = (User)principals.getPrimaryPrincipal(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 查询登录用户权限集合 List<User> userList = userService.findRolesByUsernameRoles(userLogin.getUsername()); Optional.ofNullable(userList).ifPresent(users -> users.forEach(user -> user.getRoles().forEach(role -> simpleAuthorizationInfo.addRole(role.getName())))); // 添加角色权限 System.out.println(userList); return simpleAuthorizationInfo; } //处理认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("=========================="); // 获取当前登录的用户名 String principal = token.getPrincipal().toString(); // 查询数据库 User user = userService.selectUserByName(principal); return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName()); } }
6、受限页面:index.jsp
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <h2>系统主页v1.0(受限页面)</h2> <label> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> </label> <%--获取当前登录的用户名--%> <shiro:authenticated> <%--填写的是放在认证,SimpleAuthenticationInfo中的user数据--%> <h3> 当前登录用户是:<shiro:principal property="username"/> </h3> </shiro:authenticated> <shiro:hasAnyRoles name="user,admin"> <li><a href="">用户管理(满足user/admin权限)</a> <ul> <shiro:hasPermission name="user:add:*"> <li><a href="">添加</a></li> </shiro:hasPermission> <shiro:hasPermission name="user:delete:*"> <li><a href="">删除</a></li> </shiro:hasPermission> <shiro:hasPermission name="user:update:*"> <li><a href="">修改</a></li> </shiro:hasPermission> <shiro:hasPermission name="user:find:*"> <li><a href="">查询</a></li> </shiro:hasPermission> </ul> </li> </shiro:hasAnyRoles> <hr> <shiro:hasRole name="admin"> <li><a href="">用户管理(满足admin权限)</a> <ul> <li><a href="">商品管理</a></li> <li><a href="">订单管理</a></li> <li><a href="">物流管理</a></li> </ul> </shiro:hasRole> </body> </html>
7、测试
2.6、授权-基于权限(连接数据库,操作Mybatis)
1、DB
-- 权限表数据 INSERT INTO `shiro`.`t_pers` (`id`, `name`, `url`) VALUES (1, 'user:*:*', NULL); INSERT INTO `shiro`.`t_pers` (`id`, `name`, `url`) VALUES (2, 'product:*:01', NULL); INSERT INTO `shiro`.`t_pers` (`id`, `name`, `url`) VALUES (3, 'order:*:*', NULL); -- 基于角色的权限,一个角色可以有多个权限 INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (1, 1, 1); INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (2, 1, 2); INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (3, 1, 3); INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (4, 2, 1); INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (5, 3, 2);
2、接口
/** * 根据角色id查询权限集合 * * @param id 角色ID * @return 角色的权限集合 */ List<Perms> findPermsByRoleId(Integer id);
3、 mapper
<resultMap id="roleToPerms" type="com.zhixi.pojo.Perms"> <result column="pers_name" property="name"/> <result column="pers_url" property="url"/> </resultMap> <select id="findPermsByRoleId" resultMap="roleToPerms"> SELECT role.id, role.name as role_name, pers.`name` as pers_name, pers.url as pers_url FROM shiro.t_role role LEFT JOIN shiro.t_role_perms rp ON role.id = rp.roleid LEFT JOIN shiro.t_pers pers ON rp.permsid = pers.id WHERE role.id = #{id} </select>
4、自定义Realm-添加权限信息
/** * 处理授权 * * @param principals 权限数据 * @return 返回的授权信息 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 获取登录用户名(从认证那里传过来的) User userLogin = (User) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 查询登录用户权限集合 List<User> userList = userService.findRolesByUsernameRoles(userLogin.getUsername()); Optional.ofNullable(userList). ifPresent(users -> users.forEach(user -> { user.getRoles().forEach(role -> { // 添加角色信息 simpleAuthorizationInfo.addRole(role.getName()); // 根据角色ID查询权限集合 List<Perms> permsByRoleId = userService.findPermsByRoleId(role.getId()); // 添加权限信息 Optional.ofNullable(permsByRoleId).ifPresent(perms -> perms.forEach(per -> simpleAuthorizationInfo.addStringPermission(per.getName()))); } ); })); // 添加角色权限 return simpleAuthorizationInfo; }
5、受限页面:index.jsp
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <h2>系统主页v1.0(受限页面)</h2> <label> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> </label> <%--获取当前登录的用户名--%> <shiro:authenticated> <%--填写的是放在认证,SimpleAuthenticationInfo中的user数据--%> <h3> 当前登录用户是:<shiro:principal property="username"/> </h3> </shiro:authenticated> <shiro:hasAnyRoles name="user,admin"> <li><a href="">用户管理(满足user/admin权限)</a> <ul> <shiro:hasPermission name="user:add:*"> <li><a href="">添加</a></li> </shiro:hasPermission> <shiro:hasPermission name="user:delete:*"> <li><a href="">删除</a></li> </shiro:hasPermission> <shiro:hasPermission name="user:update:*"> <li><a href="">修改</a></li> </shiro:hasPermission> <shiro:hasPermission name="order:find:*"> <li><a href="">查询</a></li> </shiro:hasPermission> </ul> </li> </shiro:hasAnyRoles> <hr> <shiro:hasRole name="admin"> <li><a href="">用户管理(满足admin权限)</a> <ul> <li><a href="">商品管理</a></li> <li><a href="">订单管理</a></li> <li><a href="">物流管理</a></li> </ul> </shiro:hasRole> </body> </html>
6、测试
使用xiaochen登录,xioachen具有admin角色,admin角色具有:user:*:* product:*:01 order:*:*权限
使用zhangsan登录,zhangsan具有user、product角色。
user角色具有:user:*:*
product角色具有:product:*:01 权限
三、Shiro整合Cache
3.1、整合Ehcache
引入依赖
<!--Shiro整合Ehcache--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.5.3</version> </dependency>
修改自定义Realm
/** * 创建自定义realm * @return 自定义Realm */ @Bean("realm") public Realm getRealm(){ CustomerRealm customerRealm = new CustomerRealm(); //设置hashed凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //设置md5加密 credentialsMatcher.setHashAlgorithmName("md5"); //设置散列次数 credentialsMatcher.setHashIterations(1024); customerRealm.setCredentialsMatcher(credentialsMatcher); //开启缓存管理 customerRealm.setCacheManager(new EhCacheManager()); customerRealm.setCachingEnabled(true);//开启全局缓存 customerRealm.setAuthenticationCachingEnabled(true);//认证认证缓存 customerRealm.setAuthenticationCacheName("authenticationCache"); customerRealm.setAuthorizationCachingEnabled(true);//开启授权缓存 customerRealm.setAuthorizationCacheName("authorizationCache"); return customerRealm; }
测试
在第一次加载的时候会加载权限认证授权等数据,后序不会加载。
3.2、整合Redis
1、导入SpringBoot整合Redis依赖
<!--redis整合springboot--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2、配置application.properties
##################Redis配置############# spring.redis.port=6379 spring.redis.host=localhost spring.redis.database=2
3、SpringBoot获取Bean工具类
import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; /** * @author zhixi */ @Component public class ApplicationContextUtils implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { context = applicationContext; } /** * 根据bean名字获取工厂中指定bean 对象 * * @param beanName bean名称 * @return bean对象 */ public static Object getBean(String beanName) { return context.getBean(beanName); } }
4、创建Redis缓存管理器
import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; /** * @author zhixi */ public class RedisCacheManager implements CacheManager { /** * 缓存 * * @param cacheName 认证或者是授权缓存的统一名称 * @param <K> k * @param <V> v * @return 自定义CacheManager * @throws CacheException 异常 */ @Override public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException { System.out.println(cacheName); return new RedisCache<K, V>(cacheName); } }
5、Redis缓存实现类
/** * 自定义redis缓存的实现 * * @param <k> k * @param <v> v */ @SuppressWarnings("all") 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 { v v = (v) getRedisTemplate().opsForHash().get(this.cacheName, k.toString()); return v; } @Override public v put(k k, v v) throws CacheException { System.out.println("put key: " + k); System.out.println("put value:" + v); getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v); return null; } @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); } private RedisTemplate getRedisTemplate() { RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate"); /*设置key的序列化方式*/ redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); return redisTemplate; } }
6、设置Shiro盐的序列化方式
此处需要在认证的时候设置盐的序列化方式,不能够使用之前的ByteSource.Util了,否则会出现登录(序列化)正常,第二次登录(反序列化)失败的错误
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; /** * 解决: * shiro 使用缓存时出现:java.io.NotSerializableException: org.apache.shiro.util.SimpleByteSource * no valid constructor * 序列化后,无法反序列化的问题(https://blog.csdn.net/qq_15090197/article/details/108743271) */ public class MySimpleByteSource implements ByteSource, Serializable { private static final long serialVersionUID = 1L; private byte[] bytes; private String cachedHex; private String cachedBase64; public MySimpleByteSource(){ } public MySimpleByteSource(byte[] bytes) { this.bytes = bytes; } public MySimpleByteSource(char[] chars) { this.bytes = CodecSupport.toBytes(chars); } public MySimpleByteSource(String string) { this.bytes = CodecSupport.toBytes(string); } public MySimpleByteSource(ByteSource source) { this.bytes = source.getBytes(); } public MySimpleByteSource(File file) { this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file); } public MySimpleByteSource(InputStream stream) { this.bytes = (new MySimpleByteSource.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 void setBytes(byte[] bytes) { this.bytes = bytes; } @Override public byte[] getBytes() { return this.bytes; } @Override public String toHex() { if(this.cachedHex == null) { this.cachedHex = Hex.encodeToString(this.getBytes()); } return this.cachedHex; } @Override public String toBase64() { if(this.cachedBase64 == null) { this.cachedBase64 = Base64.encodeToString(this.getBytes()); } return this.cachedBase64; } @Override public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; } @Override public String toString() { return this.toBase64(); } @Override public int hashCode() { return this.bytes != null && this.bytes.length != 0? Arrays.hashCode(this.bytes):0; } @Override 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); } } }
7、设置自定义Realm的管理器为redis
8、测试
四、SpringBoot整合验证码功能(非shiro)
1、验证码工具类
import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Random; /** * @创建人 cx * @创建时间 2018/11/27 17:36 * @描述 验证码生成 */ public class VerifyCodeUtils { //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符 public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; private static final Random random = new Random(); /** * 使用系统默认字符源生成验证码 * * @param verifySize 验证码长度 * @return */ public static String generateVerifyCode(int verifySize) { return generateVerifyCode(verifySize, VERIFY_CODES); } /** * 使用指定源生成验证码 * * @param verifySize 验证码长度 * @param sources 验证码字符源 * @return */ public static String generateVerifyCode(int verifySize, String sources) { if (sources == null || sources.length() == 0) { sources = VERIFY_CODES; } int codesLen = sources.length(); Random rand = new Random(System.currentTimeMillis()); StringBuilder verifyCode = new StringBuilder(verifySize); for (int i = 0; i < verifySize; i++) { verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1))); } return verifyCode.toString(); } /** * 生成随机验证码文件,并返回验证码值 * * @param w * @param h * @param outputFile * @param verifySize * @return * @throws IOException */ public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException { String verifyCode = generateVerifyCode(verifySize); outputImage(w, h, outputFile, verifyCode); return verifyCode; } /** * 输出随机验证码图片流,并返回验证码值 * * @param w * @param h * @param os * @param verifySize * @return * @throws IOException */ public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException { String verifyCode = generateVerifyCode(verifySize); outputImage(w, h, os, verifyCode); return verifyCode; } /** * 生成指定验证码图像文件 * * @param w * @param h * @param outputFile * @param code * @throws IOException */ public static void outputImage(int w, int h, File outputFile, String code) throws IOException { if (outputFile == null) { return; } File dir = outputFile.getParentFile(); if (!dir.exists()) { dir.mkdirs(); } try { outputFile.createNewFile(); FileOutputStream fos = new FileOutputStream(outputFile); outputImage(w, h, fos, code); fos.close(); } catch (IOException e) { throw e; } } /** * 输出指定验证码图片流 * * @param w * @param h * @param os * @param code * @throws IOException */ public static void outputImage(int w, int h, OutputStream os, String code) throws IOException { int verifySize = code.length(); BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); Random rand = new Random(); Graphics2D g2 = image.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Color[] colors = new Color[5]; Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN, Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.YELLOW}; float[] fractions = new float[colors.length]; for (int i = 0; i < colors.length; i++) { colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)]; fractions[i] = rand.nextFloat(); } Arrays.sort(fractions); g2.setColor(Color.GRAY);// 设置边框色 g2.fillRect(0, 0, w, h); Color c = getRandColor(200, 250); g2.setColor(c);// 设置背景色 g2.fillRect(0, 2, w, h - 4); //绘制干扰线 Random random = new Random(); g2.setColor(getRandColor(160, 200));// 设置线条的颜色 for (int i = 0; i < 20; i++) { int x = random.nextInt(w - 1); int y = random.nextInt(h - 1); int xl = random.nextInt(6) + 1; int yl = random.nextInt(12) + 1; g2.drawLine(x, y, x + xl + 40, y + yl + 20); } // 添加噪点 float yawpRate = 0.05f;// 噪声率 int area = (int) (yawpRate * w * h); for (int i = 0; i < area; i++) { int x = random.nextInt(w); int y = random.nextInt(h); int rgb = getRandomIntColor(); image.setRGB(x, y, rgb); } shear(g2, w, h, c);// 使图片扭曲 g2.setColor(getRandColor(100, 160)); int fontSize = h - 4; Font font = new Font("Algerian", Font.ITALIC, fontSize); g2.setFont(font); char[] chars = code.toCharArray(); for (int i = 0; i < verifySize; i++) { AffineTransform affine = new AffineTransform(); affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2); g2.setTransform(affine); g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10); } g2.dispose(); ImageIO.write(image, "jpg", os); } private static Color getRandColor(int fc, int bc) { if (fc > 255) fc = 255; if (bc > 255) bc = 255; int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); } private static int getRandomIntColor() { int[] rgb = getRandomRgb(); int color = 0; for (int c : rgb) { color = color << 8; color = color | c; } return color; } private static int[] getRandomRgb() { int[] rgb = new int[3]; for (int i = 0; i < 3; i++) { rgb[i] = random.nextInt(255); } return rgb; } private static void shear(Graphics g, int w1, int h1, Color color) { shearX(g, w1, h1, color); shearY(g, w1, h1, color); } private static void shearX(Graphics g, int w1, int h1, Color color) { int period = random.nextInt(2); boolean borderGap = true; int frames = 1; int phase = random.nextInt(2); for (int i = 0; i < h1; i++) { double d = (double) (0) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); g.copyArea(0, i, w1, 1, (int) d, 0); g.setColor(color); g.drawLine((int) d, i, 0, i); g.drawLine((int) d + w1, i, w1, i); } } private static void shearY(Graphics g, int w1, int h1, Color color) { int period = random.nextInt(40) + 10; // 50; boolean borderGap = true; int frames = 20; int phase = 7; for (int i = 0; i < w1; i++) { double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); g.copyArea(i, 0, 1, h1, 0, (int) d); g.setColor(color); g.drawLine(i, (int) d, i, 0); g.drawLine(i, (int) d + h1, i, h1); } } }
2、添加验证码请求以及修改登录请求
/** * 用来处理身份认证 * * @return 登录成功返回页面 */ @RequestMapping(value = "login", method = RequestMethod.POST) public String login(User user, String code, HttpSession session) { //比较验证码 String codes = session.getAttribute("code").toString(); try { if (codes.equalsIgnoreCase(code)) { //获取主体对象 Subject subject = SecurityUtils.getSubject(); subject.login(new UsernamePasswordToken(user.getUsername(), user.getPassword())); return "redirect:/index.jsp"; }else{ throw new RuntimeException("验证码错误!"); } } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误!"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("密码错误!"); } catch (Exception e) { e.printStackTrace(); System.out.println("验证码错误"); } return "redirect:/login.jsp"; }
3、登录页面添加验证码标签
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <h1>登录页面</h1> <form action="${pageContext.request.contextPath}/user/login" method="post"> <label> 用户名: <input type="text" name="username"> </label> <br/> <label> 密码 : <input type="text" name="password"> </label> <br> <label> 输入验证码:<input type="text" name="code"> <img src="${pageContext.request.contextPath}/user/getImage" alt="验证码"/> </label><br> <input type="submit" value="登录"> <a href="/register.jsp">没有账号?点我注册</a> </form> </body> </html>
4、Shiro放行验证码请求
5、测试
五、Shiro整合Thymeleaf
https://github.com/zhangzhixi0305/springboot-study-shiro/tree/master/shiro-04-thymeleaf