目录
1. 简介
Apache-Shiro是一个功能强大且易于使用的·Java·安全(权限)框架。Shiro可以完成:认证、授权、加密、会话管理、与Web·集成、缓存·等。借助-Shiro您可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的·Web·和企业应用程序。
1.1 为什么要使用shiro
自·2003·年以来,框架格局发生了相当大的变化,因此今天仍然有很多系统在使用Shiro。这与Shiro 的特性密不可分。
易于使用
:使用Shiro构建系统安全框架非常简单.就算第一次接触也可以快速掌握.
全面
: Shiro包含系统安全框架需要的功能,满足安全需求的“一站式服务”。
灵活
: Shiro可以在任何应用程序环境中工作。虽然它可以在·Web、EJB·和·IoC·环境中工作,但不需要依赖它们。Shiro·也没有强制要求任何规范,甚至没有很多依赖项。
强力支持Web
: Shiro·具有出色的·Web·应用程序支持,可以基于应用程序·URL·和·Web·协议(例如·REST)创建灵活的安全策略,同时还提供一组·JSP·库来控制页面输出。
兼容性强
: Shiro的设计模式使其易于与其他框架和应用程序集成。Shiro 与 Spring、Grails、Wicket、Tapestry、Mule、Apache· Camel、Vaadin-等框架无缝集成。
社区支持
:Shiro·是Apache-软件基金会的一个开源项目,有完备的社区支持,文档支持。如果需要,像Katasoft这样的商业公司也会提供专业的支持和服务。
1.2 shiro 与 SpringSecurity 对比
- SpringSecurity 基于 Spring 开发,项目若使用 Spring 作为基础,配合 SpringSecurity 做权限更加方便,而 Shiro需要和 Spring 进行整合开发;
- SpringSecurity 功能比 Shiro 更加丰富些,例如安全维护方面;
- SpringSecurity 社区资源相对比 Shiro 更加丰富;
- Shiro的配置和使用比较简单,SpringSecurity 上手复杂些;
- Shiro依赖性低,不需要任何框架和容器,可以独立运行 SpringSecurity 依赖Spring容器;
- shiro不仅仅可以使用在web 中,它可以工作在任何应用环境中。在集群会话时 Shiro 最重要的一个好处或许就是它的会话是独立于容器的。
1.3 基本功能
Authentication
:身份认证 / 登录,验证用户是不是拥有相应的身份;Authorization
:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;Session Management
:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;Cryptography
:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;Web Support
:Web 支持,可以非常容易的集成到 Web 环境;Caching
:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;Concurrency
:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;Testing
:提供测试支持;Run As
:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;Remember Me
:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
1.4 shiro 架构
2 基本使用
2.1 创建maven项目
- 导入maven依赖
<dependencies> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.9.1</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> </dependencies>
- 创建 ini 文件
在resources文件夹下创建shiro.ini[users] zhangsan=123456 lisi=456789
2.2 登录认证 Authentication
1、 概念
- 身份验证:一般需要提供如身份ID等一些标识信息来表明登录者的身份,如提供·email,用户名/密码来证明。
- 在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,从而应用能验证用户身份:
- principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary’ principals,一般是用户名/邮箱/手机号。
- credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
- 最常见的principals和credentials组合就是用户名/密码
2、 流程
- 收集用户身份/凭证,即如用户名/密码
- 调用
Subject.login
进行登录,如果失败将得到相应·的·AuthenticationException
异常,根据异常提示用户·错误信息;否则登录成功 重点!!!
⇒ 创建自定义的Realm
类,继承org.apache.shiro.realm. AuthenticatingRealm
类,实现doGetAuthenticationInfo()
方法!!!
2.2.1 登录认证实例
public class shiroRun {
public static void main(String[] args) {
//1 初始化SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//2 获取Subject 对象
Subject subject = SecurityUtils.getSubject();
//3 创建token对象 用户名密码从页面传
AuthenticationToken authenticationToken = new UsernamePasswordToken("zhangsan","123456");
//3 完成登陆
try {
subject.login(authenticationToken);
System.out.println("登陆成功");
}catch (UnknownAccountException e){
System.out.println("用户名不存在");
}catch (IncorrectCredentialsException e){
System.out.println("密码不正确");
}catch (AuthenticationException e) {
System.out.println("登陆失败");
}
}
}
登陆成功:
2.3 角色,授权 Authorization
1、授权概念
- 授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作·等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限·(Permission)、角色(Role)。
- 主体(Subject):访问应用的用户,在·Shiro·中使用·Subject-代表该用户。用户只有授权·后才允许访问相应的资源。
- 资源(Resource):在应用中用户可以访问的·URL,比如访问·JSP·页面、查看/编辑某些·数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
- 权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户·有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用·户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控·制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允·许。
- Shiro·支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,·即实例级别的)
- 角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有·一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等·都是角色,不同的角色拥有一组不同的权限
2、授权方式
-
编程式:
if(subject.hasRole("admin")){ // 有 }else{ //无 }
-
注解式,没有权限会报错
@RequiresRoles("admin") public void login(){ }
3、流程
- 首先调用Subject.isPermitted*/hasRole*接口,其会委托给SecurityManager,而SecurityManager接着会委托给·Authorizer
- Authorizer是真正的授权者,如果调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例;
- 在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;
- Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted*/hasRole*·会返回true,否则返回false表示授权失败
2.3.1 角色认证示例
1、ini文件修改
[users]
zhangsan=123456,role1,role2
lisi=456789
[roles]
role1=user:insert,user:select
2、代码修改
subject.hasRole
用来判断角色
subject.isPermitted
用来判断角色是否有此权限
subject.checkPermission
用来判断角色的权限,没有返回值,没有此权限会报错
//1 初始化SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//2 获取Subject 对象
Subject subject = SecurityUtils.getSubject();
//3 创建token对象 用户名密码从页面传
AuthenticationToken authenticationToken = new UsernamePasswordToken("zhangsan","123456");
//4 完成登陆
try {
subject.login(authenticationToken);
System.out.println("登陆成功");
//5 判断角色
boolean role1 = subject.hasRole("role1");
System.out.println("是否拥有此角色 = " + role1);
//6 判断权限
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否拥有用户添加权限 = " + permitted);
//6.1 判断是否有权限,没有返回值,没有此权限会报UnknownAccountException异常
subject.checkPermission("user:delete");
}catch (UnknownAccountException e){
System.out.println("用户名不存在");
}catch (IncorrectCredentialsException e){
System.out.println("密码不正确");
}catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("登陆失败");
}
2.4 加密 Cryptography
1、md5
import org.apache.shiro.crypto.hash.Md5Hash;
public class ShiroMD5 {
public static void main(String[] args) {
//1 密码明文
String password = "123456";
//2 使用md5 加密 e10adc3949ba59abbe56e057f20f883e
Md5Hash md5Hash = new Md5Hash(password);
System.out.println("md5加密 = " + md5Hash);
//2.1 加盐 cb28e00ef51374b841fb5c189b2b91c9
Md5Hash md5Hash1 = new Md5Hash(password,"password");
System.out.println("md5加盐 = " + md5Hash1);
//2.2 加盐 迭代3次 74e47e998b939a9f78742115c90f1201
Md5Hash md5Hash2 = new Md5Hash(password,"password",3);
System.out.println("md5加盐迭代 = " + md5Hash2);
}
}
2、父类加密
import org.apache.shiro.crypto.hash.SimpleHash;
//3 父类加密 74e47e998b939a9f78742115c90f1201
SimpleHash simpleHash = new SimpleHash("MD5",password,"password",3);
System.out.println("simpleHash = " + simpleHash);
2.4.1 加密登录认证
Shiro默认的登录认证是不带加密的,如果想要实现加密认证需要自定义登录认证,自定义Realm。
- 创建自定义
myRealm
继承AuthenticatingRealm
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;
public class MyRealm extends AuthenticatingRealm {
/**
* 1. 自定义的登录认证方法 shiro的login方法底层会调用 doGetAuthenticationInfo 方法进行认证
* 2. 配置 自定义的 realm 生效, 在ini文件中可以配置, 在 springboot 中配置
* 3. 该方法只是做一个获取进行对比的信息 认证逻辑还是shiro底层认证逻辑完成
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1 通过token 获取身份信息
Object principal = token.getPrincipal();
System.out.println("身份信息 = " + principal.toString());
//2 获取凭证信息
char[] credentials = (char[])token.getCredentials();
System.out.println("凭证信息 = " + new String(credentials));
//3 访问数据库中存储的用户信息
// User user = userDao.findOneByName(principal);
// String password = user.getPassword();
String password = "74e47e998b939a9f78742115c90f1201";
//4 创建封装校验逻辑对象,封装数据返回
AuthenticationInfo info = new SimpleAuthenticationInfo(principal,password, ByteSource.Util.bytes("password"),principal.toString());
return info;
}
}
- 修改ini文件
[main]
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
md5CredentialsMatcher.hashIterations=3
myrealm=com.demo.shiro.MyRealm
myrealm.credentialsMatcher=$md5CredentialsMatcher
securityManager.realms=$myrealm
[users]
zhangsan=74e47e998b939a9f78742115c90f1201,role1,role2
lisi=456789
[roles]
role1=user:insert,user:select
- 执行
2.3.1
的代码
身份验证成功,但是权限失败了
3 springboot 整合 (单机版)
项目结构
3.1 创建 项目 和 数据库(mysql => shiro)
创建数据库
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for power
-- ----------------------------
DROP TABLE IF EXISTS `power`;
CREATE TABLE `power` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of power
-- ----------------------------
INSERT INTO `power` VALUES (1, 'user:insert');
INSERT INTO `power` VALUES (2, 'user:delete');
INSERT INTO `power` VALUES (3, 'user:update');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, '管理员');
INSERT INTO `role` VALUES (2, '主管');
INSERT INTO `role` VALUES (3, '财务');
-- ----------------------------
-- Table structure for role_power
-- ----------------------------
DROP TABLE IF EXISTS `role_power`;
CREATE TABLE `role_power` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`role_id` int(0) NOT NULL,
`power_id` int(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `role_id`(`role_id`) USING BTREE,
INDEX `power_id`(`power_id`) USING BTREE,
CONSTRAINT `role_power_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `role_power_ibfk_2` FOREIGN KEY (`power_id`) REFERENCES `power` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_power
-- ----------------------------
INSERT INTO `role_power` VALUES (1, 1, 1);
INSERT INTO `role_power` VALUES (2, 1, 2);
INSERT INTO `role_power` VALUES (3, 2, 3);
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`pwd` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`rid` bigint(0) NULL DEFAULT NULL,
`phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'zhangsan', 'd1b129656359e35e95ebd56a63d7b9e0', NULL, '123456789');
INSERT INTO `user` VALUES (2, 'lisi', '7174f64b13022acd3c56e2781e098a5f', NULL, '987654321');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`user_id` int(0) NOT NULL,
`role_id` int(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `role_id`(`role_id`) USING BTREE,
INDEX `user_id`(`user_id`) USING BTREE,
CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 3);
INSERT INTO `user_role` VALUES (3, 2, 2);
SET FOREIGN_KEY_CHECKS = 1;
3.2 pom.xml
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.9.0</version>
</dependency>
<!-- ehcache 缓存 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
<!-- aop 解决配置类加载顺序而造成的null -->
<!-- @Configuration 在 @Component之前生成 在config中注入component 会 null-->
<dependency>
<groupId> org.aspectj</groupId >
<artifactId> aspectjweaver</artifactId >
<version>1.8.7</version>
</dependency>
3.3 创建自定义 多 Realm
配置
-
创建自定义
realm
父类import org.apache.shiro.SecurityUtils; import org.apache.shiro.realm.AuthorizingRealm; public abstract class RealmCustom extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = principals.getPrimaryPrincipal().toString(); ShiroUser user = userService.findUserByName(username); // ShiroUser user = (ShiroUser) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); Set<String> roles = user.getRoleList().stream().map(ShiroRole::getName).collect(Collectors.toSet()); Set<String> powers = user.getPowerList().stream().map(ShiroPower::getName).collect(Collectors.toSet()); simpleAuthorizationInfo.setRoles(roles); simpleAuthorizationInfo.setStringPermissions(powers); return simpleAuthorizationInfo; } /** * 清除登录信息 */ public final void clearCachedAuthenticationInfo() { super.clearCachedAuthenticationInfo(SecurityUtils.getSubject().getPrincipals()); } /** * 清除认证信息 */ public final void clearCachedAuthorizationInfo() { super.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals()); } public final void clearAllCache(){ clearCachedAuthenticationInfo(); clearCachedAuthorizationInfo(); } }
-
多
realm
配置,
创建 根据用户名密码登陆的子类RealmUserNamePassword
创建根据手机号登陆的用户RealmPhone
import com.shiro.demo.entity.ShiroUser; import com.shiro.demo.service.IShiroUserService; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Set; @Component public class RealmUserNamePassword extends RealmCustom { @Autowired private IShiroUserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("==============验证权限====================="); return super.doGetAuthorizationInfo(principals); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("================用户名密码验证====================="); UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String principal = token.getPrincipal().toString(); ShiroUser shiroUser = userService.findUserByName(principal); if(null != shiroUser) { //1. 验证权限时 principals.getPrimaryPrincipal() String key = principal; //2. 与 usernamePasswordToken 中的 password加密后 比较的参数 String pwd = shiroUser.getPwd(); //3. 可以使用user.getName()+"salt" 当做盐; ByteSource salt = ByteSource.Util.bytes("salt"); return new SimpleAuthenticationInfo(key, pwd, salt, this.getName()); } return null; } } import com.shiro.demo.entity.ShiroUser; import com.shiro.demo.service.IShiroUserService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class RealmPhone extends RealmCustom { @Autowired private IShiroUserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("================手机号鉴权====================="); return super.doGetAuthorizationInfo(principals); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("================手机号验证====================="); UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String phone = token.getPrincipal().toString(); String pwd = new String((char[]) token.getCredentials()); //从redis拿 String code = "111111"; // 比较 验证码是否正确 if(pwd.equals(code)){ return null; } //获取用户信息 ShiroUser shiroUser = userService.findUserByPhone(phone); if(null != shiroUser) { // 将手机号覆盖掉密码 token.setPassword(phone.toCharArray()); return new SimpleAuthenticationInfo( shiroUser.getName() , phone , this.getName()); } return null; } }
3.4 配置文件
3.4.1 配置类 ShiroConfig
import com.shiro.demo.config.filter.LoginFilter;
import com.shiro.demo.config.listener.ShiroSessionListener;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.ShiroHttpSession;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.*;
@Configuration
public class ShiroConfig {
@Autowired
private ShiroUtils shiroUtils;
/**
* 全局 shiro 配置会话管理器
*
* 重!!!!!!重!!!!!!重!!!!!!
* @return
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//2 创建认证对象,设置认证策略
ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(shiroUtils.getAtLeastOneSuccessfulStrategy());
//3 设置自定义realm验证
defaultWebSecurityManager.setRealms(shiroUtils.getRealms());
//4 reMemberMe 记住我
defaultWebSecurityManager.setRememberMeManager(shiroUtils.getRememberMeManager());
//5 设置缓存管理器
defaultWebSecurityManager.setCacheManager(shiroUtils.getEhCacheManage());
//6 设置session会话管理器
defaultWebSecurityManager.setSessionManager(sessionManager());
return defaultWebSecurityManager;
}
//========================================= 注解配置 ================================================================
/**
使用 static 脱离上下文
* 开启 shiro 注解 ----启用 IOC 容器中使用 shiro 的注解.
* 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
creator.setUsePrefix(false);
return creator;
}
/**
* Shiro生命周期处理器 ---可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法.
*
* @return
*/
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
// ========================================= 过滤器 ================================================
/**
* anon---------------没有参数,表示可以匿名使用
* org.apache.shiro.web.filter.authc.AnonymousFilter
*
* authc--------------表示需要认证(登录)才能使用,没有参数
* org.apache.shiro.web.filter.authc.FormAuthenticationFilter
*
* authcBasic---------没有参数表示httpBasic认证
* org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
*
* logout-------------退出的Filter实例
* org.apache.shiro.web.filter.authc.LogoutFilter
*
* noSessionCreation--未创建session的Filter实例
* org.apache.shiro.web.filter.session.NoSessionCreationFilter
*
* perms--------------参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],
* 当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
* org.apache.shiro.web.filter.authz.PermissionAuthorizationFilter
*
* port---------------当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,
* serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
* org.apache.shiro.web.filter.authz.PortFilter port[8081]
*
* rest---------------根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
* org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
*
* roles--------------参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],
* 每个参数通过才算通过,相当于hasAllRoles()方法。
* org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
*
* ssl----------------没有参数,表示安全的url请求,协议为https
* org.apache.shiro.web.filter.authz.SslFilter
*
* user---------------没有参数表示必须存在用户,当登入操作时不做检查
* org.apache.shiro.web.filter.authz.UserFilter
* 可将上面的过滤器分为认证过滤器和授权过滤器。
* 第一组认证过滤器:anon,authc,authcBasic,user
* 第二组授权过滤器:perms,port,rest,roles,ssl
* 注意user和authc不同:当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的
* user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe 说白了,
* 以前的一个用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 自定义拦截器处理session过期的操作
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
// filtersMap.put("clearSession", new ClearSessionCacheFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
//未登录时使用这个
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authc",new LoginFilter());
Map<String,String> filterMap = new HashMap<>(16);
filterMap.put("/shiro/login", "anon");
filterMap.put("/shiro/register", "anon");
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
//====================== shiro session 会话管理===========================
/**
* Shiro session 监听
* @return
*/
@Bean("sessionListener")
public ShiroSessionListener sessionListener(){
return new ShiroSessionListener();
}
/**
* 配置会话Id生成器
* @return
*/
@Bean
public SessionIdGenerator sessionIdGenerator(){
return new JavaUuidSessionIdGenerator();
}
/**
* SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
* MemorySessionDAO 直接在内存中进行会话维护
* EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
* @return
*/
@Bean
public SessionDAO sessionDAO(){
EnterpriseCacheSessionDAO enterpriseCacheSessionDAO = new EnterpriseCacheSessionDAO();
// 使用ehCacheManger
enterpriseCacheSessionDAO.setCacheManager(shiroUtils.getEhCacheManage());
// 设置session缓存的名字 默认为 shiro-activeSessionCache
enterpriseCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
//sessionId生成器
enterpriseCacheSessionDAO.setSessionIdGenerator(sessionIdGenerator());
return enterpriseCacheSessionDAO;
}
/**
* 配置保存sessionId的cookie
* 注意:这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理 也需要自己的cookie
* @return
*/
@Bean("sessionIdCookie")
public SimpleCookie sessionIdCookie(){
System.out.println("========================sessionIdCookie============================");
// 这个参数是cookie的名称
String defaultSessionIdName = ShiroHttpSession.DEFAULT_SESSION_ID_NAME;
SimpleCookie simpleCookie = new SimpleCookie("sid");
// setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
// 设为true后,只能通过http访问,javascript无法访问, 防止xss读取cookie
simpleCookie.setHttpOnly(false);
simpleCookie.setPath("/");
// maxAge=-1表示浏览器关闭时失效此Cookie
simpleCookie.setMaxAge(-1);
return simpleCookie;
}
/**
* 配置会话管理器,设定会话超时及保存
* @return
*/
@Bean("sessionManager")
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 为了解决输入网址地址栏出现 jsessionid 的问题
sessionManager.setSessionIdUrlRewritingEnabled(false);
Collection<SessionListener> listeners = new ArrayList<SessionListener>();
// 配置监听
listeners.add(sessionListener());
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionIdCookie(sessionIdCookie());
sessionManager.setSessionDAO(sessionDAO());
sessionManager.setCacheManager(shiroUtils.getEhCacheManage());
// 全局会话超时时间(单位毫秒),默认30分钟 暂时设置为10秒钟 用来测试
sessionManager.setGlobalSessionTimeout(10000);
// sessionManager.setGlobalSessionTimeout(1800000);
// 是否开启删除无效的session对象 默认为true
sessionManager.setDeleteInvalidSessions(true);
// 是否开启定时调度器进行检测过期session 默认为true
sessionManager.setSessionValidationSchedulerEnabled(true);
// 设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
// 设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
// 暂时设置为 5秒 用来测试
sessionManager.setSessionValidationInterval(3600000);
// sessionManager.setSessionValidationInterval(5000);
return sessionManager;
}
}
3.4.2 帮助类 ShiroUtils
import com.shiro.demo.config.realm.RealmCustom;
import com.shiro.demo.config.realm.RealmPhone;
import com.shiro.demo.config.realm.RealmUserNamePassword;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AllSuccessfulStrategy;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
@Component
public class ShiroUtils {
/**
* reMemberMe 的加密秘钥,因为是aes算法, 所以必须16位
*/
private static String reMemberMe_key = "0123456789ABCDEF";
@Autowired
private RealmUserNamePassword realmUserNamePassword;
@Autowired
private RealmPhone realmPhone;
/**
* 整理需要注册的自定义realm
* 1. 加载密码加密器(可忽略)
* 2. 加载缓存管理,可按照参数配置
* 3. 加入集合
*
* @return
*/
protected Set<Realm> getRealms(){
//1. 设置该realm密码验证是否加密
realmUserNamePassword.setCredentialsMatcher(getHashedCredentialsMatcher());
//2. 加载缓存管理器
editEhcache(realmUserNamePassword,true,true,true);
editEhcache(realmPhone,true,true,true);
//3. 添加到集合中
Set<Realm> realms = new HashSet<>();
realms.add(realmUserNamePassword);
realms.add(realmPhone);
return realms;
}
/**
* 设置缓存
* @param realm 对象
* @param enabled 是否缓存
* @param cation 是否开启认证缓存
* @param zation 是否开启授权缓存
*/
private static void editEhcache(RealmCustom realm, boolean enabled, boolean cation, boolean zation){
// 是否开启缓存 如果false, cation和zation都将为false
realm.setCachingEnabled(enabled);
// 认证缓存
realm.setAuthenticationCachingEnabled(cation);
realm.setAuthenticationCacheName("authenticationCache");
// 授权缓存
realm.setAuthorizationCachingEnabled(zation);
realm.setAuthorizationCacheName("authorizationCache");
}
protected HashedCredentialsMatcher getHashedCredentialsMatcher(){
//matcher就是用来指定加密规则
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//加密方式
matcher.setHashAlgorithmName("md5");
//hash次数
matcher.setHashIterations(3);
return matcher;
}
/**
* cookie管理对象;
* rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
* @return
*/
protected CookieRememberMeManager getRememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//使用aes加密16位
cookieRememberMeManager.setCipherKey(reMemberMe_key.getBytes());
return cookieRememberMeManager;
}
/**
* cookie对象;
* rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
* @return
*/
private SimpleCookie rememberMeCookie(){
// 这个rememberMe 是前端cookie的名称
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设置跨域 七天有效
cookie.setPath("/");
cookie.setHttpOnly(false);
cookie.setMaxAge(7*24*60*60);
return cookie;
}
// ====================== 认证策略 =======================
/**
* 默认
* 只要有一个(或多个)的realm验证成功,那么认证成功
* @return AtLeastOneSuccessfulStrategy
*/
protected AtLeastOneSuccessfulStrategy getAtLeastOneSuccessfulStrategy(){
return new AtLeastOneSuccessfulStrategy();
}
/**
* 第一个realm验证成功,整体认证成功,后续realm将忽略
* @return AllSuccessfulStrategy
*/
protected AllSuccessfulStrategy getAllSuccessfulStrategy(){
return new AllSuccessfulStrategy();
}
/**
* 所有realm成功,认证才成功
* @return FirstSuccessfulStrategy
*/
protected FirstSuccessfulStrategy getFirstSuccessfulStrategy(){
return new FirstSuccessfulStrategy();
}
/**
* 缓存管理器
* @return
*/
protected EhCacheManager getEhCacheManage(){
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return ehCacheManager;
}
}
3.4.3 登陆过滤器 LoginFilter
编写登陆过滤器 LoginFilter
继承 org.apache.shiro.web.filter.authc.UserFilter
import com.shiro.demo.exception.ResultCode;
import com.shiro.demo.exception.ResultData;
import org.apache.shiro.web.filter.authc.UserFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
public class LoginFilter extends UserFilter {
/**
* 这个方法用于处理未登录时页面重定向的逻辑
* 因此,只要进入了这个方法,就意味着登录失效了
* 我们只需要在这个方法里,给前端返回一个登录失效的状态码即可
* @param request
* @param response
* @throws IOException
*/
@Override
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
response.setContentType("application/json; charset=utf-8");
// 自定义返回内容
// 本处返回一个自己定义的ResultVo对象
ResultData error = ResultData.error(ResultCode.ERROR_100008);
response.getWriter().write(error.toJson());
}
}
在shiroConfig
的 过滤器 ShiroFilterFactoryBean
对象中加入
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//未登录时使用这个
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authc",new LoginFilter());
//其他代码省略、、、、
return shiroFilterFactoryBean;
}
3.4.4 权限过滤器注解 @Requires....
权限不足会有org.apache.shiro.authz.AuthorizationException
异常
他有两个子类UnauthenticatedException
为未登录异常 , UnauthorizedException
为权限不足异常
注解 | 解释 |
---|---|
@RequiresGuest | 验证是否是一个guest的请求,与@RequiresUser完全相反。换言之,RequiresUser == !RequiresGuest。此时subject.getPrincipal() 结果为null.。必须是一个游客,否则UnauthorizedException |
@RequiresRoles | 必须拥有该身份, 在自定义的realm 中配置, 如果subject中有RoleName角色才可以访问方法,可以有多个身份,设置logical = Logical.OR |
@RequiresPermissions | 必须拥有该权限,在自定义的realm 中配置, 权限不足UnauthorizedException |
@RequiresUser | 必须是登陆(subject.isAuthenticated() 结果为true) 或者 记忆的 (subject.isRemembered()结果为true) |
@RequiresAuthentication | 验证用户是否登录,等同于方法subject.isAuthenticated() 结果为true时。不能是rememberMe |
异常处理可以借鉴: https://blog.csdn.net/qiwunongqingyin/article/details/127112297?spm=1001.2014.3001.5502
3.4.5 会话监听 ShiroSessionListener
自定义 session 监听器,可以线程安全的根据session统计在线人数, 在 3.4.1 Shiroconfig
中使用
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import java.util.concurrent.atomic.AtomicInteger;
public class ShiroSessionListener implements SessionListener {
/**
* 统计在线人数
* juc包下线程安全自增
*/
private final AtomicInteger sessionCount = new AtomicInteger(0);
@Override
public void onStart(Session session) {
// 会话创建,在线人数加一
sessionCount.incrementAndGet();
}
/**
* 退出会话时触发
* @param session
*/
@Override
public void onStop(Session session) {
// 会话退出,在线人数减一
sessionCount.decrementAndGet();
}
/**
* 会话过期时触发
* @param session
*/
@Override
public void onExpiration(Session session) {
// 会话过期,在线人数减一
sessionCount.decrementAndGet();
}
/**
* 获取在线人数使用
* @return
*/
public AtomicInteger getSessionCount() {
return sessionCount;
}
}
4 Realm 多认证策略设置
-
realm实现原理
当应用程序配置多个
Realm
时,例如:用户名密码校验、手机号验证码校验等等。Shiro的ModularRealmAuthenticator
会使用内部的AuthenticationStrategy
组件判断认证是成功还是失败。AuthenticationStrategy
是一个无状态的组件,它在身份验证尝试中被询问4次(这4 次交互所需的任何必要的状态将被作为方法参数):
(1)在所有Realm被调用之前
(2)在调用Realm 的 getAuthenticationInfo 方法之前
(3)在调用Realm 的 getAuthenticationInfo 方法之后
(4)在所有Realm被调用之后认证策略的另外一项工作就是聚合所有 Realm的结果信息封装至一个
AuthenticationInfo
实例中,并将此信息返回,以此作为Subject的身份信息。Shiro中定义了3种认证策略的实现:
AuthenticationStrategy.class 描述 AtLeastOneSuccessfulStrategy
默认只要有一个(或多个)的realm验证成功,那么认证成功 FirstSuccessfulStrategy
第一个realm验证成功,整体认证成功,后续realm将忽略 AllSuccessfulStrategy
所有ralm成功,认证才成功 -
可以查看
3.4.2
中的配置realm配置, 别忘了在
3.4.1 ShiroConfig
中的DefaultWebSecurityManager
对象中加入:defaultWebSecurityManager.setRealms(shiroUtils.getRealms());
@Autowired private RealmUserNamePassword realmUserNamePassword; @Autowired private RealmPhone realmPhone; //有几个在这儿加几个,然后再getRealms中进行配置 /** * 整理需要注册的自定义realm * 1. 加载密码加密器(可忽略) * 2. 加载缓存管理,可按照参数配置 * 3. 加入集合 * * @return */ protected Set<Realm> getRealms(){ //1. 设置该realm密码验证是否加密 realmUserNamePassword.setCredentialsMatcher(getHashedCredentialsMatcher()); //2. 是否加载缓存管理器 没有ehcahe 可以忽略 editEhcache(realmUserNamePassword,true,true,true); editEhcache(realmPhone,true,true,true); //3. 添加到集合中 Set<Realm> realms = new HashSet<>(); realms.add(realmUserNamePassword); realms.add(realmPhone); return realms; } /** * 设置缓存 * @param realm 对象 * @param enabled 是否缓存 * @param cation 是否开启认证缓存 * @param zation 是否开启授权缓存 */ private static void editEhcache(RealmCustom realm, boolean enabled, boolean cation, boolean zation){ // 是否开启缓存 如果false, cation和zation都将为false realm.setCachingEnabled(enabled); // 认证缓存 realm.setAuthenticationCachingEnabled(cation); realm.setAuthenticationCacheName("authenticationCache"); // 授权缓存 realm.setAuthorizationCachingEnabled(zation); realm.setAuthorizationCacheName("authorizationCache"); } protected HashedCredentialsMatcher getHashedCredentialsMatcher(){ //matcher就是用来指定加密规则 HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); //加密方式 matcher.setHashAlgorithmName("md5"); //hash次数 matcher.setHashIterations(3); return matcher; }
5 Remember me (记住我)功能
Shiro·提供了记住我(RememberMe)的功能,比如访问一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,·下次访问时无需再登录即可访问。
-
基本流程
(1)首先在登录页面选中RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie 写到客户端并保存下来;(2) 关闭浏览器再重新打开;会发现浏览器还是记住你的;
(3) 访问一般的网页服务器端,仍然知道你是谁,且能正常访问;
(4) 但是,如果我们访问电商平台时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。
-
配置
可以查看3.4.2
中查看
别忘了在3.4.1 ShiroConfig
中的DefaultWebSecurityManager
对象中加入:defaultWebSecurityManager.setRememberMeManager(shiroUtils.getRememberMeManager());
/** * cookie管理对象; * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中 * @return */ protected CookieRememberMeManager getRememberMeManager(){ CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); //使用aes加密16位 cookieRememberMeManager.setCipherKey(reMemberMe_key.getBytes()); return cookieRememberMeManager; } /** * cookie对象; * rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。 * @return */ private SimpleCookie rememberMeCookie(){ // 这个rememberMe 是前端cookie的名称 SimpleCookie cookie = new SimpleCookie("rememberMe"); //设置跨域 七天有效 cookie.setPath("/"); cookie.setHttpOnly(false); cookie.setMaxAge(7*24*60*60); return cookie; }
6 EhCache 缓存管理器
简介: EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。
下图是 Ehcache 在应用程序中的位置:
主要特性:
- 快速.
- 简单.
- 多种缓存策略
- 缓存数据有两级:内存和磁盘,因此无需担心容量问题
- 缓存数据会在虚拟机重启的过程中写入磁盘
- 可以通过 RMI、可插入 API 等方式进行分布式缓存
- 具有缓存和缓存管理器的侦听接口
- 支持多缓存管理器实例,以及一个实例的多个缓存区域
- 提供 Hibernate 的缓存实现
6.1 ehcache-shiro.xml
配置文件
该文件放在 resources
文件夹下
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">
<!--
缓存对象存放路径
java.io.tmpdir:默认的临时文件存放路径。
user.home:用户的主目录。
user.dir:用户的当前工作目录,即当前程序所对应的工作路径。
其它通过命令行指定的系统属性,如“java –DdiskStore.path=D:\\abc ……”。
-->
<diskStore path="java.io.tmpdir"/>
<!--
name:缓存名称。
maxElementsOnDisk:硬盘最大缓存个数。0表示不限制
maxEntriesLocalHeap:指定允许在内存中存放元素的最大数量,0表示不限制。
maxBytesLocalDisk:指定当前缓存能够使用的硬盘的最大字节数,其值可以是数字加单位,单位可以是K、M或者G,不区分大小写,
如:30G。当在CacheManager级别指定了该属性后,Cache级别也可以用百分比来表示,
如:60%,表示最多使用CacheManager级别指定硬盘容量的60%。该属性也可以在运行期指定。当指定了该属性后会隐式的使当前Cache的overflowToDisk为true。
maxEntriesInCache:指定缓存中允许存放元素的最大数量。这个属性也可以在运行期动态修改。但是这个属性只对Terracotta分布式缓存有用。
maxBytesLocalHeap:指定当前缓存能够使用的堆内存的最大字节数,其值的设置规则跟maxBytesLocalDisk是一样的。
maxBytesLocalOffHeap:指定当前Cache允许使用的非堆内存的最大字节数。当指定了该属性后,会使当前Cache的overflowToOffHeap的值变为true,
如果我们需要关闭overflowToOffHeap,那么我们需要显示的指定overflowToOffHeap的值为false。
overflowToDisk:boolean类型,默认为false。当内存里面的缓存已经达到预设的上限时是否允许将按驱除策略驱除的元素保存在硬盘上,默认是LRU(最近最少使用)。
当指定为false的时候表示缓存信息不会保存到磁盘上,只会保存在内存中。
该属性现在已经废弃,推荐使用cache元素的子元素persistence来代替,如:<persistence strategy=”localTempSwap”/>。
diskSpoolBufferSizeMB:当往磁盘上写入缓存信息时缓冲区的大小,单位是MB,默认是30。
overflowToOffHeap:boolean类型,默认为false。表示是否允许Cache使用非堆内存进行存储,非堆内存是不受Java GC影响的。该属性只对企业版Ehcache有用。
copyOnRead:当指定该属性为true时,我们在从Cache中读数据时取到的是Cache中对应元素的一个copy副本,而不是对应的一个引用。默认为false。
copyOnWrite:当指定该属性为true时,我们在往Cache中写入数据时用的是原对象的一个copy副本,而不是对应的一个引用。默认为false。
timeToIdleSeconds:单位是秒,表示一个元素所允许闲置的最大时间,也就是说一个元素在不被请求的情况下允许在缓存中待的最大时间。默认是0,表示不限制。
timeToLiveSeconds:单位是秒,表示无论一个元素闲置与否,其允许在Cache中存在的最大时间。默认是0,表示不限制。
eternal:boolean类型,表示是否永恒,默认为false。如果设为true,将忽略timeToIdleSeconds和timeToLiveSeconds,Cache内的元素永远都不会过期,也就不会因为元素的过期而被清除了。
diskExpiryThreadIntervalSeconds :单位是秒,表示多久检查元素是否过期的线程多久运行一次,默认是120秒。
clearOnFlush:boolean类型。表示在调用Cache的flush方法时是否要清空MemoryStore。默认为true。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
maxElementsInMemory:缓存最大数目
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
memoryStoreEvictionPolicy:
Ehcache的三种清空策略;
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
<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>
<!-- session 缓存 -->
<cache name="shiro-activeSessionCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
6.2 config
在 3.4.2
中配置
/**
* 缓存管理器
* @return
*/
protected EhCacheManager getEhCacheManage(){
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return ehCacheManager;
}
在3.4.1
的DefaultWebSecurityManager
中配置
//5 设置缓存管理器
defaultWebSecurityManager.setCacheManager(shiroUtils.getEhCacheManage());
7 Session 会话管理器
-
session的作用是什么?
和web容器中的session作用一样,就是用于记录浏览器和服务器之间的交互
-
登录状态和session有没有关系?
登录认证成功之后,shiro是将认证信息存储在session中的,以后的每次请求肯定会在过滤器中判断session中有没有认证信息,以作为放行的依据。
-
为什么要把session持久化,即为sessionDao?是为了分布情况下共享session吗?
把session持久化的原因有多个,分布式情况下共享session应该是原因之一;持久化就是通过sessionDao完成的。默认情况下,sessionDao的实现是MemorySessionDAO,即把session保存在内存中
-
sessionManager可以设置cacheManager?sessionDao也可以设置CacheManager?
通过查看源码发现:给sessionManager设置的CacheManager最终还是供sessionDao使用,目的就是持久化session到缓存中
-
session 共享?
-
SessionDao的作用到底是什么?
目的是为了将session持久化在内存中(MemorySessionDAO ),或者redis中(shiro-redis)
-
Shiro提供了Session的支持,主要用途是在Service层获取到Handler层的Session的信息,推荐在Handler层使用HttpSession。
实际上httpSession和ShiroSession是一样的,主要受SessionManager的控制。
-
会话验证调度器?
默认是开启的,用来验证session的是否过期
-
session的有效期可以设置,如果session被sessionDao存储了,那么到达有效期时,是不是也会被删除?
是的,sessionDao持久化session是有原因的,所以到了有效期,也是会删除session的
-
在用户未退出登录的情况下,关闭浏览器,然后再重新打开一个浏览器去访问系统,为什么展示的就是登录页面?
关闭了浏览器,cookie失效了,重新打开一个浏览器访问系统时,是没有带着cookie的,所以服务器会认为这是一个全新的访问者,会创建一个全新的session为这次请求服务,上次的session就坐等失效。
-
session到期
默认情况下,session到期后,shiro是让其重定向到登录页面,我们可以通过覆盖user过滤器,来改变shiro的重定向行为。
7.1 config
在 3.4.1
中有
/**
* Shiro session 监听
* @return
*/
@Bean("sessionListener")
public ShiroSessionListener sessionListener(){
return new ShiroSessionListener();
}
/**
* 配置会话Id生成器
* @return
*/
@Bean
public SessionIdGenerator sessionIdGenerator(){
return new JavaUuidSessionIdGenerator();
}
/**
* SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
* MemorySessionDAO 直接在内存中进行会话维护
* EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
* @return
*/
@Bean
public SessionDAO sessionDAO(){
EnterpriseCacheSessionDAO enterpriseCacheSessionDAO = new EnterpriseCacheSessionDAO();
// 使用ehCacheManger
enterpriseCacheSessionDAO.setCacheManager(shiroUtils.getEhCacheManage());
// 设置session缓存的名字 默认为 shiro-activeSessionCache
enterpriseCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
//sessionId生成器
enterpriseCacheSessionDAO.setSessionIdGenerator(sessionIdGenerator());
return enterpriseCacheSessionDAO;
}
/**
* 配置保存sessionId的cookie
* 注意:这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理 也需要自己的cookie
* @return
*/
@Bean("sessionIdCookie")
public SimpleCookie sessionIdCookie(){
// 这个参数是cookie的名称
SimpleCookie simpleCookie = new SimpleCookie("sid");
// setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
// 设为true后,只能通过http访问,javascript无法访问, 防止xss读取cookie
simpleCookie.setHttpOnly(false);
simpleCookie.setPath("/");
// maxAge=-1表示浏览器关闭时失效此Cookie
simpleCookie.setMaxAge(-1);
return simpleCookie;
}
/**
* 配置会话管理器,设定会话超时及保存
* @return
*/
@Bean("sessionManager")
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 为了解决输入网址地址栏出现 jsessionid 的问题
sessionManager.setSessionIdUrlRewritingEnabled(false);
Collection<SessionListener> listeners = new ArrayList<SessionListener>();
// 配置监听
listeners.add(sessionListener());
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionIdCookie(sessionIdCookie());
sessionManager.setSessionDAO(sessionDAO());
sessionManager.setCacheManager(shiroUtils.getEhCacheManage());
// 全局会话超时时间(单位毫秒),默认30分钟 暂时设置为10秒钟 用来测试
sessionManager.setGlobalSessionTimeout(10000);
// sessionManager.setGlobalSessionTimeout(1800000);
// 是否开启删除无效的session对象 默认为true
sessionManager.setDeleteInvalidSessions(true);
// 是否开启定时调度器进行检测过期session 默认为true
sessionManager.setSessionValidationSchedulerEnabled(true);
// 设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
// 设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
// 暂时设置为 5秒 用来测试
sessionManager.setSessionValidationInterval(3600000);
// sessionManager.setSessionValidationInterval(5000);
return sessionManager;
}
在3.4.1
的DefaultWebSecurityManager
中添加
//6 设置session会话管理器
defaultWebSecurityManager.setSessionManager(sessionManager());
8 ShiroFilter 过滤器
在 3.4.1
中有
// ========================================= 过滤器 ================================================
/**
* anon---------------没有参数,表示可以匿名使用
* org.apache.shiro.web.filter.authc.AnonymousFilter
*
* authc--------------表示需要认证(登录)才能使用,没有参数
* org.apache.shiro.web.filter.authc.FormAuthenticationFilter
*
* authcBasic---------没有参数表示httpBasic认证
* org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
*
* logout-------------退出的Filter实例
* org.apache.shiro.web.filter.authc.LogoutFilter
*
* noSessionCreation--未创建session的Filter实例
* org.apache.shiro.web.filter.session.NoSessionCreationFilter
*
* perms--------------参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],
* 当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
* org.apache.shiro.web.filter.authz.PermissionAuthorizationFilter
*
* port---------------当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,
* serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
* org.apache.shiro.web.filter.authz.PortFilter port[8081]
*
* rest---------------根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
* org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
*
* roles--------------参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],
* 每个参数通过才算通过,相当于hasAllRoles()方法。
* org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
*
* ssl----------------没有参数,表示安全的url请求,协议为https
* org.apache.shiro.web.filter.authz.SslFilter
*
* user---------------没有参数表示必须存在用户,当登入操作时不做检查
* org.apache.shiro.web.filter.authz.UserFilter
* 可将上面的过滤器分为认证过滤器和授权过滤器。
* 第一组认证过滤器:anon,authc,authcBasic,user
* 第二组授权过滤器:perms,port,rest,roles,ssl
* 注意user和authc不同:当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的
* user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe 说白了,
* 以前的一个用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 自定义拦截器处理session过期的操作
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
// filtersMap.put("clearSession", new ClearSessionCacheFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
//未登录时使用这个
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authc",new LoginFilter());
Map<String,String> filterMap = new HashMap<>(16);
filterMap.put("/shiro/login", "anon");
filterMap.put("/shiro/register", "anon");
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
9 自定义登陆TOKEN
自定义token需要实现HostAuthenticationToken, RememberMeAuthenticationToken接口
public class UserNamePhoneToken implements HostAuthenticationToken, RememberMeAuthenticationToken, Serializable{ private String phone; private String code; private boolean rememberMe; private String host; @Override public String getHost() { return host; } @Override public boolean isRememberMe() { return rememberMe; } @Override public Object getPrincipal() { return phone; } @Override public Object getCredentials() { return phone; } @Override public String toString() { //... } }
- 在对应的realm中重写方法
@Override public boolean supports(AuthenticationToken token) { return token instanceof UserNamePhoneToken; }
10 退出登陆无法清除认证缓存失效
由于在登录认证doGetAuthenticationInfo
方法中返回的return new SimpleAuthenticationInfo(userInfo, phone, this.getName());
第一个参数是用户对象, 在退出登陆时拿的是第一个参数userInfo, 而缓存中的key是登陆token中的getPrincipal()
方法,所以我们需要重写对应的方法来进行适配
在登录认证对应的realm中重写此方法:
// 获取登录认证缓存的key
@Override
protected Object getAuthenticationCacheKey(PrincipalCollection principals) {
UserInfoDTO primaryPrincipal = (UserInfoDTO) principals.getPrimaryPrincipal();
return primaryPrincipal.getPhone();
}
11 管理员更改其他人的权限, 自动刷新该用户的权限
其实就是删除该用户的权限缓存,在再次访问需要权限的方法时重新加载缓存的过程
public void flushUserCache(Serializable userId) {
// 获取需要刷新权限的用户
UserInfoDTO userinfo = iUserInfoService.getUserById(userId);
// 获取对应用户的登录认证,第一个参数是realm中登陆时返回的第一个参数, 第二个参数是realm注册到shiro中的名字,默认是类名
SimplePrincipalCollection principals = new SimplePrincipalCollection(userinfo,"RealmPhone");
// 清除对应用户的权限认证
realmPhone.clearCachedAuthorizationInfo(principals);
}
4 分布式使用 redis 整合
4.1 创建 SpringUtils
使用ApplicationContext
和 ConfigurableListableBeanFactory
创建bean 点我进入帮助类链接
4.2 修改项目
先创建redis帮助类等: 点我进入帮助类
修改流程:
如果 在自定义Realm
中加盐进行密码处理的话,需要自定义加盐类
实现ByteSource, Serializable
并且将org.apache.shiro.util.SimpleByteSource
类中所有代码复制过来就好,
并在自定义realm
中的认证方法doGetAuthenticationInfo
中修改加盐代码
ByteSource salt = new MySimpleByteSource("salt");
new SimpleAuthenticationInfo(shiroUser.getName(), shiroUser.getPwd(), salt, this.getName());
在 3.4.1 ShiroConfig 的 DefaultWebSecurityManager
中的 第五步
我们使用了 EhCache
,而我们现在要改成redisCache
,并将Session的配置也存入redis
中
- 创建
RedisCache
implementsCache<K,V>
并实现其中的方法 - 创建
RedisCacheManager
implementsCacheManager
实现getCache(cacheName)
方法,返回RedisCache
- 修改
3.4.2 ShiroUtils
中的editEhcache
设置 缓存认证和权限 方法 - 在
3.4.1 ShiroConfig
中设置认证权限缓存和session缓存 - 修改
ShiroSessionListener
根据session统计在线人数
4.2.1 创建RedisCache
切记!!! redis 的 Value 千万不要序列化
否则在这里的 V get(K k)
方法中进行(V)value
泛型强转会失败从而认证或授权失败!
切记!!! redis 的 Value 千万不要序列化
否则在这里的 V get(K k)
方法中进行(V)value
泛型强转会失败从而认证或授权失败!
切记!!! redis 的 Value 千万不要序列化
否则在这里的 V get(K k)
方法中进行(V)value
泛型强转会失败从而认证或授权失败!
import com.shiro.demo.util.redis.RedisTemplateUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import java.util.*;
/**
* 自定义redis 缓存的实现
* @ClassName RedisCache
*/
public class RedisCache<K,V> implements Cache<K,V> {
private String cacheName;
public RedisCache(){
}
public RedisCache(String cacheName){
this.cacheName = cacheName;
}
//登陆or鉴权or验证session
@Override
public V get(K k) throws CacheException {
System.out.println("get k = " + getKey(k));
if(k == null){
return null;
}
Object value = RedisTemplateUtils.get(getKey(k));
return (V)value;
}
//首次登陆或验证权限不存在时
@Override
public V put(K k, V v) throws CacheException {
System.out.println("put k = " + getKey( k ));
System.out.println("put v = " + v);
RedisTemplateUtils.set(getKey(k), v);
return v;
}
//登出时
@Override
public V remove(K k) throws CacheException {
System.out.println("remove key = " + cacheName +" "+ k);
Object o = RedisTemplateUtils.del(getKey(k));
return (V)o;
}
//清空所有
@Override
public void clear() throws CacheException {
System.out.println("clear");
RedisTemplateUtils.del(this.cacheName);
}
@Override
public int size() {
return RedisTemplateUtils.getKeys(cacheName).size();
}
@Override
public Set<K> keys() {
return (Set<K>) RedisTemplateUtils.getKeys(cacheName);
}
@Override
public Collection<V> values() {
return (Collection<V>) RedisTemplateUtils.getValues(cacheName);
}
private String getKey(K k){
return (cacheName + k);
}
}
4.2.2 创建 RedisCacheManager
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
public class RedisCacheManager implements CacheManager {
/**
* @param cacheName 认证或授权缓存的名字
*/
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
return new RedisCache<K, V>(cacheName);
}
}
4.2.3 配置认证 缓存的命名空间
这个名字将会作为 4.2.1 RedisCache
的 cacheName
出现
/**
* 授权缓存key
*/
private static final String zationCache_key = "demo:zationCache:";
/**
* 认证缓存key
*/
private static final String cationCache_key = "demo:cationCache:";
/**
* session 缓存key
*/
protected static final String sessionCache_key = "demo:sessionCache:";
/**
* 设置缓存
* @param realm 对象
* @param enabled 是否缓存
* @param cation 是否开启认证缓存
* @param zation 是否开启授权缓存
*/
private static void editEhcache(RealmCustomer realm, boolean enabled, boolean cation, boolean zation){
// 是否开启缓存 如果false, cation和zation都将为false
realm.setCachingEnabled(enabled);
// 认证缓存
realm.setAuthenticationCachingEnabled(cation);
realm.setAuthenticationCacheName(cationCache_key);
// 授权缓存
realm.setAuthorizationCachingEnabled(zation);
realm.setAuthorizationCacheName(zationCache_key);
}
/**
* 创建RedisCacheManager
*/
protected RedisCacheManager getRedisCacheManager(){
return new RedisCacheManager();
}
4.2.4 修改ShiroConfig 的 缓存配置
- 修改
DefaultWebSecurityManager
//5 设置缓存管理器 defaultWebSecurityManager.setCacheManager(shiroUtils.getRedisCacheManager());
- 修改
SessionDao
//设置session分组名 sessionDAO.setActiveSessionsCacheName(ShiroUtils.sessionCache_key); //设置使用redis缓存 sessionDAO.setCacheManager(shiroUtils.getRedisCacheManager());
- 修改
SessionManager
/** * 配置会话管理器,设定会话超时及保存 * @return */ @Bean("sessionManager") public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); // 为了解决输入网址地址栏出现 jsessionid 的问题 sessionManager.setSessionIdUrlRewritingEnabled(false); Collection<SessionListener> listeners = new ArrayList<SessionListener>(); // 配置监听 listeners.add(sessionListener()); sessionManager.setSessionListeners(listeners); sessionManager.setSessionIdCookie(sessionIdCookie()); sessionManager.setSessionDAO(sessionDAO()); //这里配置redisCache sessionManager.setCacheManager(shiroUtils.getRedisCacheManager()); // 心跳检测 定时 间隔 // 设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时 // 设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler sessionManager.setSessionValidationInterval(3600000); // 全局会话超时时间(单位毫秒),默认30分钟 sessionManager.setGlobalSessionTimeout(1800000); // 是否开启删除无效的session对象 默认为true sessionManager.setDeleteInvalidSessions(true); // 是否开启定时调度器进行检测过期session 默认为true sessionManager.setSessionValidationSchedulerEnabled(true); return sessionManager; }
4.2.5 修改 ShiroSessionListener
使用redis进行重构
import com.shiro.demo.util.redis.RedisTemplateUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
/**
* 这里面可以根据当前session做一些操作
*/
public class ShiroSessionListener implements SessionListener {
/**
* 统计在线人数
*/
private static final String sessionCount_key = "demo:session:count";
@Override
public void onStart(Session session) {
// 会话创建,在线人数加一
RedisTemplateUtils.incr(sessionCount_key,1);
}
/**
* 退出会话时触发
* @param session
*/
@Override
public void onStop(Session session) {
// 会话退出,在线人数减一
RedisTemplateUtils.decr(sessionCount_key,1);
}
/**
* 会话过期时触发
* @param session
*/
@Override
public void onExpiration(Session session) {
// 会话过期,在线人数减一
RedisTemplateUtils.decr(sessionCount_key,1);
}
/**
* 获取在线人数使用
* @return
*/
public static Long getSessionCount() {
return (Long) RedisTemplateUtils.get(sessionCount_key);
}
}
4.2.6 结果
5 SSO 单点登录(伪)
单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分