SpringBoot整合Shiro
Shiro简介 Shiro是Apache下的一个开源项目,我们称之为Apache Shiro。它是一个很易用与Java项目的的安全框架,提供了认证、授权、加密、会话管理,与Spring Security 一样都是做一个权限的安全框架,但是与Spring Security 相比,在于 Shiro 使用了比较简单易懂易于使用的授权方式。shiro属于轻量级框架,相对于security简单的多,也没有security那么复杂。所以我这里也是简单介绍一下shiro的使用。
Shiro的架构
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。
● Authentication(认证):用户身份识别,通常被称为用户“登录” ● Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。 ● Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。 ● Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。 同时Shiro还提供了其他特性来在不同的应用程序环境下使用强化以上的四大基石: ● Web支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。 ● 缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。 ● 并发:Apache Shiro 支持多线程应用程序的并发特性。 ● 测试:支持单元测试和集成测试,确保代码和预想的一样安全。 ● “Run As”:这个功能允许用户假设另一个用户的身份(在许可的前提下)。 ● “Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。
Shiro 的三大核心组件
● Subject其实代表的就是当前正在执行操作的用户,只不过因为“User”一般指代人,但是一个“Subject”可以是人,也可以是任何的第三方系统,服务账号等任何其他正在和当前系统交互的第三方软件系统。所有的Subject实例都被绑定到一个SecurityManager,如果你和一个Subject交互,所有的交互动作都会被转换成Subject与SecurityManager的交互。
● SecurityManager 是Shiro的核心,用于管理所有的Subject ,它主要用于协调Shiro内部各种安全组件,不过我们一般不用太关心SecurityManager,对于应用程序开发者来说,主要还是使用Subject的API来处理各种安全验证逻辑。
● Realm 这是用于连接Shiro和客户系统的用户数据的桥梁。一旦Shiro真正需要访问各种安全相关的数据(比如使用用户账户来做用户身份验证以及权限验证)时,他总是通过调用系统配置的各种Realm来读取数据。 正因为如此,Realm往往被看做是安全领域的DAO,他封装了数据源连接相关的细节,将数据以Shiro需要的格式提供给Shiro。当我们配置Shiro的时候,我们至少需要配置一个Realm来提供用户验证和权限控制方面的数据。我们可能会给SecurityManager配置多个Realm,但是不管怎样,我们至少需要配置一个。 Shiro提供了几种开箱即用的Realm来访问安全数据源,比如LDAP、关系数据库、基于ini的安全配置文件等等,如果默认提供的这几种Realm无法满足你的需求,那么你也可以编写自己的定制化的Realm插件。 和其他内部组件一样,SecurityManager决定了在Shiro中如何使用Realm来读取身份和权限方面的数据,然后组装成Subject实例。
Shiro完整架构
● Subject (org.apache.shiro.subject.Subject),如上所述. ● SecurityManager (org.apache.shiro.mgt.SecurityManager),如上所述. ● Authenticator(用户认证管理器), (org.apache.shiro.authc.Authenticator) 这个组件主要用于处理用户登录逻辑,他通过调用Realm的接口来判断当前登录的用户的身份。 用户认证策略,(org.apache.shiro.authc.pam.AuthenticationStrategy) 如果系统配置了多个Realm,则需要使用AuthenticationStrategy 来协调这些Realm以便决定一个用户登录的认证是成功还是失败。(比如,如果一个Realm验证成功了,但是其他的都失败了,怎么算?还是说都成功才算成功). ● Authorizer(权限管理器)(org.apache.shiro.authz.Authorizer)这个组件主要是用来做用户的访问控制。通俗来说就是决定用户能做什么、不能做什么。和Authenticator类似,Authorizer也知道怎么协调多个Realm数据源的数据,他有自己的一套策略。 ● SessionManager(会话管理器) (org.apache.shiro.session.mgt.SessionManager) SessionManager知道如何创建会话、管理用户回话的声明周期以便在所有运行环境下都可以给用户提供一个健壮的回话管理体验。Shiro在任何运行环境下都可以在本地管理用户会话(即便没有Web或者EJB容器也可以)——这在安全管理的框架中算是独门绝技了。当然,如果当前环境中有会话管理机制(比如Servlet容器),则Shiro默认会使用该环境的会话管理机制。而如果像控制台程序这种独立的应用程序,本身没有会话管理机制,此时Shiro就会使用内部的会话管理器来给应用的开发提供一直的编程体验。SessionDAO允许用户使用任何类型的数据源来存储Session数据。 ● SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO) 用于代替SessionManager执行Session相关的增删改查。这个接口允许我们将任意种类的数据存储方式引入到Session管理的基础框架中。 ● CacheManager (org.apache.shiro.cache.CacheManager) CacheManager用于创建和维护一些在其他的Shiro组件中用到的Cache实例,维护这些Cache实例的生命周期。缓存用于存储那些从后端获取到的用户验证与权限控制方面的数据以提高性能,缓存是一等公民,在获取数据时,总是先从缓存中查找,如果没有再调用后端接口从其他数据源获取。Shiro允许用户使用其他更加现代的、企业级的数据源来替代内部的默认实现,以提供更高的性能和更好的用户体验。 ● Cryptography 加密技术,(org.apache.shiro.crypto.*) 对于一个企业级的安全框架来说,加密算是其固有的一种特性。Shiro的crypto包中包含了一系列的易于理解和使用的加密、哈希(aka摘要)辅助类。这个包内的所有类都是经过精心设计,相比于java本身提供的那一套反人类的加密组件,Shiro提供的这套加密组件简直不要好用太多。 ● Realm (org.apache.shiro.realm.Realm) 就如上文所提到的,Realm是连接Shiro和你的安全数据的桥梁。任何时候当Shiro需要执行登录或者访问控制的时候,都需要调用已经配置的Realm的接口去获取数据。一个应用程序可以配置一个或者多个Realm(最少配置一个)。
整合Shiro
使用springboot+mybatis+druid+thymleaf模板实现快速入门
前置工作 整合mybatis+durid
参考springboot 整合durid,springboot 整合mybatis
整合thymleaf
参考springboot 整合模板引擎
配置文件配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 spring.datasource.type =com.alibaba.druid.pool.DruidDataSource spring.datasource.driverClassName =com.mysql.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC spring.datasource.username =root spring.datasource.password =root spring.druid.initialSize =5 spring.druid.minIdle =5 spring.druid.maxActive =20 spring.druid.maxWait =60000 spring.druid.timeBetweenEvictionRunsMillis =60000 spring.druid.minEvictableIdleTimeMillis =300000 spring.druid.validationQuery =SELECT 1 FROM DUAL spring.druid.testWhileIdle =true spring.druid.testOnBorrow =false spring.druid.testOnReturn =false spring.druid.poolPreparedStatements =false spring.druid.maxPoolPreparedStatementPerConnectionSize =20 spring.druid.filters =stat,wall,log4j spring.druid.connectionProperties ='druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000' spring.redis.port =6379 spring.redis.host =127.0.0.1 spring.redis.database =0 spring.redis.jedis.pool.max-active =8 spring.redis.jedis.pool.max-wait =-1 spring.redis.jedis.pool.max-idle =5 spring.redis.jedis.pool.min-idle =0 spring.redis.timeout =10000 mybatis.mapper-locations =classpath:mapper/*.xml mybatis.type-aliases-package =com.demo.entity
POM依赖导入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-spring</artifactId > <version > 1.5.2</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency > <dependency > <groupId > com.github.theborakompanioni</groupId > <artifactId > thymeleaf-extras-shiro</artifactId > <version > 2.0.0</version > <exclusions > <exclusion > <groupId > org.apache.shiro</groupId > <artifactId > shiro-core</artifactId > </exclusion > <exclusion > <groupId > org.thymeleaf</groupId > <artifactId > thymeleaf</artifactId > </exclusion > <exclusion > <groupId > junit</groupId > <artifactId > junit</artifactId > </exclusion > </exclusions > </dependency >
数据库建表 user_info.sql(用户表) 1 2 3 4 5 6 7 8 9 10 11 12 DROP TABLE IF EXISTS `user_info`;CREATE TABLE `user_info` ( `uid` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (50 ) DEFAULT '' COMMENT '用户名' , `password` varchar (256 ) DEFAULT NULL COMMENT '登录密码' , `name` varchar (256 ) DEFAULT NULL COMMENT '用户真实姓名' , `id_card_num` varchar (256 ) DEFAULT NULL COMMENT '用户身份证号' , `state` char (1 ) DEFAULT '0' COMMENT '用户状态:0:正常状态,1:用户被锁定' , PRIMARY KEY (`uid`), UNIQUE KEY `username` (`username`) USING BTREE, UNIQUE KEY `id_card_num` (`id_card_num`) USING BTREE ) ENGINE= InnoDB DEFAULT CHARSET= utf8;
sys_role.sql(角色表) 1 2 3 4 5 6 7 8 9 DROP TABLE IF EXISTS `sys_role`;CREATE TABLE `sys_role` ( `id` int (11 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `available` char (1 ) DEFAULT '0' COMMENT '是否可用0可用 1不可用' , `role` varchar (20 ) DEFAULT NULL COMMENT '角色标识程序中判断使用,如"admin"' , `description` varchar (100 ) DEFAULT NULL COMMENT '角色描述,UI界面显示使用' , PRIMARY KEY (`id`), UNIQUE KEY `role` (`role`) USING BTREE ) ENGINE= InnoDB DEFAULT CHARSET= utf8;
sys_user_role.sql(用户-角色表) 1 2 3 4 5 6 7 DROP TABLE IF EXISTS `sys_user_role`;CREATE TABLE `sys_user_role` ( `uid` int (11 ) DEFAULT NULL COMMENT '用户id' , `role_id` int (11 ) DEFAULT NULL COMMENT '角色id' , KEY `uid` (`uid`) USING BTREE, KEY `role_id` (`role_id`) USING BTREE ) ENGINE= InnoDB DEFAULT CHARSET= utf8;
sys_permission.sql(权限表) 1 2 3 4 5 6 7 8 9 10 11 12 DROP TABLE IF EXISTS `sys_permission`;CREATE TABLE `sys_permission` ( `id` int (11 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `parent_id` int (11 ) DEFAULT NULL COMMENT '父编号,本权限可能是该父编号权限的子权限' , `parent_ids` varchar (20 ) DEFAULT NULL COMMENT '父编号列表' , `permission` varchar (100 ) DEFAULT NULL COMMENT '权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view' , `resource_type` varchar (20 ) DEFAULT NULL COMMENT '资源类型,[menu|button]' , `url` varchar (200 ) DEFAULT NULL COMMENT '资源路径 如:/userinfo/list' , `name` varchar (50 ) DEFAULT NULL COMMENT '权限名称' , `available` char (1 ) DEFAULT '0' COMMENT '是否可用0可用 1不可用' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8;
sys_role_permission.sql(角色-权限表) 1 2 3 4 5 6 7 DROP TABLE IF EXISTS `sys_role_permission`;CREATE TABLE `sys_role_permission` ( `role_id` int (11 ) DEFAULT NULL COMMENT '角色id' , `permission_id` int (11 ) DEFAULT NULL COMMENT '权限id' , KEY `role_id` (`role_id`) USING BTREE, KEY `permission_id` (`permission_id`) USING BTREE ) ENGINE= InnoDB DEFAULT CHARSET= utf8;
初始化数据导入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #插入用户信息表 INSERT INTO user_info(uid,username,`password`,`name`,id_card_num) VALUES (null ,'admin' ,'123456' ,'唐僧' ,'133333333333333333' );INSERT INTO user_info(uid,username,`password`,`name`,id_card_num) VALUES (null ,'test' ,'123456' ,'孙悟空' ,'155555555555555555' );#插入用户角色表 INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (null ,0 ,'管理员' ,'admin' );INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (null ,0 ,'VIP会员' ,'vip' );INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (null ,1 ,'测试' ,'test' );#插入用户_角色关联表 INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1 ,1 );INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (2 ,2 );#插入权限表 INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (null ,0 ,'用户管理' ,0 ,'0/' ,'userInfo:view' ,'menu' ,'userInfo/view' );INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (null ,0 ,'用户添加' ,1 ,'0/1' ,'userInfo:add' ,'button' ,'userInfo/add' );INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (null ,0 ,'用户删除' ,1 ,'0/1' ,'userInfo:del' ,'button' ,'userInfo/del' );#插入角色_权限表 INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1 ,1 );INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2 ,1 );INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (3 ,2 );
创建实体类 用户信息 1 2 3 4 5 6 7 8 9 10 public class User { private Integer uid; private String username; private String password; private String name; private String id_card_num; private String state; private Set<Role> roles = new HashSet <>(); }
角色信息 1 2 3 4 5 6 7 8 9 10 public class Role { private Integer id; private String role; private String description; private String available; private Set<User> users = new HashSet <>(); private Set<Permission> permissions = new HashSet <>(); }
权限信息 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Permission { private Integer id; private Integer parent_id; private String parent_ids; private String permission; private String resource_type; private String url; private String name; private String available; private Set<Role> roles = new HashSet <>(); }
编写mapper UserMapper 1 2 3 4 5 6 @Mapper public interface UserMapper { User findByUserName (String userName) ; int insert (User user) ; int del (@Param("username") String username) ; }
UserMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.demo.mapper.UserMapper" > <select id ="findByUserName" resultType ="com.demo.entity.User" > SELECT * FROM user_info WHERE username = #{userName} </select > <insert id ="insert" parameterType ="com.demo.entity.User" > <selectKey resultType ="java.lang.Integer" keyProperty ="uid" order ="AFTER" > SELECT LAST_INSERT_ID() </selectKey > insert into user_info <trim prefix ="(" suffix =")" suffixOverrides ="," > <if test ="uid != null" > uid, </if > <if test ="username != null and username != ''" > username, </if > <if test ="password != null and password != ''" > password, </if > <if test ="name != null and name != ''" > `name`, </if > <if test ="id_card_num != null and id_card_num != ''" > id_card_num, </if > <if test ="state != null and state != ''" > state, </if > </trim > <trim prefix ="values (" suffix =")" suffixOverrides ="," > <if test ="uid != null" > #{uid}, </if > <if test ="username != null and username != ''" > #{username}, </if > <if test ="password != null and password != ''" > #{password}, </if > <if test ="name != null and name != ''" > #{name}, </if > <if test ="id_card_num != null and id_card_num != ''" > #{id_card_num}, </if > <if test ="state != null and state != ''" > #{state}, </if > </trim > </insert > <delete id ="del" > DELETE FROM user_info WHERE username = #{username} </delete > </mapper >
RoleMapper 1 2 3 4 @Mapper public interface RoleMapper { Set<Role> findRolesByUserId (@Param("uid") Integer uid) ; }
RoleMapper.xml 1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.demo.mapper.RoleMapper" > <select id ="findRolesByUserId" resultType ="com.demo.entity.Role" > SELECT r.* from sys_role r LEFT JOIN sys_user_role ur on r.id = ur.role_id where ur.uid = #{uid} </select > </mapper >
PermissionMapper 1 2 3 4 @Mapper public interface PermissionMapper { Set<Permission> findPermissionsByRoleId (@Param("roles") Set<Role> roles) ; }
PermissionMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.demo.mapper.PermissionMapper" > <select id ="findPermissionsByRoleId" resultType ="com.demo.entity.Permission" > SELECT p.* from sys_permission p LEFT JOIN sys_role_permission rp on p.id = rp.permission_id WHERE rp.role_id IN <foreach collection ="roles" index ="index" item ="item" open ="(" close =")" separator ="," > #{item.id} </foreach > </select > </mapper >
Shiro 相关配置类 ShiroConfig
创建ShiroConfig.java配置类
我们需要定义一系列关于URL的规则和访问权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 @Configuration public class ShiroConfig { @Bean(name = "shirFilter") public ShiroFilterFactoryBean shiroFilter (@Qualifier("securityManager") SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean (); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login" ); shiroFilterFactoryBean.setSuccessUrl("/index" ); shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized" ); LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap <>(); filterChainDefinitionMap.put("/login" , "anon" ); filterChainDefinitionMap.put("/" , "anon" ); filterChainDefinitionMap.put("/css/**" , "anon" ); filterChainDefinitionMap.put("/js/**" , "anon" ); filterChainDefinitionMap.put("/img/**" , "anon" ); filterChainDefinitionMap.put("/druid/**" , "anon" ); filterChainDefinitionMap.put("/logout" , "logout" ); filterChainDefinitionMap.put("/**" , "authc" ); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean(name = "securityManager") public SecurityManager securityManager (@Qualifier("shiroRealm") ShiroRealm shiroRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager (); securityManager.setRealm(shiroRealm); return securityManager; } @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor () { return new LifecycleBeanPostProcessor (); } @Bean public ShiroRealm shiroRealm () { ShiroRealm shiroRealm = new ShiroRealm (); return shiroRealm; } @Bean public ShiroDialect shiroDialect () { return new ShiroDialect (); } }
Shiro内置的FilterChain
Filter Name
Class
anon
org.apache.shiro.web.filter.authc.AnonymousFilter
authc
org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic
org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms
org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port
org.apache.shiro.web.filter.authz.PortFilter
rest
org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles
org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl
org.apache.shiro.web.filter.authz.SslFilter
user
org.apache.shiro.web.filter.authc.UserFilter
anon :所有url都都可以匿名访问;authc : 需要认证才能进行访问;user :配置记住我或认证通过可以访问;
这几个是我们会用到的,在这里说明下,其它的请自行查询文档进行学习。
ShiroRealm
在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。因为在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的。通常情况下,在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 public class ShiroRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private PermissionMapper permissionMapper; @Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; String username = usernamePasswordToken.getUsername(); String password = new String (usernamePasswordToken.getPassword()); User user = this .userMapper.findByUserName(username); if (user == null ) { throw new UnknownAccountException ("用户名或密码错误!" ); } if (!password.equals(user.getPassword())) { throw new IncorrectCredentialsException ("用户名或密码错误!" ); } if ("1" .equals(user.getState())) { throw new LockedAccountException ("账号已被锁定,请联系管理员!" ); } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo (user,user.getPassword(), getName()); return info; } @Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) { User user = (User) SecurityUtils.getSubject().getPrincipal(); Set<Role> roles =this .roleMapper.findRolesByUserId(user.getUid()); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo (); for (Role role : roles) { authorizationInfo.addRole(role.getRole()); } Set<Permission> permissions = this .permissionMapper.findPermissionsByRoleId(roles); for (Permission permission:permissions) { authorizationInfo.addStringPermission(permission.getPermission()); } return authorizationInfo; } }
编写Service UserService 1 2 3 4 5 6 7 8 public interface UserService { User findByUserName (String userName) ; int insert (User user) ; int del (@Param("username") String username) ; }
UserServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class UserServiceImpl implements UserService { private UserMapper userMapper; @Override public User findByUserName (String userName) { return userMapper.findByUserName(userName); } @Override public int insert (User user) { return userMapper.insert(user); } @Override public int del (String username) { return userMapper.del(username); } }
RoleService 1 2 3 4 5 public interface RoleService { Set<Role> findRolesByUserId (Integer uid) ; }
RoleServiceIml 1 2 3 4 5 6 7 8 9 10 11 12 @Service public class RoleServiceImpl implements RoleService { @Autowired private RoleMapper roleMapper; @Override public Set<Role> findRolesByUserId (Integer uid) { return roleMapper.findRolesByUserId(uid); } }
PermissionService 1 2 3 4 5 public interface PermissionService { Set<Permission> findPermissionsByRoleId (Set<Role> roles) ; }
PermissionServiceImpl 1 2 3 4 5 6 7 8 9 10 11 @Service public class PermissionServiceImpl implements PermissionService { @Autowired private PermissionMapper permissionMapper; @Override public Set<Permission> findPermissionsByRoleId (Set<Role> roles) { return permissionMapper.findPermissionsByRoleId(roles); } }
编写Controller LoginController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 @Controller public class LoginController { @RequestMapping(value = "/",method = RequestMethod.GET) public String root (Model model) { Subject subject = SecurityUtils.getSubject(); User user=(User) subject.getPrincipal(); if (user == null ){ return "redirect:/login" ; }else { return "redirect:/index" ; } } @RequestMapping(value = "/login",method = RequestMethod.GET) public String login (Model model) { Subject subject = SecurityUtils.getSubject(); User user=(User) subject.getPrincipal(); if (user == null ){ return "login" ; }else { return "redirect:index" ; } } @RequestMapping(value = "/login",method = RequestMethod.POST) public String loginUser (HttpServletRequest request, String username, String password, Model model, HttpSession session) { UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken (username,password); Subject subject = SecurityUtils.getSubject(); try { subject.login(usernamePasswordToken); User user=(User) subject.getPrincipal(); session.setAttribute("user" , user); model.addAttribute("user" ,user); return "index" ; } catch (Exception e) { String exception = (String) request.getAttribute("shiroLoginFailure" ); model.addAttribute("msg" ,e.getMessage()); return "login" ; } } @RequestMapping("/index") public String index (HttpSession session, Model model) { Subject subject = SecurityUtils.getSubject(); User user=(User) subject.getPrincipal(); if (user == null ){ return "login" ; }else { model.addAttribute("user" ,user); return "index" ; } } @RequestMapping("/logout") public String logout (HttpSession session, Model model) { Subject subject = SecurityUtils.getSubject(); subject.logout(); model.addAttribute("msg" ,"安全退出!" ); return "login" ; } }
UserController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @RestController @RequestMapping("userInfo") public class UserController { @Autowired private UserService userService; @RequestMapping(value = "/add",method = RequestMethod.GET) @ResponseBody public String login (Model model) { User user = new User (); user.setName("王赛超" ); user.setId_card_num("177777777777777777" ); user.setUsername("baiyp" ); userService.insert(user); return "创建用户成功" ; } @RequestMapping(value = "/del",method = RequestMethod.GET) @ResponseBody public String del (Model model) { userService.del("baiyp" ); return "删除用户名为baiyp用户成功" ; } @RequestMapping(value = "/view",method = RequestMethod.GET) @ResponseBody public String view (Model model) { return "这是用户列表页" ; } }
UnifiedErrorController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Controller public class UnifiedErrorController { @RequestMapping("/unauthorized") public String unauthorized (HttpSession session, Model model) { return "unauthorized" ; } @RequestMapping("/404") public String error404 (HttpSession session, Model model) { return "404" ; } @RequestMapping("/500") public String error500 (HttpSession session, Model model) { return "500" ; } }
新建页面 login.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!DOCTYPE html > <html xmlns ="http://www.w3.org/1999/xhtml" xmlns:th ="http://www.thymeleaf.org" xmlns:sec ="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3" xmlns:shiro ="http://www.pollix.at/thymeleaf/shiro" > <head > <meta charset ="UTF-8" /> <title > Insert title here</title > </head > <body > <h1 > 欢迎登录</h1 > <h1 th:if ="${msg != null }" th:text ="${msg}" style ="color: red" > </h1 > <form action ="/login" method ="post" > 用户名:<input type ="text" name ="username" /> <br /> 密 码:<input type ="password" name ="password" /> <br /> <input type ="submit" value ="提交" /> </form > </body > </html >
index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 <!DOCTYPE html > <html xmlns ="http://www.w3.org/1999/xhtml" xmlns:th ="http://www.thymeleaf.org" xmlns:sec ="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3" xmlns:shiro ="http://www.pollix.at/thymeleaf/shiro" > <head > <meta charset ="UTF-8" /> <title > Insert title here</title > </head > <body > <h1 th:text ="'欢迎' + ${user.username } + '光临!请选择你的操作'" > </h1 > <br /> <ul > <h1 th:if ="${msg != null }" th:text ="${msg}" style ="color: red" > </h1 > <shiro:hasPermission name ="userInfo:add" > <a href ="/userInfo/add" > 点击添加固定用户信息(后台写死,方便测试)</a > </shiro:hasPermission > <br /> <shiro:hasPermission name ="userInfo:del" > <a href ="/userInfo/del" > 点击删除固定用户信息(后台写死,方便测试)</a > </shiro:hasPermission > <br /> <shiro:hasPermission name ="userInfo:view" > <a href ="/userInfo/view" > 显示此内容表示拥有查看用户列表的权限</a > </shiro:hasPermission > <br /> <shiro:guest > 游客显示的信息</shiro:guest > <br /> <shiro:user > 用户已经登录过了</shiro:user > <br /> <shiro:authenticated > 不是记住我登录</shiro:authenticated > <br /> <shiro:principal > </shiro:principal > <br /> <shiro:notAuthenticated > 已记住用户</shiro:notAuthenticated > <br /> <shiro:principal type ="java.lang.String" /> <br /> <shiro:principal property ="username" /> <br /> <shiro:hasRole name ="admin" > 这是admin角色</shiro:hasRole > <br /> <shiro:hasAnyRoles name ="admin,vip" > 用户拥有admin角色 或者 vip角色</shiro:hasAnyRoles > <br /> <shiro:lacksRole name ="admin" > 如果不是admin角色,显示内容</shiro:lacksRole > <br /> <shiro:hasPermission name ="userInfo:add" > 用户拥有添加权限</shiro:hasPermission > <br /> <shiro:hasAllPermissions name ="userInfo:add,userInfo:view" > 用户同时拥有列表权限和添加权限</shiro:hasAllPermissions > <br /> <shiro:hasAnyPermissions name ="userInfo:view,userInfo:del" > 用户拥有列表权限或者删除权限</shiro:hasAnyPermissions > <br /> <shiro:lacksPermission name ="userInfo:add" > 如果用户没有添加权限,显示的内容</shiro:lacksPermission > <br /> </ul > <a href ="/logout" > 点我注销</a > </body > </html >
unauthorized.html 1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html > <html xmlns ="http://www.w3.org/1999/xhtml" xmlns:th ="http://www.thymeleaf.org" xmlns:sec ="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3" xmlns:shiro ="http://www.pollix.at/thymeleaf/shiro" > <head > <meta charset ="UTF-8" /> <title > Insert title here</title > </head > <body > <h1 > 对不起,您没有权限</h1 > </body > </html >
500.html 1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html > <html xmlns ="http://www.w3.org/1999/xhtml" xmlns:th ="http://www.thymeleaf.org" xmlns:sec ="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3" xmlns:shiro ="http://www.pollix.at/thymeleaf/shiro" > <head > <meta charset ="UTF-8" /> <title > Insert title here</title > </head > <body > <h1 > 对不起,服务器报错了</h1 > </body > </html >
404.html 1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html > <html xmlns ="http://www.w3.org/1999/xhtml" xmlns:th ="http://www.thymeleaf.org" xmlns:sec ="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3" xmlns:shiro ="http://www.pollix.at/thymeleaf/shiro" > <head > <meta charset ="UTF-8" /> <title > Insert title here</title > </head > <body > <h1 > 对不起,你的页面没有找到</h1 > </body > </html >
进行身份验证测试 第一步: 访问http://localhost:8080/userInfo/add 发现自动跳转到登录页
第二步: 使用admin登录
第三步: 注销之后使用test登录
不同的用户登录,显示不同的功能,点击之后也可以调用后台服务,证明身份验证成功。
权限功能校验
经过上面的过程,已经可以对用户的身份进行校验,但是这个时候,但是权限控制好像没有什么作用
,因为我们使用admin用户登录之后,在浏览器上访问地址 /userInfo/del发现也是可以使用的,其实我们还少了以下步骤,也就是开启注解支持
开启Shiro的注解
在ShiroConfig中配置以下bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Bean public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator () { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator (); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true ); return defaultAdvisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor (@Qualifier("securityManager") SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor (); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }
在controller增加权限 1 2 3 4 5 6 7 8 @RequiresPermissions("userInfo:del") @RequestMapping(value = "/del",method = RequestMethod.GET) @ResponseBody public String del (Model model) { userService.del("baiyp" ); return "删除用户名为baiyp用户成功" ; }
添加@RequiresPermissions("userInfo:del")
然后重启项目,再次使用amdin登录之后,在浏览器上调用http://localhost:9090/userInfo/del就会跳转到以下错误页。证明权限校验成功。
后台会报以下异常:调用未授权的方法
到此,shiro入门完了,我相信很多人对shiro 已经可以说了解怎么用了,其实还有很多问题:
首先是错误页显示,没有权限理论应该跳转到我们配置的无权限的页面,但是并没有
我们不断的访问http://localhost:8080/userInfo/view 发现每次都会去数据库查询权限,但是实际中我们的权限信息是不怎么会改变的,所以我们希望是第一次访问,然后进行缓存处理等等,这些会在后面的文章中。
解决 Whitelabel Error Page
页面显示为默认springboot展示的页面Whitelabel Error Page,具体页面如下,并没有跳转到我们之前配置的页面 。
配置异常处理器
新增SimpleMappingExceptionResolver
也就是说shiroFilterFactoryBean.setUnauthorizedUrl(“/unauthorized”);无效!但是有时候我们需要向客户展示友好界面,所以,还需要如下配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Bean public SimpleMappingExceptionResolver simpleMappingExceptionResolver () { SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver (); Properties properties = new Properties (); properties.setProperty("org.apache.shiro.authz.UnauthorizedException" , "/unauthorized" ); properties.setProperty("org.apache.shiro.authz.UnauthenticatedException" , "/unauthorized" ); simpleMappingExceptionResolver.setExceptionMappings(properties); return simpleMappingExceptionResolver; }
测试
然后重新启动程序,发现访问无权限的后台服务,就会跳转到我们配置的页面。
但是这个时候还有一个问题:访问不存在的后台服务,如http://localhost:8080/userInfo/xxxxx 这样后面随便乱打的,这种情况,依然还是返回了Whitelabel Error Page页面。
我们不想看到这种效果,所以还需要在ShiroConfig配置类中,添加以下bean
1 2 3 4 5 6 7 8 9 10 11 12 13 @Bean public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer () { return factory -> { ErrorPage error401Page = new ErrorPage (HttpStatus.UNAUTHORIZED, "/unauthorized" ); ErrorPage error404Page = new ErrorPage (HttpStatus.NOT_FOUND, "/404" ); ErrorPage error500Page = new ErrorPage (HttpStatus.INTERNAL_SERVER_ERROR, "/500" ); factory.addErrorPages(error401Page, error404Page, error500Page); };
再次访问http://localhost:8080/userInfo/xxxxx 因为服务根本不存在,就会跳转到404页面。
如果访问http://localhost:8080/userInfo/del 就会跳转到unauthorized.html 因为没有权限。
配置记住我
记住我功能在各各网站是比较常见的,实现起来也都差不多,主要就是利用cookie来实现,而shiro对记住我功能的实现也是比较简单的,只需要几步即可。
基本流程 Shiro提供了记住我(RememberMe)的功能,比如访问一些网站时,关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
首先在登录页面选中RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来;
关闭浏览器再重新打开;会发现浏览器还是记住你的;
访问一般的网页服务器端还是知道你是谁,且能正常访问;
ShiroConfig相关配置 记住我cookie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean public SimpleCookie rememberMeCookie () { SimpleCookie simpleCookie = new SimpleCookie ("rememberMe" ); simpleCookie.setHttpOnly(true ); simpleCookie.setPath("/" ); simpleCookie.setMaxAge(2592000 ); return simpleCookie; }
记住我管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 @Bean public CookieRememberMeManager rememberMeManager () { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager (); cookieRememberMeManager.setCookie(rememberMeCookie()); cookieRememberMeManager.setCipherKey(Base64.decode("6ZmI6I2j5Y+R5aSn5ZOlAA==" )); return cookieRememberMeManager; }
记住我Filter 1 2 3 4 5 6 7 8 9 10 11 12 @Bean public FormAuthenticationFilter formAuthenticationFilter () { FormAuthenticationFilter formAuthenticationFilter = new FormAuthenticationFilter (); formAuthenticationFilter.setRememberMeParam("rememberMe" ); return formAuthenticationFilter; }
修改相关代码 修改安全事务管理器
在SecurityManager中打开记住我,之前是注释掉的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Bean(name = "securityManager") public SecurityManager securityManager (@Qualifier("shiroRealm") ShiroRealm shiroRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager (); securityManager.setRealm(shiroRealm); securityManager.setRememberMeManager(rememberMeManager()); ... return securityManager; }
修改拦截规则
修改shirFilter中拦截请求的规则,将/**从authc 改为user
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Bean(name = "shirFilter") public ShiroFilterFactoryBean shiroFilter (@Qualifier("securityManager") SecurityManager securityManager) { ... filterChainDefinitionMap.put("/**" , "user" ); ... return shiroFilterFactoryBean; }
更改LoginController
接受前台记住我参数,然后传给usernamePasswordToken,将之前的那行注释掉。
1 2 3 4 5 6 7 8 9 10 11 @RequestMapping(value = "/login", method = RequestMethod.POST) public String loginUser (HttpServletRequest request, String username, String password, boolean rememberMe, Model model, HttpSession session) { UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken (username, password, rememberMe); ... }
更改User实体类
User实体类必须实现序列化接口,否则报序列化异常。因为RememberMe会将用户信息加密然后以Cookie保存。
1 2 3 public class User implements Serializable { ... }
更改Login.html
前台页面添加记住我复选框
1 2 3 4 5 6 7 8 9 10 ... <h1 > 欢迎登录</h1 > <h1 th:if ="${msg != null }" th:text ="${msg}" style ="color: red" > </h1 > <form action ="/login" method ="post" > 用户名:<input type ="text" name ="username" /> <br /> 密 码:<input type ="password" name ="password" /> <br />
测试
上面的过程已经配置完成,现在进行测试,重启项目,使用admin登录,然后访问:http://localhost:8080/userInfo/view会跳转到登录页面,登录之后退出浏览器,再次点开访问刚才的页面,可以访问。
浏览器cookie查看
rememberMe 大概内容如下 1 blDl1IGoHN12pWm/dS8+PCwp81NwooEbdvkxMreWffEGcWtVeOUBusIPgjnUbByPhK9dxXuybhvUwjf5AVrYoACHHN3qim0A0Gw1jIPMYibzbcra+dYkBAQo3PKq+bPzDg99zSM60PcT4k7ZauMoGnZmTKrmgItZ2FytCFam9xUBgueIgSSocoHpG2yedJI9RqgCr9KNJVeSg9NKiSJ3cfqTjbjvHZ2yXzTym0735Pq9hZvJ0PicqqXtZphVzIXL4TfER7MRInKgD1GmLXdhGySKHrnvzpGrqUj4f4a1C6IjbTwRcE+Gf2nK9ydqcrHqAjlrbADn8uVInP9uqTDtHU0tiB73M6GQtwdXrMWXD9r75FhUCYZVA9ZxrMgxpISaVBygCWoLjQpR8SR/PRG40qP6lNqs/xjEVd+EUcfIIKdWTVQhCv4/HDCFxF2w5CFPY2KZqWmClLIPKjUVf+40Otur8/eLJZE9LSsb2GiKpZOnFy4/5rgVWLvPGWBcca/UaO/ZYJ/cANHP70/rmIsRblu42ZAXGZiIyI++S+hxDPgfTU44X2xKCKaFhQLkWV0akoXrmrwQV3aMcJrMusIL2lIk+Cyw0yhCxePPQQyuvHTRcm8LpFtVLm1nOqv+l2TRgIVjhKXutIRIFWNPP05q0+LSnJ+3xYSO9Kvo36PN+EqjU2vTy5oTbNaqkm070NXclKju0f37+c9J4VVz96I3dvSHqBgYevRDUGOdw5LuDWP7lh83l8ptfn1m5Mn2Hr9TmLrbscAN8c00sorGOaB1mcjlQyH7uYoAF/i03RfCD7uA1RMd+6NSXyeSFehmueVn58rv3BHPVS9UlZxoiGXoPkP3sQc0QKdccCfN0Iolt2ZnPmvZDpx3VA963zzqfMYYYc6T/3q0XCyga6lVdMqsi+llkTsydQWx3rtRSLfyTkHTpDxmZv5qfR+f8mDPRJ6qI0pKf8SuTr0=
其实是将用户信息加密后放到前台的(包含密码),在项目中user对象信息过于庞大,不能全部存入Cookie,Cookie对长度有一定的限制。
整合shiro-ehcache缓存
我们最后有两个问题,其中关于无权限页面的问题已经解决了,还有一个问题就是每次访问服务,都会去查询数据库,真实的情况是权限并不会经常出现变化,所以最好的办法就是做缓存处理,如果只是公司后台运营管理系统,可能只启动一个单节点就够了,这个时候我们就可以用到ehcache缓存。
前置工作 修改RoleService
增加delPermission,addPermission 方法
1 2 3 4 5 6 7 8 public interface RoleService { Set<Role> findRolesByUserId (Integer uid) ; void delPermission (int roleId, int permissionId) ; void addPermission (int i, int i1) ; }
RoleServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class RoleServiceImpl implements RoleService { @Autowired private RoleMapper roleMapper; @Override public Set<Role> findRolesByUserId (Integer uid) { return roleMapper.findRolesByUserId(uid); } @Override public void delPermission (int roleId, int permissionId) { roleMapper.delPermission(roleId,permissionId); } @Override public void addPermission (int roleId, int permissionId) { roleMapper.addPermission(roleId,permissionId); } }
修改RoleMapper 1 2 3 4 5 6 7 8 9 @Mapper public interface RoleMapper { Set<Role> findRolesByUserId (@Param("uid") Integer uid) ; void delPermission (@Param("roleId") Integer roleId, @Param("permissionId") Integer permissionId) ; void addPermission (@Param("roleId") Integer roleId, @Param("permissionId") Integer permissionId) ; }
RoleMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.demo.mapper.RoleMapper" > <select id ="findRolesByUserId" resultType ="com.demo.entity.Role" > SELECT r.* from sys_role r LEFT JOIN sys_user_role ur on r.id = ur.role_id where ur.uid = #{uid} </select > <insert id ="addPermission" > INSERT INTO sys_role_permission (role_id, permission_id) VALUES (#{roleId},#{permissionId}) </insert > <delete id ="delPermission" > DELETE FROM sys_role_permission WHERE role_id=#{roleId} and permission_id=#{permissionId} </delete > </mapper >
pom添加依赖 1 2 3 4 5 <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-ehcache</artifactId > <version > 1.5.2</version > </dependency >
修改ShiroConfig
在ShiroConfig中添加缓存管理器
添加shiro缓存管理器 1 2 3 4 5 6 7 8 9 10 11 12 @Bean public EhCacheManager ehCacheManager () { EhCacheManager cacheManager = new EhCacheManager (); cacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml" ); return cacheManager; }
添加Spring静态注入 1 2 3 4 5 6 7 8 9 10 11 12 13 @Bean public MethodInvokingFactoryBean getMethodInvokingFactoryBean () { MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean (); factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager" ); factoryBean.setArguments(new Object []{securityManager()}); return factoryBean; }
修改安全事务管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Bean(name = "securityManager") public SecurityManager securityManager () { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager (); securityManager.setRealm(shiroRealm()); securityManager.setRememberMeManager(rememberMeManager()); securityManager.setCacheManager(ehCacheManager()); return securityManager; }
重写shiroRealm方法
重写shiroRealm开启Ehcache缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean public ShiroRealm shiroRealm () { ShiroRealm shiroRealm = new ShiroRealm (); shiroRealm.setCachingEnabled(true ); shiroRealm.setAuthenticationCachingEnabled(true ); shiroRealm.setAuthenticationCacheName("authenticationCache" ); shiroRealm.setAuthorizationCachingEnabled(true ); shiroRealm.setAuthorizationCacheName("authorizationCache" ); return shiroRealm; }
配置ehcache配置文件
在config下添加ehcache-shiro.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 <?xml version="1.0" encoding="UTF-8" ?> <ehcache name ="es" > <diskStore path ="java.io.tmpdir" /> <defaultCache maxElementsInMemory ="10000" eternal ="false" timeToIdleSeconds ="0" timeToLiveSeconds ="0" overflowToDisk ="false" diskPersistent ="false" diskExpiryThreadIntervalSeconds ="120" /> <cache name ="authorizationCache" maxEntriesLocalHeap ="2000" eternal ="false" timeToIdleSeconds ="0" timeToLiveSeconds ="0" overflowToDisk ="false" statistics ="true" > </cache > <cache name ="authenticationCache" maxEntriesLocalHeap ="2000" eternal ="false" timeToIdleSeconds ="0" timeToLiveSeconds ="0" overflowToDisk ="false" statistics ="true" > </cache > </ehcache >
在ShiroRealm的doGetAuthorizationInfo方法中添加日志,或者直接看控制台sql打印也行。 启动项目,使用admin登录,访问http://localhost:8080/userInfo/view 第一次访问,查看控制台,有sql查询,再次访问将不再打印查询数据库的日志,证明缓存生效了。上面的缓存配置时间配置为永久,请根据需求自己更改值来进行测试。 关于在ShiroRealm中对 用户信息 角色信息 权限信息的查询,如果需要添加缓存 请自行处理。
缓存更新 如果用户的权限发生改变怎么办 上面已经启用了缓存,第一次请求走数据库查询,后续请求将直接查询ehcache缓存,假如这个时候在权限控制台分配了某个权限给某个角色,那么拥有这个角色的所有用户在下次请求之前都需要从数据库查询最新的权限信息。下面开始进行在权限发生改变时,该如何做:
清理缓存
在ShiroRealm类中添加以下方法(清理缓存)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class ShiroRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private PermissionMapper permissionMapper; ... @Override public void clearCachedAuthorizationInfo (PrincipalCollection principals) { super .clearCachedAuthorizationInfo(principals); } @Override public void clearCachedAuthenticationInfo (PrincipalCollection principals) { super .clearCachedAuthenticationInfo(principals); } @Override public void clearCache (PrincipalCollection principals) { super .clearCache(principals); } public void clearAllCachedAuthorizationInfo () { getAuthorizationCache().clear(); } public void clearAllCachedAuthenticationInfo () { getAuthenticationCache().clear(); } public void clearAllCache () { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } }
修改UserController
在UserController中添加下面两个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @RestController @RequestMapping("userInfo") public class UserController { @Autowired private UserService userService; @Autowired private RoleService roleService; ..... @RequestMapping(value = "/addPermission",method = RequestMethod.GET) @ResponseBody public String addPermission (Model model) { roleService.addPermission(1 ,3 ); DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager)SecurityUtils.getSecurityManager(); ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next(); shiroRealm.clearAllCache(); return "给admin用户添加 userInfo:del 权限成功" ; } @RequestMapping(value = "/delPermission",method = RequestMethod.GET) @ResponseBody public String delPermission (Model model) { roleService.delPermission(1 ,3 ); DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager(); ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next(); shiroRealm.clearAllCache(); return "删除admin用户userInfo:del 权限成功" ; } }
注意:在添加权限 或者 删除权限之后 都有调用shiroRealm.clearAllCache();来清除所有的缓存。
测试步骤:
两个浏览器: 一个谷歌浏览器登录 admin账户,另一个360浏览器登录 test账户,每个账户登录跳转到idnex页面之后 ,刷新两下,发现之后无论怎么刷新都不再打印查询数据库的sql
在谷歌浏览器地址调用添加权限的方法http://localhost:8080/userInfo/addPermission 显示添加完成,然后再次访问http://localhost:8080/index 页面上已经显示刚添加的权限,查看日志发现走数据库查询了最新的权限信息
这个时候在360浏览器刷新http://localhost:8080/index 查看控制台日志发现test的用户也有走数据库查询权限信息
shiro会话管理 Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如Tomcat),不管是J2SE还是J2EE环境都可以使用,提供了会话管理,会话事件监听,会话存储/持久化,容器无关的集群,失效/过期支持,对Web的透明支持,SSO单点登录的支持等特性。即直接使用 Shiro 的会话管理可以直接替换如 Web 容器的会话管理。
shiro中的session特性
基于POJO/J2SE:shiro中session相关的类都是基于接口实现的简单的java对象(POJO),兼容所有java对象的配置方式,扩展也更方便,完全可以定制自己的会话管理功能 。
简单灵活的会话存储/持久化:因为shiro中的session对象是基于简单的java对象的,所以你可以将session存储在任何地方,例如,文件,各种数据库,内存中等。
容器无关的集群功能:shiro中的session可以很容易的集成第三方的缓存产品完成集群的功能。例如,Ehcache + Terracotta, Coherence, GigaSpaces等。你可以很容易的实现会话集群而无需关注底层的容器实现。
异构客户端的访问:可以实现web中的session和非web项目中的session共享。
会话事件监听:提供对对session整个生命周期的监听。
保存主机地址:在会话开始session会存用户的ip地址和主机名,以此可以判断用户的位置。
会话失效/过期的支持:用户长时间处于不活跃状态可以使会话过期,调用touch()方法,可以主动更新最后访问时间,让会话处于活跃状态。
透明的Web支持:shiro全面支持Servlet 2.5中的session规范。这意味着你可以将你现有的web程序改为shiro会话,而无需修改代码。
单点登录的支持:shiro session基于普通java对象,使得它更容易存储和共享,可以实现跨应用程序共享。可以根据共享的会话,来保证认证状态到另一个程序。从而实现单点登录。
会话相关API 获取Session的方式 1 2 Subject subject = SecurityUtils.getSubject();Session session = subject.getSession();
与web中的 HttpServletRequest.getSession(boolean create) 类似! Subject.getSession(true)。即如果当前没有创建session对象会创建一个; Subject.getSession(false),如果当前没有创建session对象则返回null。
Session相关API
返回值
方法名
描述
Object
getAttribute(Object key)
根据key标识返回绑定到session的对象
Collection
getAttributeKeys()
获取在session中存储的所有的key
String
getHost()
获取当前主机ip地址,如果未知,返回null
Serializable
getId()
获取session的唯一id
Date
getLastAccessTime()
获取最后的访问时间
Date
getStartTimestamp()
获取session的启动时间
long
getTimeout()
获取session失效时间,单位毫秒
void
setTimeout(long maxIdleTimeInMillis)
设置session的失效时间
Object
removeAttribute(Object key)
通过key移除session中绑定的对象
void
setAttribute(Object key, Object value)
设置session会话属性
void
stop()
销毁会话
void
touch()
更新会话最后访问时间
会话管理器
会话管理器管理着应用中所有Subject的会话的创建、维护、删除、失效、验证等工作。是Shiro的核心组件,顶层组件SecurityManager直接继承了SessionManager,且提供了SessionsSecurityManager实现直接把会话管理委托给相应的SessionManager,DefaultSecurityManager及DefaultWebSecurityManager默认SecurityManager都继承了SessionsSecurityManager。
SecurityManager提供了如下接口:
另外用于Web环境的WebSessionManager又提供了如下接口:
boolean isServletContainerSessions();//是否使用Servlet容器的会话
Shiro还提供了ValidatingSessionManager用于验资并过期会话:
void validateSessions();//验证所有会话是否过期
Shiro提供了三个默认实现
DefaultSessionManager DefaultSecurityManager使用的默认实现,用于JavaSE环境;
ServletContainerSessionManager DefaultWebSecurityManager使用的默认实现,用于Web环境,其直接使用Servlet容器的会话;
DefaultWebSessionManager 用于Web环境的实现,可以替代ServletContainerSessionManager,自己维护着会话,直接废弃了Servlet容器的会话管理。
shiro配置会话管理配置 SessionListener
创建session监听类,实现SessionListener接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class ShiroSessionListener implements SessionListener { private final AtomicInteger sessionCount = new AtomicInteger (0 ); @Override public void onStart (Session session) { sessionCount.incrementAndGet(); } @Override public void onStop (Session session) { sessionCount.decrementAndGet(); } @Override public void onExpiration (Session session) { sessionCount.decrementAndGet(); } public AtomicInteger getSessionCount () { return sessionCount; } }
配置ShiroConfig 配置session监听 1 2 3 4 5 6 7 8 9 10 @Bean("sessionListener") public ShiroSessionListener sessionListener () { ShiroSessionListener sessionListener = new ShiroSessionListener (); return sessionListener; }
配置会话ID生成器 1 2 3 4 5 6 7 8 9 @Bean public SessionIdGenerator sessionIdGenerator () { return new JavaUuidSessionIdGenerator (); }
配置sessionDAO
SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
MemorySessionDAO 直接在内存中进行会话维护
EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Bean public SessionDAO sessionDAO () { EnterpriseCacheSessionDAO enterpriseCacheSessionDAO = new EnterpriseCacheSessionDAO (); enterpriseCacheSessionDAO.setCacheManager(ehCacheManager()); enterpriseCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache" ); enterpriseCacheSessionDAO.setSessionIdGenerator(sessionIdGenerator()); return enterpriseCacheSessionDAO; }
配置sessionId的Cookie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Bean("sessionIdCookie") public SimpleCookie sessionIdCookie () { SimpleCookie simpleCookie = new SimpleCookie ("session_id" ); simpleCookie.setHttpOnly(true ); simpleCookie.setPath("/" ); simpleCookie.setMaxAge(-1 ); return simpleCookie; }
配置session管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Bean("sessionManager") public SessionManager sessionManager () { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager (); Collection<SessionListener> listeners = new ArrayList <SessionListener>(); listeners.add(sessionListener()); sessionManager.setSessionListeners(listeners); sessionManager.setSessionIdCookie(sessionIdCookie()); sessionManager.setSessionDAO(sessionDAO()); sessionManager.setCacheManager(ehCacheManager()); sessionManager.setGlobalSessionTimeout(1800000 ); sessionManager.setDeleteInvalidSessions(true ); sessionManager.setSessionValidationSchedulerEnabled(true ); sessionManager.setSessionValidationInterval(3600000 ); sessionManager.setSessionIdUrlRewritingEnabled(false ); return sessionManager; }
注意:这里的SessionIdCookie 是新建的一个SimpleCookie对象,不是之前整合记住我的那个rememberMeCookie 如果配错了,就会出现session经典问题:每次请求都是一个新的session 并且后台报以下异常,解析的时候报错.因为记住我cookie是加密的用户信息,所以报解密错误
1 org.apache.shiro.crypto.CryptoException: Unable to execute 'doFinal' with cipher instance [javax.crypto.Cipher@461df537].
配置清理孤立session
以上整合会话管理,还有一个问题: 如果用户如果不点注销,直接关闭浏览器,不能够进行session的清空处理,所以为了防止这样的问题,还需要增加有一个会话的验证调度。
1 2 3 4 5 6 7 8 9 10 sessionManager.setGlobalSessionTimeout(1800000 ); sessionManager.setDeleteInvalidSessions(true ); sessionManager.setSessionValidationSchedulerEnabled(true ); sessionManager.setSessionValidationInterval(3600000 );
取消URL的JSESSIONID
shiro取消url上面的JSESSIONID
1 2 sessionManager.setSessionIdUrlRewritingEnabled(false );
修改SecurityManager
将session管理器交给SecurityManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Bean(name = "securityManager") public SecurityManager securityManager () { ... securityManager.setSessionManager(sessionManager()); return securityManager; }
配置Ehcache缓存
ehcache-shiro.xml添加session缓存属性
1 2 3 4 5 6 7 8 9 <cache name ="shiro-activeSessionCache" maxEntriesLocalHeap ="2000" eternal ="false" timeToIdleSeconds ="0" timeToLiveSeconds ="0" overflowToDisk ="false" statistics ="true" > </cache >
注意:
这里一定要注意缓存的设置过期时间,还有setGlobalSessionTimeout
的值,任一个时间设置的比较短,session就会从ehcache中清除,到时候就会报There is no session with id [2e9e317f-7575-4bb0-98c4-3e6e5d2578f5]
启动测试
配置完成之后启动测试,登陆的时候点击 rememberMe 查看cookie 可以看到一个sessionId 和 一个记住我cookie
在线人数以及并发登录人数控制 项目中有时候会遇到统计当前在线人数的需求,也有这种情况当A 用户在邯郸地区登录 ,然后A用户在北京地区再登录 ,要踢出邯郸登录的状态。如果用户在北京重新登录,那么又要踢出邯郸的用户,这样反复。 这样保证了一个帐号只能同时一个人使用。那么下面来讲解一下 Shiro 怎么实现在线人数统计 以及 并发人数控制这个功能。
并发人数控制 使用的技术其实是 shiro的自定义filter,在 springboot整合shiro -快速入门 中 我们已经了解到,在shiroConfig的ShiroFilterFactoryBean中使用的过滤规则,如:anon ,authc,user等本质上是通过调用各自对应的filter方式集成的,也就是说,它是遵循过滤器链规则的。
创建KickoutSessionControlFilter
写一个KickoutSessionControlFilter类继承AccessControlFilter类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 public class KickoutSessionControlFilter extends AccessControlFilter { private String kickoutUrl; private boolean kickoutAfter = false ; private int maxSession = 1 ; private SessionManager sessionManager; private Cache<String, Deque<Serializable>> cache; public void setKickoutUrl (String kickoutUrl) { this .kickoutUrl = kickoutUrl; } public void setKickoutAfter (boolean kickoutAfter) { this .kickoutAfter = kickoutAfter; } public void setMaxSession (int maxSession) { this .maxSession = maxSession; } public void setSessionManager (SessionManager sessionManager) { this .sessionManager = sessionManager; } public void setCacheManager (CacheManager cacheManager) { this .cache = cacheManager.getCache("shiro-activeSessionCache" ); } @Override protected boolean isAccessAllowed (ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false ; } @Override protected boolean onAccessDenied (ServletRequest request, ServletResponse response) throws Exception { Subject subject = getSubject(request, response); if (!subject.isAuthenticated() && !subject.isRemembered()) { return true ; } Session session = subject.getSession(); String username = ((User) subject.getPrincipal()).getUsername(); Serializable sessionId = session.getId(); Deque<Serializable> deque = cache.get(username); if (deque == null ) { deque = new LinkedList <Serializable>(); cache.put(username, deque); } if (!deque.contains(sessionId) && session.getAttribute("kickout" ) == null ) { deque.push(sessionId); } while (deque.size() > maxSession) { Serializable kickoutSessionId = null ; if (kickoutAfter) { kickoutSessionId = deque.getFirst(); kickoutSessionId = deque.removeFirst(); } else { kickoutSessionId = deque.removeLast(); } try { Session kickoutSession = sessionManager.getSession(new DefaultSessionKey (kickoutSessionId)); if (kickoutSession != null ) { kickoutSession.setAttribute("kickout" , true ); } } catch (Exception e) { e.printStackTrace(); } } if (session.getAttribute("kickout" ) != null ) { try { subject.logout(); } catch (Exception e) { } WebUtils.issueRedirect(request, response, kickoutUrl); return false ; } return true ; } }
注意 :我们首先看一下 isAccessAllowed() 方法,在这个方法中,如果返回 true,则表示“通过”,走到下一个过滤器。如果没有下一个过滤器的话,表示具有了访问某个资源的权限。如果返回 false,则会调用 onAccessDenied 方法,去实现相应的当过滤不通过的时候执行的操作,例如检查用户是否已经登陆过,如果登陆过,根据自定义规则选择踢出前一个用户 还是 后一个用户。 onAccessDenied方法 返回 true 表示 自己处理完成,然后继续拦截器链执行。 只有当两者都返回false时,才会终止后面的filter执行。
shiroConfig相关配置 并发登录控制配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Bean public KickoutSessionControlFilter kickoutSessionControlFilter () { KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter (); kickoutSessionControlFilter.setSessionManager(sessionManager()); kickoutSessionControlFilter.setCacheManager(ehCacheManager()); kickoutSessionControlFilter.setKickoutAfter(false ); kickoutSessionControlFilter.setMaxSession(1 ); kickoutSessionControlFilter.setKickoutUrl("/login?kickout=1" ); return kickoutSessionControlFilter; }
修改shirFilter
修改shiroConfig中shirFilter中配置KickoutSessionControlFilter 并修改过滤规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Bean(name = "shirFilter") public ShiroFilterFactoryBean shiroFilter (@Qualifier("securityManager") SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean (); ...... LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap <>(); filtersMap.put("kickout" , kickoutSessionControlFilter()); shiroFilterFactoryBean.setFilters(filtersMap); LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap <>(); filterChainDefinitionMap.put("/login" , "kickout,anon" ); ...... filterChainDefinitionMap.put("/**" , "kickout,user" ); return shiroFilterFactoryBean; }
解释:
filterChainDefinitionMap.put("/**", "kickout,user");
表示 访问/**下的资源 首先要通过 kickout 后面的filter,然后再通过user后面对应的filter才可以访问。
修改login.html
修改login.html添加踢出登录的信息提示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <!DOCTYPE html > <html xmlns ="http://www.w3.org/1999/xhtml" xmlns:th ="http://www.thymeleaf.org" xmlns:sec ="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3" xmlns:shiro ="http://www.pollix.at/thymeleaf/shiro" > <head > <meta charset ="UTF-8" /> <title > Insert title here</title > </head > <body > <h1 > 欢迎登录</h1 > <h1 th:if ="${msg != null }" th:text ="${msg}" style ="color: red" > </h1 > <form action ="/login" method ="post" > 用户名:<input type ="text" name ="username" /> <br /> 密 码:<input type ="password" name ="password" /> <br /> <input type ="checkbox" name ="rememberMe" /> 记住我<br /> <input type ="submit" value ="提交" /> </form > </body > <script src ="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.0/jquery.js" > </script > <script type ="text/javascript" > $(function ( ) { var href = location.href ; if (href.indexOf ("kickout" ) > 0 ) { alert ("您的账号在另一台设备上登录,如非本人操作,请立即修改密码!" ); } }); </script > </html >
测试
先通过admin登录,然后换一个浏览器 再次登录admin,然后再回到原来的 浏览器刷新页面,会弹出框提示已下线
统计在线人数 springboot整合shiro-session管理 博客中,我们有配置过一个监听类 ,在该类中有统计session创建个数,我们也就用session的个数来统计在线的人数,但是这个统计人数是不准确的,存在这样一种情况,用户登录之后,强制退出浏览器,再次打开浏览器重新登录,在线人数一直在增加。暂时也没有想到特别好的方案。
修改UserController
添加在线人数的返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @RestController @RequestMapping("userInfo") public class UserController { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private ShiroSessionListener shiroSessionListener; ..... @RequestMapping(value = "/userCount", method = RequestMethod.GET) @ResponseBody public String getCurrentUserCount () { int userCount = shiroSessionListener.getSessionCount(); return "当前在线人数" + userCount + "人" ; } }
测试
登录多个账户访问 http://localhost:8080/userInfo/userCount
登录失败次数限制 如何限制用户登录尝试次数,防止坏人多次尝试,恶意暴力破解密码的情况出现,要限制用户登录尝试次数,必然要对用户名密码验证失败做记录,Shiro中用户名密码的验证交给了CredentialsMatcher 所以在CredentialsMatcher里面检查,记录登录次数是最简单的做法。当登录失败次数达到限制,修改数据库中的状态字段,并返回前台错误信息。 因为之前的博客都是用的明文,这里就不对密码进行加密了,如果有需要加密,将自定义密码比较器从SimpleCredentialsMatcher改为HashedCredentialsMatcher 然后将对应的配置项打开就可以。
限制登录次数
自定义RetryLimitHashedCredentialsMatcher继承SimpleCredentialsMatcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public class RetryLimitHashedCredentialsMatcher extends SimpleCredentialsMatcher { private static final Logger logger = LoggerFactory.getLogger(RetryLimitHashedCredentialsMatcher.class); private UserServiceAgent userServiceAgent = new UserServiceAgent (); private Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher (CacheManager cacheManager) { passwordRetryCache = cacheManager.getCache("passwordRetryCache" ); } @Override public boolean doCredentialsMatch (AuthenticationToken token, AuthenticationInfo info) { String username = (String) token.getPrincipal(); AtomicInteger retryCount = passwordRetryCache.get(username); if (retryCount == null ) { retryCount = new AtomicInteger (0 ); passwordRetryCache.put(username, retryCount); } if (retryCount.incrementAndGet() > 5 ) { User user = userServiceAgent.findByUserName(username); if (user != null && "0" .equals(user.getState())) { user.setState("1" ); userServiceAgent.update(user); } logger.info("锁定用户" + user.getUsername()); throw new LockedAccountException (); } boolean matches = super .doCredentialsMatch(token, info); if (matches) { passwordRetryCache.remove(username); } return matches; } public void unlockAccount (String username) { User user = userServiceAgent.findByUserName(username); if (user != null ) { user.setState("0" ); userServiceAgent.update(user); passwordRetryCache.remove(username); } } }
shiroConfig配置 配置密码比较器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean("credentialsMatcher") public RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher () { RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher (ehCacheManager()); return retryLimitHashedCredentialsMatcher; }
shiroRealm中配置密码比较器 1 2 3 4 5 6 7 8 @Bean public ShiroRealm shiroRealm () { ShiroRealm shiroRealm = new ShiroRealm (); ..... shiroRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher()); return shiroRealm; }
配置缓存
在ehcache-shiro.xml添加缓存项
1 2 3 4 5 6 7 8 9 10 11 12 13 <cache name ="passwordRetryCache" maxEntriesLocalHeap ="2000" eternal ="false" timeToIdleSeconds ="0" timeToLiveSeconds ="300" overflowToDisk ="false" statistics ="true" > </cache >
修改LoginController
在LoginController中添加解除admin用户限制方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Controller public class LoginController { @Autowired private RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher; ..... @RequestMapping("/unlockAccount") public String unlockAccount (Model model) { model.addAttribute("msg" , "用户解锁成功" ); retryLimitHashedCredentialsMatcher.unlockAccount("admin" ); return "login" ; } }
注意:
为了方便测试,记得将 unlockAccount 权限改为 任何人可访问。
修改login.html
在login.html页面 添加 解锁admin用户的按钮
1 <a href ="/unlockAccount" > 解锁admin用户</a > </button >
测试
连续五次输错密码
实现验证码认证 验证码是有效防止暴力破解的一种手段,常用做法是在服务端产生一串随机字符串与当前用户会话关联(我们通常说的放入 Session),然后向终端用户展现一张经过“扰乱”的图片,只有当用户输入的内容与服务端产生的内容相同时才允许进行下一步操作.
shiro添加验证码 说的是shiro添加验证码,其实不如说是web服务登录功能添加验证码,因为这个功能和shiro完全没有任何关系,网上大都是 实现 FormAuthenticationFilter 然后 在filter中进行验证码校验,或者是 在shiroRealm中进行验证码校验,一大堆的代码要写,感觉很麻烦,下面进行最简单的代码实现验证码功能。直接在 login方法内,判断验证码是否正确。
创建验证码类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 package com.demo.utils;import java.awt.*;import java.awt.image.BufferedImage;import java.util.Random;public class CaptchaUtil { private static final String RANDOM_STRS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ; private static final String FONT_NAME = "Fixedsys" ; private static final int FONT_SIZE = 18 ; private Random random = new Random (); private int width = 80 ; private int height = 25 ; private int lineNum = 50 ; private int strNum = 4 ; public BufferedImage genRandomCodeImage (StringBuffer randomCode) { BufferedImage image = new BufferedImage (width, height, BufferedImage.TYPE_INT_BGR); Graphics g = image.getGraphics(); g.setColor(getRandColor(200 , 250 )); g.fillRect(0 , 0 , width, height); g.setColor(getRandColor(110 , 120 )); for (int i = 0 ; i <= lineNum; i++) { drowLine(g); } g.setFont(new Font (FONT_NAME, Font.ROMAN_BASELINE, FONT_SIZE)); for (int i = 1 ; i <= strNum; i++) { randomCode.append(drowString(g, i)); } g.dispose(); return image; } private 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 String drowString (Graphics g, int i) { g.setColor(new Color (random.nextInt(101 ), random.nextInt(111 ), random .nextInt(121 ))); String rand = String.valueOf(getRandomString(random.nextInt(RANDOM_STRS .length()))); g.translate(random.nextInt(3 ), random.nextInt(3 )); g.drawString(rand, 13 * i, 16 ); return rand; } private void drowLine (Graphics g) { int x = random.nextInt(width); int y = random.nextInt(height); int x0 = random.nextInt(16 ); int y0 = random.nextInt(16 ); g.drawLine(x, y, x + x0, y + y0); } private String getRandomString (int num) { return String.valueOf(RANDOM_STRS.charAt(num)); } }
获取验证码Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Controller public class CaptchaController { public static final String KEY_CAPTCHA = "KEY_CAPTCHA" ; @RequestMapping("/Captcha.jpg") public void getCaptcha (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("image/jpeg" ); response.setHeader("Pragma" , "No-cache" ); response.setHeader("Cache-Control" , "no-cache" ); response.setDateHeader("Expire" , 0 ); try { HttpSession session = request.getSession(); CaptchaUtil tool = new CaptchaUtil (); StringBuffer code = new StringBuffer (); BufferedImage image = tool.genRandomCodeImage(code); session.removeAttribute(KEY_CAPTCHA); session.setAttribute(KEY_CAPTCHA, code.toString()); ImageIO.write(image, "JPEG" , response.getOutputStream()); } catch (Exception e) { e.printStackTrace(); } } }
修改shiroConfig
在shiroConfig中对 获取验证码的功能开放权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Bean(name = "shirFilter") public ShiroFilterFactoryBean shiroFilter (@Qualifier("securityManager") SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean (); ..... LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap <>(); filterChainDefinitionMap.put("/Captcha.jpg" ,"anon" ); filterChainDefinitionMap.put("/login" , "kickout,anon" ); filterChainDefinitionMap.put("/" , "anon" ); ..... return shiroFilterFactoryBean; }
修改LoginController
在login方法内添加验证码校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RequestMapping(value = "/login", method = RequestMethod.POST) public String loginUser (HttpServletRequest request, String username, String password, boolean rememberMe, String captcha, Model model, HttpSession session) { String sessionCaptcha = (String) SecurityUtils.getSubject().getSession().getAttribute(CaptchaController.KEY_CAPTCHA); if (null == captcha || !captcha.equalsIgnoreCase(sessionCaptcha)) { model.addAttribute("msg" , "验证码错误!" ); return "login" ; } UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken (username, password, rememberMe); ..... }
修改login.html
登录页面添加验证码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!DOCTYPE html > <html xmlns ="http://www.w3.org/1999/xhtml" xmlns:th ="http://www.thymeleaf.org" xmlns:sec ="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3" xmlns:shiro ="http://www.pollix.at/thymeleaf/shiro" > <head > <meta charset ="UTF-8" /> <title > Insert title here</title > </head > <body > <h1 > 欢迎登录</h1 > <h1 th:if ="${msg != null }" th:text ="${msg}" style ="color: red" > </h1 > <form action ="/login" method ="post" > 用户名:<input type ="text" name ="username" /> <br /> 密 码:<input type ="password" name ="password" /> <br /> 验证码:<input type ="text" name ="captcha" /> <img alt ="验证码" th:src ="@{/Captcha.jpg}" title ="点击更换" id ="captcha_img" /> (看不清<a id ="refreshCaptcha" href ="javascript:void(0)" > 换一张</a > )<br /> <input type ="checkbox" name ="rememberMe" /> 记住我<br /> <input type ="submit" value ="提交" /> <a href ="/unlockAccount" > 解锁admin用户</a > </button > </form > </body > <script src ="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.0/jquery.js" > </script > <script type ="text/javascript" > $(function ( ) { var href = location.href ; if (href.indexOf ("kickout" ) > 0 ) { alert ("您的账号在另一台设备上登录,如非本人操作,请立即修改密码!" ); } $("#refreshCaptcha,#captcha_img" ).click (function ( ) { $("#captcha_img" ).attr ("src" , "/Captcha.jpg?id=" + new Date () + Math .floor (Math .random () * 24 )); }); }); </script > </html >
测试
故意输错验证码
整合redis作为缓存 本来的整合过程是顺着博客的顺序来的,越往下,集成的越多,由于之前是使用ehcache缓存,现在改为redis,限制登录人数 以及 限制登录次数等 都需要改动,本篇为了简单,目前先将这两个功能下线,配置暂时是注销的,原类保存,在下篇博客中改。 还有之前是使用SessionListener监听session创建来统计在线人数,在本篇中也将改为统计redis中的key数目。 如果是单机,使用ehcache是最快的,项目一般都不是单节点,为了方便之后使用sso单点登录,以及多节点部署,所以使用shiro整合redis。
整合Redis
参考整合Redis
删除Ehcache POM文件修改
去掉shiro-ehcache依赖
删除Ehcache配置
删除ehcache-shiro.xml
创建代理类
因为通过@Bean的方式初始化一些组件会产生业务Bean的污染,需要进行采用代理的方式屏蔽依赖污染。
ApplicationUtils
Application相关工具类,使用getBean的方式来获取实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class ApplicationUtils { private static ApplicationContext appContext = null ; public static void setApplicationContext (ApplicationContext applicationContext) throws BeansException { appContext = applicationContext; } public static Object getBean (String beanId) { Object bean = null ; if (null != appContext) { bean = appContext.getBean(beanId); } return bean; } public static <T> T getBean (Class clazz) { Object bean = null ; if (null != appContext) { bean = appContext.getBean(clazz); } return (T) bean; } public static UserMapper getUserMapper () { return ApplicationUtils.getBean(UserMapper.class); } public static RoleMapper getRoleMapper () { return ApplicationUtils.getBean(RoleMapper.class); } public static PermissionMapper getPermissionMapper () { return ApplicationUtils.getBean(PermissionMapper.class); } public static RedisManagerImpl getRedisManager () { return ApplicationUtils.getBean(RedisManagerImpl.class); } }
ApplicationContexConfig
实现applicationContext的启动注册
1 2 3 4 5 6 7 @Component public class ApplicationContexConfig implements ApplicationContextAware { @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { ApplicationUtils.setApplicationContext(applicationContext); } }
UserServiceAgent
用户操作的代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class UserServiceAgent implements UserService { @Override public User findByUserName (String userName) { return ApplicationUtils.getUserMapper().findByUserName(userName); } @Override public int insert (User user) { return ApplicationUtils.getUserMapper().insert(user); } @Override public int update (User user) { return ApplicationUtils.getUserMapper().update(user); } @Override public int del (String username) { return ApplicationUtils.getUserMapper().del(username); } }
RoleServiceAgent
角色代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class RoleServiceAgent implements RoleService { @Override public Set<Role> findRolesByUserId (Integer uid) { return ApplicationUtils.getRoleMapper().findRolesByUserId(uid); } @Override public void delPermission (int roleId, int permissionId) { ApplicationUtils.getRoleMapper().delPermission(roleId, permissionId); } @Override public void addPermission (int roleId, int permissionId) { ApplicationUtils.getRoleMapper().addPermission(roleId, permissionId); } }
PermissionServiceAgent
权限代理类
1 2 3 4 5 6 7 public class PermissionServiceAgent implements PermissionService { @Override public Set<Permission> findPermissionsByRoleId (Set<Role> roles) { return ApplicationUtils.getPermissionMapper().findPermissionsByRoleId(roles); } }
RedisManagerAgent
为了解决初始化bean 需要依赖redis 造成的 bean过早被初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 public class RedisManagerAgent implements RedisManager { public void expire (String key, long time) { ApplicationUtils.getRedisManager().expire(key, time); } public Boolean hasKey (String key) { return ApplicationUtils.getRedisManager().hasKey(key); } @SuppressWarnings("unchecked") public void del (String... key) { ApplicationUtils.getRedisManager().del(key); } public void del (Collection keys) { ApplicationUtils.getRedisManager().del(keys); } public Object get (String key) { return ApplicationUtils.getRedisManager().get(key); } public void set (String key, Object value) { ApplicationUtils.getRedisManager().set(key, value); } public void set (String key, Object value, long time) { ApplicationUtils.getRedisManager().set(key, value, time); } public Set<String> scan (String key) { return ApplicationUtils.getRedisManager().scan(key); } public Long scanSize (String key) { return ApplicationUtils.getRedisManager().scanSize(key); } }
相关依赖类 ShiroSession
由于SimpleSession lastAccessTime更改后也会调用SessionDao update方法,更新的字段只有LastAccessTime(最后一次访问时间),由于会话失效是由Redis数据过期实现的,这个字段意义不大,为了减少对Redis的访问,降低网络压力,实现自己的Session,在SimpleSession上套一层,增加一个标识位,如果Session除lastAccessTime意外其它字段修改,就标识一下,只有标识为修改的才可以通过doUpdate访问Redis,否则直接返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 public class ShiroSession extends SimpleSession implements Serializable { private boolean isChanged = false ; public ShiroSession () { super (); this .setChanged(true ); } public ShiroSession (String host) { super (host); this .setChanged(true ); } @Override public void setId (Serializable id) { super .setId(id); this .setChanged(true ); } @Override public void setStopTimestamp (Date stopTimestamp) { super .setStopTimestamp(stopTimestamp); this .setChanged(true ); } @Override public void setExpired (boolean expired) { super .setExpired(expired); this .setChanged(true ); } @Override public void setTimeout (long timeout) { super .setTimeout(timeout); this .setChanged(true ); } @Override public void setHost (String host) { super .setHost(host); this .setChanged(true ); } @Override public void setAttributes (Map<Object, Object> attributes) { super .setAttributes(attributes); this .setChanged(true ); } @Override public void setAttribute (Object key, Object value) { super .setAttribute(key, value); this .setChanged(true ); } @Override public Object removeAttribute (Object key) { this .setChanged(true ); return super .removeAttribute(key); } @Override public void stop () { super .stop(); this .setChanged(true ); } @Override protected void expire () { this .stop(); this .setExpired(true ); } public boolean isChanged () { return isChanged; } public void setChanged (boolean isChanged) { this .isChanged = isChanged; } @Override public boolean equals (Object obj) { return super .equals(obj); } @Override protected boolean onEquals (SimpleSession ss) { return super .onEquals(ss); } @Override public int hashCode () { return super .hashCode(); } @Override public String toString () { return super .toString(); } }
SessionInMemory 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class SessionInMemory { private Session session; private Date createTime; public Session getSession () { return session; } public void setSession (Session session) { this .session = session; } public Date getCreateTime () { return createTime; } public void setCreateTime (Date createTime) { this .createTime = createTime; } }
ShiroSessionFactory 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class ShiroSessionFactory implements SessionFactory { private static final Logger logger = LoggerFactory.getLogger(ShiroSessionFactory.class); @Override public Session createSession (SessionContext initData) { ShiroSession session = new ShiroSession (); HttpServletRequest request = (HttpServletRequest)initData.get(DefaultWebSessionContext.class.getName() + ".SERVLET_REQUEST" ); session.setHost(getIpAddress(request)); return session; } public static String getIpAddress (HttpServletRequest request) { String localIP = "127.0.0.1" ; String ip = request.getHeader("x-forwarded-for" ); if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP" ); } if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP" ); } if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown" .equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
ShiroSessionListener
创建Session监听器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class ShiroSessionListener implements SessionListener { private final AtomicInteger sessionCount = new AtomicInteger (0 ); @Override public void onStart (Session session) { sessionCount.incrementAndGet(); } @Override public void onStop (Session session) { sessionCount.decrementAndGet(); } @Override public void onExpiration (Session session) { sessionCount.decrementAndGet(); } public int getSessionCount () { return sessionCount.get(); } }
重写密码比较器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public class RetryLimitHashedCredentialsMatcher extends SimpleCredentialsMatcher { private static final Logger logger = LoggerFactory.getLogger(RetryLimitHashedCredentialsMatcher.class); public static final String DEFAULT_RETRYLIMIT_CACHE_KEY_PREFIX = "shiro:cache:retrylimit:" ; private String keyPrefix = DEFAULT_RETRYLIMIT_CACHE_KEY_PREFIX; private UserServiceAgent userServiceAgent = new UserServiceAgent (); private RedisManager redisManager; public void setRedisManager (RedisManager redisManager) { this .redisManager = redisManager; } private String getRedisKickoutKey (String username) { return this .keyPrefix + username; } @Override public boolean doCredentialsMatch (AuthenticationToken token, AuthenticationInfo info) { String username = (String) token.getPrincipal(); AtomicInteger retryCount = (AtomicInteger) redisManager.get(getRedisKickoutKey(username)); if (retryCount == null ) { retryCount = new AtomicInteger (0 ); } if (retryCount.incrementAndGet() > 5 ) { User user = userServiceAgent.findByUserName(username); if (user != null && "0" .equals(user.getState())) { user.setState("1" ); userServiceAgent.update(user); } logger.info("锁定用户" + user.getUsername()); throw new LockedAccountException (); } boolean matches = super .doCredentialsMatch(token, info); if (matches) { redisManager.del(getRedisKickoutKey(username)); } { redisManager.set(getRedisKickoutKey(username), retryCount); } return matches; } public void unlockAccount (String username) { User user = userServiceAgent.findByUserName(username); if (user != null ) { user.setState("0" ); userServiceAgent.update(user); redisManager.del(getRedisKickoutKey(username)); } } }
SerializeUtils 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 public class SerializeUtils implements RedisSerializer { private static Logger logger = LoggerFactory.getLogger(SerializeUtils.class); public static boolean isEmpty (byte [] data) { return (data == null || data.length == 0 ); } @Override public byte [] serialize(Object object) throws SerializationException { byte [] result = null ; if (object == null ) { return new byte [0 ]; } try ( ByteArrayOutputStream byteStream = new ByteArrayOutputStream (128 ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteStream) ) { if (!(object instanceof Serializable)) { throw new IllegalArgumentException (SerializeUtils.class.getSimpleName() + " requires a Serializable payload " + "but received an object of type [" + object.getClass().getName() + "]" ); } objectOutputStream.writeObject(object); objectOutputStream.flush(); result = byteStream.toByteArray(); } catch (Exception ex) { logger.error("Failed to serialize" , ex); } return result; } @Override public Object deserialize (byte [] bytes) throws SerializationException { Object result = null ; if (isEmpty(bytes)) { return null ; } try ( ByteArrayInputStream byteStream = new ByteArrayInputStream (bytes); ObjectInputStream objectInputStream = new ObjectInputStream (byteStream) ) { result = objectInputStream.readObject(); } catch (Exception e) { logger.error("Failed to deserialize" , e); } return result; } }
重写并发登录控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 public class KickoutSessionControlFilter extends AccessControlFilter { private String kickoutUrl; private boolean kickoutAfter = false ; private int maxSession = 1 ; private SessionManager sessionManager; private RedisManager redisManager; public static final String DEFAULT_KICKOUT_CACHE_KEY_PREFIX = "shiro:cache:kickout:" ; private String keyPrefix = DEFAULT_KICKOUT_CACHE_KEY_PREFIX; public void setKickoutUrl (String kickoutUrl) { this .kickoutUrl = kickoutUrl; } public void setKickoutAfter (boolean kickoutAfter) { this .kickoutAfter = kickoutAfter; } public void setMaxSession (int maxSession) { this .maxSession = maxSession; } public void setSessionManager (SessionManager sessionManager) { this .sessionManager = sessionManager; } public void setRedisManager (RedisManager redisManager) { this .redisManager = redisManager; } public String getKeyPrefix () { return keyPrefix; } public void setKeyPrefix (String keyPrefix) { this .keyPrefix = keyPrefix; } private String getRedisKickoutKey (String username) { return this .keyPrefix + username; } @Override protected boolean isAccessAllowed (ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false ; } @Override protected boolean onAccessDenied (ServletRequest request, ServletResponse response) throws Exception { Subject subject = getSubject(request, response); if (!subject.isAuthenticated() && !subject.isRemembered()) { return true ; } HttpServletRequest httpServletRequest = (HttpServletRequest) request; String path = httpServletRequest.getServletPath(); if (isStaticFile(path)) { return true ; } Session session = subject.getSession(); String username = ((User) subject.getPrincipal()).getUsername(); Serializable sessionId = session.getId(); Deque<Serializable> deque = (Deque<Serializable>) redisManager.get(getRedisKickoutKey(username)); if (deque == null || deque.size() == 0 ) { deque = new LinkedList <Serializable>(); } if (!deque.contains(sessionId) && session.getAttribute("kickout" ) == null ) { deque.push(sessionId); } while (deque.size() > maxSession) { Serializable kickoutSessionId = null ; if (kickoutAfter) { kickoutSessionId = deque.getFirst(); kickoutSessionId = deque.removeFirst(); } else { kickoutSessionId = deque.removeLast(); } try { Session kickoutSession = sessionManager.getSession(new DefaultSessionKey (kickoutSessionId)); if (kickoutSession != null ) { kickoutSession.setAttribute("kickout" , true ); } } catch (Exception e) { e.printStackTrace(); } } redisManager.set(getRedisKickoutKey(username), deque); if (session.getAttribute("kickout" ) != null ) { try { subject.logout(); } catch (Exception e) { } WebUtils.issueRedirect(request, response, kickoutUrl); return false ; } return true ; } private boolean isStaticFile (String path) { return true ; } }
配置Redis管理器 Redis 配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration public class RedisConfiguration extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate (RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate <>(); SerializeUtils serializeUtils = new SerializeUtils (); template.setValueSerializer(serializeUtils); template.setHashValueSerializer(serializeUtils); template.setKeySerializer(new StringRedisSerializer ()); template.setHashKeySerializer(new StringRedisSerializer ()); template.setConnectionFactory(factory); return template; } }
创建RedisManager接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 public interface RedisManager { public void expire (String key, long time) ; public Boolean hasKey (String key) ; public void del (String... key) ; public void del (Collection keys) ; public Object get (String key) ; public void set (String key, Object value) ; public void set (String key, Object value, long time) ; public Set<String> scan (String key) ; public Long scanSize (String key) ; }
创建RedisManagerImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 @Component("redisManager") public class RedisManagerImpl implements RedisManager { @Autowired private RedisTemplate<String, Object> redisTemplate; public void expire (String key, long time) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } public Boolean hasKey (String key) { return redisTemplate.hasKey(key); } @SuppressWarnings("unchecked") public void del (String... key) { if (key != null && key.length > 0 ) { if (key.length == 1 ) { redisTemplate.delete(key[0 ]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } public void del (Collection keys) { redisTemplate.delete(keys); } public Object get (String key) { return redisTemplate.opsForValue().get(key); } public void set (String key, Object value) { redisTemplate.opsForValue().set(key, value); } public void set (String key, Object value, long time) { if (time > 0 ) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } } public Set<String> scan (String key) { Set<String> execute = this .redisTemplate.execute(new RedisCallback <Set<String>>() { @Override public Set<String> doInRedis (RedisConnection connection) throws DataAccessException { Set<String> binaryKeys = new HashSet <>(); Cursor<byte []> cursor = connection.scan(new ScanOptions .ScanOptionsBuilder().match(key).count(1000 ).build()); while (cursor.hasNext()) { binaryKeys.add(new String (cursor.next())); } return binaryKeys; } }); return execute; } public Long scanSize (String key) { long dbSize = this .redisTemplate.execute(new RedisCallback <Long>() { @Override public Long doInRedis (RedisConnection connection) throws DataAccessException { long count = 0L ; Cursor<byte []> cursor = connection.scan(ScanOptions.scanOptions().match(key).count(1000 ).build()); while (cursor.hasNext()) { cursor.next(); count++; } return count; } }); return dbSize; } }
创建创建Redis缓存管理器 创建RedisCache 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 public class RedisCache <K, V> implements Cache <K, V> { private static Logger logger = LoggerFactory.getLogger(RedisCache.class); private RedisManager redisManager; private String keyPrefix = "" ; private int expire = 0 ; private String principalIdFieldName = RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME; public RedisCache (RedisManager redisManager, String prefix, int expire, String principalIdFieldName) { if (redisManager == null ) { throw new IllegalArgumentException ("redisManager cannot be null." ); } this .redisManager = redisManager; if (prefix != null && !"" .equals(prefix)) { this .keyPrefix = prefix; } if (expire != -1 ) { this .expire = expire; } if (principalIdFieldName != null && !"" .equals(principalIdFieldName)) { this .principalIdFieldName = principalIdFieldName; } } @Override public V get (K key) throws CacheException { logger.debug("get key [{}]" , key); if (key == null ) { return null ; } try { String redisCacheKey = getRedisCacheKey(key); Object rawValue = redisManager.get(redisCacheKey); if (rawValue == null ) { return null ; } V value = (V) rawValue; return value; } catch (Exception e) { throw new CacheException (e); } } @Override public V put (K key, V value) throws CacheException { logger.debug("put key [{}]" , key); if (key == null ) { logger.warn("Saving a null key is meaningless, return value directly without call Redis." ); return value; } try { String redisCacheKey = getRedisCacheKey(key); redisManager.set(redisCacheKey, value != null ? value : null , expire); return value; } catch (Exception e) { throw new CacheException (e); } } @Override public V remove (K key) throws CacheException { logger.debug("remove key [{}]" , key); if (key == null ) { return null ; } try { String redisCacheKey = getRedisCacheKey(key); Object rawValue = redisManager.get(redisCacheKey); V previous = (V) rawValue; redisManager.del(redisCacheKey); return previous; } catch (Exception e) { throw new CacheException (e); } } private String getRedisCacheKey (K key) { if (key == null ) { return null ; } return this .keyPrefix + getStringRedisKey(key); } private String getStringRedisKey (K key) { String redisKey; if (key instanceof PrincipalCollection) { redisKey = getRedisKeyFromPrincipalIdField((PrincipalCollection) key); } else { redisKey = key.toString(); } return redisKey; } private String getRedisKeyFromPrincipalIdField (PrincipalCollection key) { String redisKey; Object principalObject = key.getPrimaryPrincipal(); Method pincipalIdGetter = null ; Method[] methods = principalObject.getClass().getDeclaredMethods(); for (Method m : methods) { if (RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME.equals(this .principalIdFieldName) && ("getAuthCacheKey" .equals(m.getName()) || "getId" .equals(m.getName()))) { pincipalIdGetter = m; break ; } if (m.getName().equals("get" + this .principalIdFieldName.substring(0 , 1 ).toUpperCase() + this .principalIdFieldName.substring(1 ))) { pincipalIdGetter = m; break ; } } if (pincipalIdGetter == null ) { throw new PrincipalInstanceException (principalObject.getClass(), this .principalIdFieldName); } try { Object idObj = pincipalIdGetter.invoke(principalObject); if (idObj == null ) { throw new PrincipalIdNullException (principalObject.getClass(), this .principalIdFieldName); } redisKey = idObj.toString(); } catch (IllegalAccessException e) { throw new PrincipalInstanceException (principalObject.getClass(), this .principalIdFieldName, e); } catch (InvocationTargetException e) { throw new PrincipalInstanceException (principalObject.getClass(), this .principalIdFieldName, e); } return redisKey; } @Override public void clear () throws CacheException { logger.debug("clear cache" ); Set<String> keys = null ; try { keys = redisManager.scan(this .keyPrefix + "*" ); } catch (Exception e) { logger.error("get keys error" , e); } if (keys == null || keys.size() == 0 ) { return ; } for (String key : keys) { redisManager.del(key); } } @Override public int size () { Long longSize = 0L ; try { longSize = new Long (redisManager.scanSize(this .keyPrefix + "*" )); } catch (Exception e) { logger.error("get keys error" , e); } return longSize.intValue(); } @SuppressWarnings("unchecked") @Override public Set<K> keys () { Set<String> keys = null ; try { keys = redisManager.scan(this .keyPrefix + "*" ); } catch (Exception e) { logger.error("get keys error" , e); return Collections.emptySet(); } if (CollectionUtils.isEmpty(keys)) { return Collections.emptySet(); } Set<K> convertedKeys = new HashSet <K>(); for (String key : keys) { try { convertedKeys.add((K) key); } catch (Exception e) { logger.error("deserialize keys error" , e); } } return convertedKeys; } @Override public Collection<V> values () { Set<String> keys = null ; try { keys = redisManager.scan(this .keyPrefix + "*" ); } catch (Exception e) { logger.error("get values error" , e); return Collections.emptySet(); } if (CollectionUtils.isEmpty(keys)) { return Collections.emptySet(); } List<V> values = new ArrayList <V>(keys.size()); for (String key : keys) { V value = null ; try { value = (V) redisManager.get(key); } catch (Exception e) { logger.error("deserialize values= error" , e); } if (value != null ) { values.add(value); } } return Collections.unmodifiableList(values); } public String getKeyPrefix () { return keyPrefix; } public void setKeyPrefix (String keyPrefix) { this .keyPrefix = keyPrefix; } public String getPrincipalIdFieldName () { return principalIdFieldName; } public void setPrincipalIdFieldName (String principalIdFieldName) { this .principalIdFieldName = principalIdFieldName; } }
getRedisKeyFromPrincipalIdField()是获取缓存的用户身份信息 和用户权限信息。 里面有一个属性principalIdFieldName 在RedisCacheManager也有这个属性,设置其中一个就可以.是为了给缓存用户身份和权限信息在Redis中的key唯一,登录用户名可能是username 或者 phoneNum 或者是Email中的一个,如 我的User实体类中 有一个 usernane字段,也是登录时候使用的用户名,在redis中缓存的权限信息key 如下, 这个admin 就是 通过getUsername获得的。
创建RedisCacheManager 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 public class RedisCacheManager implements CacheManager { private final Logger logger = LoggerFactory.getLogger(RedisCacheManager.class); private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap <String, Cache>(); private RedisManager redisManager; private static final int DEFAULT_EXPIRE = 1800 ; private int expire = DEFAULT_EXPIRE; public static final String DEFAULT_CACHE_KEY_PREFIX = "shiro:cache:" ; private String keyPrefix = DEFAULT_CACHE_KEY_PREFIX; public static final String DEFAULT_PRINCIPAL_ID_FIELD_NAME = "authCacheKey or id" ; private String principalIdFieldName = DEFAULT_PRINCIPAL_ID_FIELD_NAME; @Override public <K, V> Cache<K, V> getCache (String name) throws CacheException { logger.debug("get cache, name={}" , name); Cache cache = caches.get(name); if (cache == null ) { cache = new RedisCache <K, V>(redisManager, keyPrefix + name + ":" , expire, principalIdFieldName); caches.put(name, cache); } return cache; } public RedisManager getRedisManager () { return redisManager; } public void setRedisManager (RedisManager redisManager) { this .redisManager = redisManager; } public String getKeyPrefix () { return keyPrefix; } public void setKeyPrefix (String keyPrefix) { this .keyPrefix = keyPrefix; } public int getExpire () { return expire; } public void setExpire (int expire) { this .expire = expire; } public String getPrincipalIdFieldName () { return principalIdFieldName; } public void setPrincipalIdFieldName (String principalIdFieldName) { this .principalIdFieldName = principalIdFieldName; } }
创建RedisSessionDAO 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 public class RedisSessionDAO extends AbstractSessionDAO { private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class); private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:" ; private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX; private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 1000L ; private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT; private static final int DEFAULT_EXPIRE = -2 ; private static final int NO_EXPIRE = -1 ; private int expire = DEFAULT_EXPIRE; private static final int MILLISECONDS_IN_A_SECOND = 1000 ; private RedisManager redisManager; private static ThreadLocal sessionsInThread = new ThreadLocal (); @Override public void update (Session session) throws UnknownSessionException { try { if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) { return ; } if (session instanceof ShiroSession) { ShiroSession ss = (ShiroSession) session; if (!ss.isChanged()) { return ; } ss.setChanged(false ); } this .saveSession(session); } catch (Exception e) { logger.warn("update Session is failed" , e); } } private void saveSession (Session session) throws UnknownSessionException { if (session == null || session.getId() == null ) { logger.error("session or session id is null" ); throw new UnknownSessionException ("session or session id is null" ); } String key = getRedisSessionKey(session.getId()); if (expire == DEFAULT_EXPIRE) { this .redisManager.set(key, session, (int ) (session.getTimeout() / MILLISECONDS_IN_A_SECOND)); return ; } if (expire != NO_EXPIRE && expire * MILLISECONDS_IN_A_SECOND < session.getTimeout()) { logger.warn("Redis session expire time: " + (expire * MILLISECONDS_IN_A_SECOND) + " is less than Session timeout: " + session.getTimeout() + " . It may cause some problems." ); } this .redisManager.set(key, session, expire); } @Override public void delete (Session session) { if (session == null || session.getId() == null ) { logger.error("session or session id is null" ); return ; } try { redisManager.del(getRedisSessionKey(session.getId())); } catch (Exception e) { logger.error("delete session error. session id= {}" ,session.getId()); } } @Override public Collection<Session> getActiveSessions () { Set<Session> sessions = new HashSet <Session>(); try { Set<String> keys = redisManager.scan(this .keyPrefix + "*" ); if (keys != null && keys.size() > 0 ) { for (String key:keys) { Session s = (Session) redisManager.get(key); sessions.add(s); } } } catch (Exception e) { logger.error("get active sessions error." ); } return sessions; } public Long getActiveSessionsSize () { Long size = 0L ; try { size = redisManager.scanSize(this .keyPrefix + "*" ); } catch (Exception e) { logger.error("get active sessions error." ); } return size; } @Override protected Serializable doCreate (Session session) { if (session == null ) { logger.error("session is null" ); throw new UnknownSessionException ("session is null" ); } Serializable sessionId = this .generateSessionId(session); this .assignSessionId(session, sessionId); this .saveSession(session); return sessionId; } @Override protected Session doReadSession (Serializable sessionId) { if (sessionId == null ) { logger.warn("session id is null" ); return null ; } Session s = getSessionFromThreadLocal(sessionId); if (s != null ) { return s; } logger.debug("read session from redis" ); try { s = (Session) redisManager.get(getRedisSessionKey(sessionId)); setSessionToThreadLocal(sessionId, s); } catch (Exception e) { logger.error("read session error. settionId= {}" ,sessionId); } return s; } private void setSessionToThreadLocal (Serializable sessionId, Session s) { Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get(); if (sessionMap == null ) { sessionMap = new HashMap <Serializable, SessionInMemory>(); sessionsInThread.set(sessionMap); } SessionInMemory sessionInMemory = new SessionInMemory (); sessionInMemory.setCreateTime(new Date ()); sessionInMemory.setSession(s); sessionMap.put(sessionId, sessionInMemory); } private Session getSessionFromThreadLocal (Serializable sessionId) { Session s = null ; if (sessionsInThread.get() == null ) { return null ; } Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get(); SessionInMemory sessionInMemory = sessionMap.get(sessionId); if (sessionInMemory == null ) { return null ; } Date now = new Date (); long duration = now.getTime() - sessionInMemory.getCreateTime().getTime(); if (duration < sessionInMemoryTimeout) { s = sessionInMemory.getSession(); logger.debug("read session from memory" ); } else { sessionMap.remove(sessionId); } return s; } private String getRedisSessionKey (Serializable sessionId) { return this .keyPrefix + sessionId; } public RedisManager getRedisManager () { return redisManager; } public void setRedisManager (RedisManager redisManager) { this .redisManager = redisManager; } public String getKeyPrefix () { return keyPrefix; } public void setKeyPrefix (String keyPrefix) { this .keyPrefix = keyPrefix; } public long getSessionInMemoryTimeout () { return sessionInMemoryTimeout; } public void setSessionInMemoryTimeout (long sessionInMemoryTimeout) { this .sessionInMemoryTimeout = sessionInMemoryTimeout; } public int getExpire () { return expire; } public void setExpire (int expire) { this .expire = expire; } }
自定义异常类 PrincipalInstanceException 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class PrincipalInstanceException extends RuntimeException { private static final String MESSAGE = "We need a field to identify this Cache Object in Redis. " + "So you need to defined an id field which you can get unique id to identify this principal. " + "For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. " + "For example, getUserId(), getUserName(), getEmail(), etc.\n" + "Default value is authCacheKey or id, that means your principal object has a method called \"getAuthCacheKey()\" or \"getId()\"" ; public PrincipalInstanceException (Class clazz, String idMethodName) { super (clazz + " must has getter for field: " + idMethodName + "\n" + MESSAGE); } public PrincipalInstanceException (Class clazz, String idMethodName, Exception e) { super (clazz + " must has getter for field: " + idMethodName + "\n" + MESSAGE, e); } }
PrincipalIdNullException 1 2 3 4 5 6 7 8 public class PrincipalIdNullException extends RuntimeException { private static final String MESSAGE = "Principal Id shouldn't be null!" ; public PrincipalIdNullException (Class clazz, String idMethodName) { super (clazz + " id field: " + idMethodName + ", value is null\n" + MESSAGE); } }
重写ShiroRealm 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 public class ShiroRealm extends AuthorizingRealm { private UserServiceAgent userServiceAgent = new UserServiceAgent (); private RoleServiceAgent roleServiceAgent = new RoleServiceAgent (); private PermissionServiceAgent permissionServiceAgent = new PermissionServiceAgent (); @Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; String username = usernamePasswordToken.getUsername(); String password = new String (usernamePasswordToken.getPassword()); User user = userServiceAgent.findByUserName(username); if (user == null ) { throw new UnknownAccountException ("用户名或密码错误!" ); } if ("1" .equals(user.getState())) { throw new LockedAccountException ("账号已被锁定,请联系管理员!" ); } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo (user, user.getPassword(), getName()); return info; } @Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) { User user = (User) SecurityUtils.getSubject().getPrincipal(); Set<Role> roles = roleServiceAgent.findRolesByUserId(user.getUid()); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo (); for (Role role : roles) { authorizationInfo.addRole(role.getRole()); } Set<Permission> permissions = permissionServiceAgent.findPermissionsByRoleId(roles); for (Permission permission : permissions) { authorizationInfo.addStringPermission(permission.getPermission()); } return authorizationInfo; } @Override public void clearCachedAuthorizationInfo (PrincipalCollection principals) { super .clearCachedAuthorizationInfo(principals); } @Override public void clearCachedAuthenticationInfo (PrincipalCollection principals) { super .clearCachedAuthenticationInfo(principals); } @Override public void clearCache (PrincipalCollection principals) { super .clearCache(principals); } public void clearAllCachedAuthorizationInfo () { getAuthorizationCache().clear(); } public void clearAllCachedAuthenticationInfo () { getAuthenticationCache().clear(); } public void clearAllCache () { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } }
ShiroConfig 配置 配置redisManagerAgent 1 2 3 4 @Bean("redisManagerAgent") public RedisManager redisManager () { return new RedisManagerAgent (); }
配置缓存管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Bean public RedisCacheManager cacheManager () { RedisCacheManager redisCacheManager = new RedisCacheManager (); redisCacheManager.setRedisManager(redisManager()); redisCacheManager.setPrincipalIdFieldName("username" ); redisCacheManager.setExpire(200000 ); return redisCacheManager; }
配置密码比较器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Bean("credentialsMatcher") public RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher () { RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher (); retryLimitHashedCredentialsMatcher.setRedisManager(redisManager()); return retryLimitHashedCredentialsMatcher; }
并发登录控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean public KickoutSessionControlFilter kickoutSessionControlFilter () { KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter (); kickoutSessionControlFilter.setSessionManager(sessionManager()); kickoutSessionControlFilter.setRedisManager(redisManager()); kickoutSessionControlFilter.setKickoutAfter(false ); kickoutSessionControlFilter.setMaxSession(1 ); kickoutSessionControlFilter.setKickoutUrl("/login?kickout=1" ); return kickoutSessionControlFilter; }
配置sessionDAO 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Bean public SessionDAO sessionDAO () { RedisSessionDAO redisSessionDAO = new RedisSessionDAO (); redisSessionDAO.setRedisManager(redisManager()); redisSessionDAO.setExpire(12000 ); return redisSessionDAO; }
配置sessionFactory 1 2 3 4 5 @Bean("sessionFactory") public ShiroSessionFactory sessionFactory () { ShiroSessionFactory sessionFactory = new ShiroSessionFactory (); return sessionFactory; }
配置cacheManager 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Bean public RedisCacheManager cacheManager () { RedisCacheManager redisCacheManager = new RedisCacheManager (); redisCacheManager.setRedisManager(redisManager()); redisCacheManager.setPrincipalIdFieldName("username" ); redisCacheManager.setExpire(200000 ); return redisCacheManager; }
配置Session监听器 1 2 3 4 5 6 7 8 9 10 11 @Bean("sessionListener") public ShiroSessionListener sessionListener () { ShiroSessionListener sessionListener = new ShiroSessionListener (); return sessionListener; }
配置会话管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Bean("sessionManager") public SessionManager sessionManager () { ShiroSessionManager sessionManager = new ShiroSessionManager (); Collection<SessionListener> listeners = new ArrayList <SessionListener>(); listeners.add(sessionListener()); sessionManager.setSessionListeners(listeners); sessionManager.setSessionIdCookie(sessionIdCookie()); sessionManager.setSessionDAO(sessionDAO()); sessionManager.setCacheManager(cacheManager()); sessionManager.setSessionFactory(sessionFactory()); sessionManager.setGlobalSessionTimeout(1800000 ); sessionManager.setDeleteInvalidSessions(true ); sessionManager.setSessionValidationSchedulerEnabled(true ); sessionManager.setSessionValidationInterval(3600000 ); sessionManager.setSessionIdUrlRewritingEnabled(false ); return sessionManager; }
安全事务管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean(name = "securityManager") public SecurityManager securityManager () { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager (); securityManager.setRealm(shiroRealm()); securityManager.setRememberMeManager(rememberMeManager()); securityManager.setCacheManager(cacheManager()); securityManager.setSessionManager(sessionManager()); return securityManager; }