Shiro 会话管理和加密
会话管理
缓存
加密
会话管理
Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如Tomcat),不管是J2SE还是J2EE环境都可以使用,提供了会话管理,会话事件监听,会话存储/持久化,容器无关的集群,失效/过期支持,对 Web 的透明支持,SSO 单点登录的支持等特性。
会话相关API
Subject.getSession():获取会话,等价于 Subject.getSession(true),即如果当前没有创建 session 对象会创建一个;Subject.getSession(false),如果当前没有创建 session 对象则返回 null。
session.getId():获取当前会话的唯一标识。
session.setAttribute(key,val) :设置会话属性。
session.getAttribute(key) :获取会话属性。
session.removeAttribute(key):删除会话属性。
SessionDAO
Shiro 提供 SessionDao 用于会话持久化。提供 CRUD 操作。
AbstractSessionDAO 提供了 SessionDAO 的基础实现,如生成会话 ID 等。
CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager。
MemorySessionDAO 直接在内存中进行会话维护。
EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。
在实际开发中,如果要用到 SessionDAO 组件,可以自定义类实现自 EnterpriseCacheSessionDAO 类,为其注入 sessionIdGenerator 属性,如果用到缓存的话还可以注入一个缓存的名字。最后将这个 SesionDAO 组件注入给 SessionManager(会话管理器),最后将 SessionManager 配置给 SecurityManager。
会话使用
建议在开发中,Controller 层使用原生的 HttpSession 对象,在 Service 层中使用 Shiro 提供的 Session 对象。如果在 Service 层中使用 HttpSession 对象,那么属于侵入式,并不建议这么做。Shiro 提供的 Session 能够很好的解决这个问题。
那么,问题来了,两种方式获取的 session 是否相同呢?
在 Controller 中,通过 request.getSession() 获取会话 session ,该 session 到底来源于
ServletRequest 还是由 Shiro 管理并创建的会话,主要由安全管理器 SecurityManager 和
SessionManager 会话管理器决定。
在使用默认 SessionManager 会话管理器的情况下,不管是通过 request.getSession() 或者 subject.getSession() 获取到 session,操作 session,两者都是等价的,请大家放心使用!
缓存
使用第三方的 shiro-redis 集成 redis 实现缓存
- 添加依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
- application.poperties 配置文件中添加 Redis 配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis 服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=hxy
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阳塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默认日
spring.redis.lettuce.pool.min-idle=0
#Redis服务超时时间
spring.redis.timeout=5000
- ShiroConfig.java
package com.bdqn.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.bdqn.pojo.Right;
import com.bdqn.service.RightService;
import com.bdqn.shiro.MyShiroRealm;
import jakarta.annotation.Resource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.mgt.SecurityManager;
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.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* ShiroConfig
*/
@Configuration
public class ShiroConfig {
// 注入Redis参数,从application.yml获得
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password}")
private String password;
@Value("${spring.data.redis.connect-timeout}")
private int timeout;
@Resource
private RightService rightService;
/**
* 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个 bean(DefaultAdvisorAutoProxyCreator 和 AuthorizationAttributeSourceAdvisor)
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启AOP注解支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 验证匹配规则
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 使用 md5 算法进行加密
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 设置散列次数:意为加密几次
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}
/**
* 自定义Realm(基于数据库)
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm shiroRealm = new MyShiroRealm();
// 设置启用缓存,并设置缓存名称
shiroRealm.setCachingEnabled(true);
shiroRealm.setAuthorizationCachingEnabled(true);
shiroRealm.setAuthorizationCacheName("authorization");
// 设置凭证(密码)匹配器
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
/**
* Thymeleaf页面上使用shiro标签
*/
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
/**
* Redis管理器
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host + ":" + port); // #shiro-redis v3.3.1
redisManager.setPassword(password);
redisManager.setTimeout(timeout);
return redisManager;
}
/**
* 缓存管理器
*/
@Bean
public CacheManager shiroCacheManager() {
RedisCacheManager cacheManager = new RedisCacheManager();
cacheManager.setRedisManager(redisManager());
// 缓存名称
cacheManager.setPrincipalIdFieldName("usrName");
// 缓存有效时间
cacheManager.setExpire(1800);
return cacheManager;
}
/**
* 会话持久化操作
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO sessionDAO = new RedisSessionDAO();
sessionDAO.setRedisManager(redisManager());
return sessionDAO;
}
/**
* 安全管理器SecurityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入自定义的Realm
securityManager.setRealm(myShiroRealm());
// 注入缓存管理器
securityManager.setCacheManager(shiroCacheManager());
// 注入会话管理器
securityManager.setSessionManager(sessionManager());
// Shiro2.0.1新版本需要SecurityUtils设置SecurityManager,否则报错(org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code)
SecurityUtils.setSecurityManager(securityManager);
return securityManager;
}
/**
* 会话管理
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* Shiro过滤器:权限验证
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
// 注入SecurityManager
shiroFilterFactory.setSecurityManager(securityManager);
// 权限验证:使用 Filter 控制资源(URL)的访问
shiroFilterFactory.setLoginUrl("/login"); // 登录页面URL
shiroFilterFactory.setSuccessUrl("/main"); // 登录成功URL
shiroFilterFactory.setUnauthorizedUrl("/403"); // 没有权限跳转403页面
// 权限配置集合,必须使用LinkedHashMap(有序集合)
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 静态资源授权
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/localcss/**", "anon");
filterChainDefinitionMap.put("/localjs/**", "anon");
// API接口请求授权
filterChainDefinitionMap.put("/api/**", "anon");
// 登录退出页面设置
filterChainDefinitionMap.put("/login", "anon"); // 点击登录按钮时放行
filterChainDefinitionMap.put("/logout", "logout"); // 点击退出按钮时放行
// 配置需要特定权限才能访问的资源(URL)
// 静态授权:包括全部需要特定权限才能访问的资源(URL)
/*
filterChainDefinitionMap.put("/user/list", "perms[用户列表]");
filterChainDefinitionMap.put("/user/add", "perms[用户添加]");
filterChainDefinitionMap.put("/user/edit", "perms[用户编辑]");
filterChainDefinitionMap.put("/user/del", "perms[用户删除]");
*/
// 动态授权
List<Right> rights = rightService.findAll();
for (Right right : rights) {
if (right.getRightUrl() != null && !right.getRightUrl().trim().equals("")) {
filterChainDefinitionMap.put(right.getRightUrl(), "perms[" + right.getRightCode() + "]");
}
}
// 配置认证访问:其他资源(URL)必须认证通过才能访问
filterChainDefinitionMap.put("/**", "authc"); // 必须放在过滤器链的最后面
shiroFilterFactory.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactory;
}
}
在测试中如出现了 java.lang.ClassCastException: com.crm.pojo.User cannot be cast to com.crm.pojo.User 异常, 原因是我们的项目可能使用了热部署, 所以造成类加载器不一致所导致。
解决方案:
在 resource 目录下新建 META-INF 目录,在 META-INF 目录下新建 spring-devtools.properties文件,在 spring-devtools.properties 文件中添加以下两条配置信息:
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
restart.include.thymeleaf-extras-shiro=/thymeleaf-extras-[\\w-\\.]+jar
配置完成后重启服务就OK了。
加密
哈希与盐
如果你需要保存密码(比如网站用户的密码),你要考虑如何保护这些密码数据,像下面那样直接将密码写入数据库中是极不安全的,因为任何可以打开数据库的人,都将可以直接看到这些密码,比如之前的600w CSDN账号泄露对用户可能造成很大损失。
解决的办法是将密码加密后再存储进数据库,比较常用的加密方法是使用哈希函数(散列算法),常见的散列算法如 MD5、SHA 等。哈希函数的具体定义,大家可以在网上或者相关书籍中查阅到,简单地说,它的特性如下:
原始密码经哈希函数计算后得到一个哈希值
改变原始密码,哈希函数计算出的哈希值也会相应改变
同样的密码,哈希值也是相同的
哈希函数是单向、不可逆的。也就是说从哈希值,你无法推算出原始的密码是多少。
一般进行散列时最好提供一个 salt(盐),加密领域的盐 salt,不是炒菜的调料,而是为了提高加密的安全性。不管是对称加密,还是非对称加密,在用户信息和算法可能被泄漏的情况下,都存在密码被反推算出来的可能。在加密环节如果加盐,等于多了一重安全因素。一般来说,盐就是一个不被外界知道的随机字符串,把用户的明文密码加上盐,再进行加密得到密文(密码的加密后的形式)。
比如加密密码 “admin” ,产生的散列值是 “21232f297a57a5a743894aOe4a801fc3”,可以到一些 MD5 解密网站很容易的通过散列值得到密码 “admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和 ID(即盐);这样散列的对象是"密码+用户名+ID",这样生成的散列值相对来说更难破解。
@Test
public void testMd5Hash() {
String password = "123456";
String salt = "czkt";
long start = System.currentTimeMillis();
Md5Hash md5Hash = new Md5Hash(password, salt, 1024);
long end = System.currentTimeMillis();
System.out.println(md5Hash);
System.out.println("用时:" + (end - start) + "ms");
//输出:36aa3e095a86925b7658b5d00557fa77
}
加密与验证+登录次数限制
加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
如上的 ShiroConfig :
/**
* 验证匹配规则
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 使用 md5 算法进行加密
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 设置散列次数:意为加密几次
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}
/**
* 自定义Realm(基于数据库)
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm shiroRealm = new MyShiroRealm();
// 设置启用缓存,并设置缓存名称
shiroRealm.setCachingEnabled(true);
shiroRealm.setAuthorizationCachingEnabled(true);
shiroRealm.setAuthorizationCacheName("authorization");
// 设置凭证(密码)匹配器
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
加 MyShiroRealm.java
package com.bdqn.shiro;
import com.bdqn.pojo.Right;
import com.bdqn.pojo.Role;
import com.bdqn.pojo.User;
import com.bdqn.service.RightService;
import com.bdqn.service.UserService;
import jakarta.annotation.Resource;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.lang.util.ByteSource;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.context.annotation.Lazy;
import java.util.Set;
/**
* MyShiroRealm(安全数据源)
*/
public class MyShiroRealm extends AuthorizingRealm {
@Lazy // Shiro框架执行比@Cacheable注解AOP代理早,导致对象代理不成功
@Resource
private UserService userService;
@Lazy // Shiro框架执行比@Cacheable注解AOP代理早,导致对象代理不成功
@Resource
private RightService rightService;
/**
* 自定义认证流程(机制)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("调用 MyShiroRealm.doGetAuthenticationInfo 获取身份信息!");
// 获得身份信息
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String userName = token.getUsername();
// 从数据库中根据用户名查询用户对象(联表查询,包含角色信息)
User user = userService.getByUsrName(userName);
if (user == null) {
throw new UnknownAccountException("用户不存在"); // 用户不存在
}
if (user.getUsrFlag() == null || user.getUsrFlag() == 0) {
throw new LockedAccountException("账号已锁定");
}
// 通过用户获取角色信息
Role role = user.getRole(); // int &207 -> list
// 根据角色获取权限列表
Set<Right> rights = rightService.findRightByRoleId(role.getRoleId());
// 给角色设置权限集合
role.setRights(rights);
System.out.println(user);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getUsrPassword(), ByteSource.Util.bytes("czkt"), this.getName());
// 返回身份信息
return info;
}
/**
* 自定义授权(认证通过才会授权)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("调用 MyShiroRealm.doGetAuthorizationInfo 获取权限信息!");
// 获得权限信息
User user = (User) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 静态授权:授予主体(用户)相应的角色和权限
/*
info.addRole(user.getRole().getRoleName());
info.addStringPermission("用户列表"); // 所有用户拥有"用户列表"权限
if ("管理员".equals(user.getRole().getRoleName())) { // 管理员拥有"增删改"权限
info.addStringPermission("用户添加");
info.addStringPermission("用户编辑");
info.addStringPermission("用户删除");
}
*/
// 动态授权:从数据库中获取角色和权限
Role role = user.getRole();
if (role != null) {
info.addRole(role.getRoleName()); // 动态设置角色
Set<Right> rights = role.getRights();
if (rights != null && !rights.isEmpty()) {
for (Right right : rights) {
info.addStringPermission(right.getRightCode()); // 动态设置权限
}
}
}
// 返回授权信息
return info;
}
}
控制器:
/**
* 登录注销相关控制器
*/
@Controller
public class IndexController {
@Resource
private UserService userService;
@Resource
private RedisService redisService;
/**
* 去登录页
*/
@GetMapping("/login")
public String toLogin() {
return "login";
}
/**
* 执行登录操作
*/
@PostMapping("/login")
public String doLogin(Model model, HttpSession session, String usrName, String usrPassword) {
/* User loginUser = userService.login(usrName, usrPassword);
if (loginUser != null) {
session.setAttribute("loginUser", loginUser);
return "redirect:/main"; // 重定向到首页
} else {
model.addAttribute("msg", "用户名或密码错误,登录失败! ");
return "login"; // 必须转发
} */
// 使用Shiro安全框架进行登录验证
try {
// 构造一个Shiro令牌
UsernamePasswordToken token = new UsernamePasswordToken(usrName, usrPassword);
// 创建一个主体
Subject subject = SecurityUtils.getSubject();
subject.login(token);
// 认证成功
User loginUser = (User) subject.getPrincipal();
// 将认证的用户存储到Session
session.setAttribute("loginUser", loginUser);
System.out.println(session.toString() + "\nWeb Session: id=" + session.getId() + " usrName=" + ((User) session.getAttribute("loginUser")).getUsrName());
Session shiroSession = subject.getSession();
System.out.println(shiroSession.toString() + "\nShiro Session: id=" + shiroSession.getId() + " usrName=" + ((User) shiroSession.getAttribute("loginUser")).getUsrName());
return "redirect:/main"; // 重定向到首页
} catch (UnknownAccountException | IncorrectCredentialsException e) {
model.addAttribute("msg", "用户名或密码错误,登录失败!");
return "login";
} catch (LockedAccountException e) {
model.addAttribute("msg", "用户被禁用,登录失败!");
return "login";
} catch (AuthenticationException e) {
model.addAttribute("msg", "认证异常,登录失败!");
return "login";
}
}
测试时记得启动 Redis
完成