文章目录
一、前言
由于之前没有使用过 Shiro,最近开始使用,故对其部分流程和源码进行了阅读,大体总结了一些内容记录下来。本系列并不会完完全全分析 Shiro 的全部代码,仅把主(我)要(用)流(到)程(的) 简单分析一下。由于本系列大部分为个人内容理解 并且 个人学艺实属不精,故难免出现 “冤假错乱”。如有发现,感谢指正,不胜感激。(个人来说并不喜欢使用Shiro )
Shiro 源码分析全集:
本文会搭建一个最简单的Springboot + Shiro项目,目的在于为后续的Shiro 源码分析做准备,故不会详细介绍搭建过程。下面先介绍Shiro 中两个比较关键的类的实现。
1. SessionDao
- SessionDAO :定义了从持久层操作session的标准;
- AbstractSessionDAO :提供了SessionDAO的基础实现,如生成会话ID等;
- CachingSessionDAO :提供了对开发者透明的session缓存的功能,只需要设置相应的 CacheManager 即可;
- MemorySessionDAO :直接在内存中进行session维护;
- EnterpriseCacheSessionDAO :提供了缓存功能的session维护,默认情况下使用 MapCache 实现,内部使用ConcurrentHashMap保存缓存的会话。
2. SessionManager
- DefaultSessionManager :JavaSE环境
- ServletContainerSessionManager: Web环境,直接使用servlet容器会话。DefaultWebSecurityManager中默认使用的是ServletContainerSessionManager
- DefaultWebSessionManager:用于Web环境的实现,可以替第二个,自己维护着会话,直接废弃了Servlet容器的会话管理
3. 各种过滤器
详细释义如下:
二、项目搭建
1. ShiroConfig
这是Shiro 的主要配置类
@Configuration
public class ShiroConfig {
/**
* 配置Shiro生命周期处理器。
* 在 Bean创建时调用 init 方法,在 销毁时调用 destroy 方法
*
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 定制的Realm,来完成认证和鉴权的部分功能
*
* @return
*/
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
/**
* SecurityManager :核心类,具有极高的扩展性
*
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置定制的 Realm
securityManager.setRealm(customRealm());
// 配置 Session管理器
securityManager.setSessionManager(sessionManager());
// // 设置缓存管理器
// securityManager.setCacheManager();
// // 设置 RememberMeManager 来管理 RememberMeManager
// securityManager.setRememberMeManager();
return securityManager;
}
/**
* 设置 ShiroFilter
* 这里可以配置 Shiro 对接口的控制权限
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 使用Map映射,key是接口路径,value为指定的Filter 名称,具体对应名称参考上图
// 这里的配置是有顺序的,如果一个路径匹配多个过滤,那么只有先配置的过滤器生效
Map<String, String> map = new HashMap<>();
//配置登出接口
map.put("/logout", "logout");
// 放行登录接口
map.put("/shiro/login", "anon");
// 拦截其他接口
map.put("/**", "authc");
//设置登录路径,session失效会跳转该页面
shiroFilterFactoryBean.setLoginUrl("/shiro/login");
//登录成功页面
shiroFilterFactoryBean.setSuccessUrl("/success");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 这里指定了动态代理的方式使用了 Cglib
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 设置是否直接代理目标类,而不是仅代理特定的接口。 默认值为“ false”。
// 将此设置为“ true”可强制代理TargetSource的公开目标类。 如果该目标类是接口,则将为给定接口创建一个JDK代理。 如果该目标类是任何其他类,则将为给定类创建CGLIB代理。
// 注意:如果未指定接口(并且未激活接口自动检测),则根据具体代理工厂的配置,也会应用代理目标类行为
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 开启注解支持,包括 RequiresPermissions.class, RequiresRoles.class, RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class。
* 和Aop相同的逻辑,通过注入 Advisor 来增强一些类的和方法
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 在 DefaultWebSessionManager 情况下可以自定义Cookies 的一些信息
* @return
*/
public SimpleCookie sessionIdCookie() {
//这个参数是cookie的名称
SimpleCookie simpleCookie = new SimpleCookie("sid");
//setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
//setcookie()的第七个参数
//设为true后,只能通过http访问,javascript无法访问
//防止xss读取cookie
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
//maxAge=-1表示浏览器关闭时失效此Cookie
simpleCookie.setMaxAge(-1);
return simpleCookie;
}
/**
* 设置SessionManager,由我们自己来控制Session
* @return
*/
@Bean
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());
//全局会话超时时间(单位毫秒),默认30分钟 暂时设置为10秒钟 用来测试
sessionManager.setGlobalSessionTimeout(1800000);
//是否开启删除无效的session对象 默认为true
sessionManager.setDeleteInvalidSessions(true);
//是否开启定时调度器进行检测过期session 默认为true
sessionManager.setSessionValidationSchedulerEnabled(true);
//设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
//设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
//暂时设置为 5秒 用来测试
sessionManager.setSessionValidationInterval(3600000);
//取消url 后面的 JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
}
2. CustomRealm
CustomRealm 继承了AuthorizingRealm 类, 用来进行认证和鉴权的操作
public class CustomRealm extends AuthorizingRealm {
/**
* 权限认证
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = UserConstants.map.get(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
//添加权限
for (Permissions permissions : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
}
}
return simpleAuthorizationInfo;
}
/**
* 认证认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = UserConstants.map.get(name);
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword(), getName());
return simpleAuthenticationInfo;
}
}
}
3. ShiroDemoController
这里是测试用的控制层,为了简单,这里直接没有业务层,使用模拟数据
/**
* @Email : kingfishx@163.com
* @Data: 2020/11/19 14:04
* @Des:
*/
@RequestMapping("shiro")
@RestController
public class ShiroDemoController {
@PostMapping("admin")
// 需要admin角色,这里就说明只有 “张三”的账号才能访问
@RequiresRoles("admin")
public String admin() {
return "admin";
}
@PostMapping("user")
// 需要user角色, 这里只有“李四的账号才能访问”
@RequiresRoles("user")
public String user(HttpServletRequest request) {
HttpSession session = request.getSession(true);
return "user";
}
@PostMapping("login")
public String login(HttpServletRequest request, HttpServletResponse response) {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("张三", "123456");
Subject subject = SecurityUtils.getSubject();
subject.login(usernamePasswordToken);
return "login";
}
}
4. pojo 类
@Data
public class Permissions {
private String id;
private String permissionsName;
}
@Data
public class Role {
private String id;
private String roleName;
/**
* 角色对应权限集合
*/
private Set<Permissions> permissions;
public Role() {
}
public Role(String id, String roleName, Set<Permissions> permissions) {
this.id = id;
this.roleName = roleName;
this.permissions = permissions;
}
}
@Data
public class User {
private String id;
private String userName;
private String password;
/**
* 用户对应的角色集合
*/
private Set<Role> roles;
public User(String id, String userName, String password, Set<Role> roles) {
this.id = id;
this.userName = userName;
this.password = password;
this.roles = roles;
}
public User() {
}
}
5. UserConstants
这里对User 用户进行数据模拟
/**
* @Email : kingfishx@163.com
* @Data : 2021/1/28 10:54
* @Desc : 模拟角色
*/
public class UserConstants {
/**
* 两个角色
* 1. 张三 -》admin :具有 query,add 权限
* 2. 李四 -》user : 具有 query 权限
*/
public static Map<String, User> map = new HashMap<>();
static {
Permissions addPermissions = new Permissions("1", "query");
Permissions queryPermissions = new Permissions("2", "add");
Role adminRole = new Role("1", "admin", Sets.newHashSet(addPermissions, queryPermissions));
Role userRole = new Role("2", "user", Sets.newHashSet(queryPermissions));
User user = new User("1", "张三", "123456", Sets.newHashSet(adminRole));
User user1 = new User("2", "李四", "123456", Sets.newHashSet(userRole));
map.put(user.getUserName(), user);
map.put(user1.getUserName(), user1);
}
}
6. 异常拦截
全局异常拦截,方便看返回
@ControllerAdvice
@Slf4j
public class ShiroExceptionHandler {
@ExceptionHandler
@ResponseBody
public String ErrorHandler(AuthorizationException e) {
log.error("没有通过权限验证!", e);
return "没有通过权限验证! " + e.getMessage();
}
}
三、验证
当我们使用张三账号登录后,是无权访问user接口的,但是却可以访问 admin接口
四、 shiro-redis 的集成
Shiro 集成 Redis 可以通过引入 shiro-redis 依赖实现
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
使用方式也很简单,直接注入RedisSessionDao 即可:
@Bean
public SessionDAO sessionDao(JedisPool jedisPool) {
// 使用redis
RedisSessionDAO sessionDAO = new RedisSessionDAO();
RedisManager redisManager = new RedisManager();
redisManager.setJedisPool(jedisPool);
sessionDAO.setExpire(10000);
sessionDAO.setRedisManager(redisManager);
return sessionDAO;
}
1. 问题
但是由于其已经不再更新维护,以及jedis 2.x版本和 3.x版本的差异性(具体体现在哪个版本暂未考究),在某些情况下,如果项目引入了 jedis3.x 版本时会无法兼容,会出现如下错误
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
***************************
APPLICATION FAILED TO START
***************************
Description:
An attempt was made to call a method that does not exist. The attempt was made from the following location:
org.crazycake.shiro.WorkAloneRedisManager.keys(WorkAloneRedisManager.java:149)
The following method did not exist:
redis.clients.jedis.ScanResult.getStringCursor()Ljava/lang/String;
The method's class, redis.clients.jedis.ScanResult, is available from the following locations:
jar:file:/F:/maven/maven-jar-home/redis/clients/jedis/3.3.0/jedis-3.3.0.jar!/redis/clients/jedis/ScanResult.class
The class hierarchy was loaded from the following locations:
redis.clients.jedis.ScanResult: file:/F:/maven/maven-jar-home/redis/clients/jedis/3.3.0/jedis-3.3.0.jar
Action:
Correct the classpath of your application so that it contains a single, compatible version of redis.clients.jedis.ScanResult
Disconnected from the target VM, address: '127.0.0.1:54508', transport: 'socket'
Process finished with exit code 1
究其原因是因为在 jedis 2.x 时 redis.clients.jedis.ScanResult#getStringCursor
在 jedis 3.x 中被替换成了 redis.clients.jedis.ScanResult#getCursor
。而 org.crazycake.shiro.WorkAloneRedisManager#keys
中调用的仍是 ScanResult#getStringCursor
。
2. 解决方案
2.1 jedis 降级
这个是最直接的方案,将jedis 降级到 2.9 版本。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2.2 ShiroRedisManager
但是某些情况下,我们不能随意降级 jedis 的版本,便可以选择重写RedisManager。
我直接使用了反射来获取方法,获取到哪个执行哪个。
public class ShiroRedisManager extends RedisManager {
private static Method cursorMethod;
public ShiroRedisManager() {
Class<ScanResult> scanResultClass = ScanResult.class;
try {
cursorMethod = scanResultClass.getMethod("getStringCursor");
} catch (NoSuchMethodException e) {
try {
cursorMethod = scanResultClass.getMethod("getCursor");
} catch (NoSuchMethodException noSuchMethodException) {
}
}
cursorMethod.setAccessible(true);
}
@Override
public Set<byte[]> keys(byte[] pattern) {
Set<byte[]> keys = new HashSet<byte[]>();
Jedis jedis = getJedis();
try {
ScanParams params = new ScanParams();
params.count(getCount());
params.match(pattern);
byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY;
ScanResult<byte[]> scanResult;
do {
scanResult = jedis.scan(cursor, params);
keys.addAll(scanResult.getResult());
cursor = scanResult.getCursorAsBytes();
} while (((String)cursorMethod.invoke(scanResult)).compareTo(ScanParams.SCAN_POINTER_START) > 0);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
} finally {
jedis.close();
}
return keys;
}
}
2.3 RedisSessionDAO
上面的两种方案都不适合解决我目前的问题,所以干脆重写了 RedisSessionDAO 。RedisSessionDAO 使用了 RedisTemplate 来完成Redis 的操作,其定义过程参考了 shiro-redis 的实现,仅在Session操作部分有所替换。使用的时候不再依赖 Jedis,而是依赖 RedisConnectionFactory。
package com.yunjiahealth.shiro.constants.config;
import com.google.common.collect.Lists;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.crazycake.shiro.SessionInMemory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
import java.util.*;
/**
* @Data: 2021/4/1 10:23
* @Des: 自定义 RedisSessionDAO 解决 Shiro redis 集成版本不兼容方面问题
*/
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;
/**
* doReadSession be called about 10 times when login.
* Save Session in ThreadLocal to resolve this problem. sessionInMemoryTimeout is expiration of Session in ThreadLocal.
* The default value is 1000 milliseconds (1s).
* Most of time, you don't need to change it.
*/
private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT;
private static final boolean DEFAULT_SESSION_IN_MEMORY_ENABLED = true;
private boolean sessionInMemoryEnabled = DEFAULT_SESSION_IN_MEMORY_ENABLED;
// expire time in seconds
private static final int DEFAULT_EXPIRE = -2;
private static final int NO_EXPIRE = -1;
/**
* Please make sure expire is longer than sesion.getTimeout()
*/
private int expire = DEFAULT_EXPIRE;
private static ThreadLocal sessionsInThread = new ThreadLocal();
private RedisTemplate<Serializable, Session> redisTemplate;
private RedisConnectionFactory redisConnectionFactory;
public RedisConnectionFactory getRedisConnectionFactory() {
return redisConnectionFactory;
}
public void setRedisConnectionFactory(RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
}
public RedisTemplate<Serializable, Session> getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate<Serializable, Session> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@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);
redisTemplate.opsForValue().set(getRedisSessionKey(sessionId), session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
logger.warn("session id is null");
return null;
}
if (this.sessionInMemoryEnabled) {
Session session = getSessionFromThreadLocal(sessionId);
if (session != null) {
return session;
}
}
logger.debug("read session from redis");
Session session = redisTemplate.opsForValue().get(getRedisSessionKey(sessionId));
if (this.sessionInMemoryEnabled) {
setSessionToThreadLocal(sessionId, session);
}
return session;
}
@Override
public void update(Session session) throws UnknownSessionException {
redisTemplate.opsForValue().set(getRedisSessionKey(session.getId()), session);
if (this.sessionInMemoryEnabled) {
this.setSessionToThreadLocal(session.getId(), session);
}
}
@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return;
}
redisTemplate.delete(getRedisSessionKey(session.getId()));
}
@Override
public Collection<Session> getActiveSessions() {
Set<Serializable> keys = redisTemplate.keys(this.keyPrefix);
if (null != keys) {
// 批量获取数据
return redisTemplate.opsForValue().multiGet(keys);
} else {
return Lists.newArrayListWithCapacity(0);
}
}
private void removeExpiredSessionInMemory(Map<Serializable, SessionInMemory> sessionMap) {
Iterator<Serializable> it = sessionMap.keySet().iterator();
while (it.hasNext()) {
Serializable sessionId = it.next();
SessionInMemory sessionInMemory = sessionMap.get(sessionId);
if (sessionInMemory == null) {
it.remove();
continue;
}
long liveTime = getSessionInMemoryLiveTime(sessionInMemory);
if (liveTime > sessionInMemoryTimeout) {
it.remove();
}
}
}
private void setSessionToThreadLocal(Serializable sessionId, Session s) {
Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
if (sessionMap == null) {
sessionMap = new HashMap<>();
sessionsInThread.set(sessionMap);
}
removeExpiredSessionInMemory(sessionMap);
SessionInMemory sessionInMemory = new SessionInMemory();
sessionInMemory.setCreateTime(new Date());
sessionInMemory.setSession(s);
sessionMap.put(sessionId, sessionInMemory);
}
private Session getSessionFromThreadLocal(Serializable sessionId) {
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;
}
long liveTime = getSessionInMemoryLiveTime(sessionInMemory);
if (liveTime > sessionInMemoryTimeout) {
sessionMap.remove(sessionId);
return null;
}
logger.debug("read session from memory");
return sessionInMemory.getSession();
}
private long getSessionInMemoryLiveTime(SessionInMemory sessionInMemory) {
Date now = new Date();
return now.getTime() - sessionInMemory.getCreateTime().getTime();
}
private String getRedisSessionKey(Serializable sessionId) {
return this.keyPrefix + sessionId;
}
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;
}
public boolean getSessionInMemoryEnabled() {
return sessionInMemoryEnabled;
}
public void setSessionInMemoryEnabled(boolean sessionInMemoryEnabled) {
this.sessionInMemoryEnabled = sessionInMemoryEnabled;
}
public static ThreadLocal getSessionsInThread() {
return sessionsInThread;
}
}
以上:内容部分参考
https://blog.csdn.net/dgh112233/article/details/100083287
https://www.zhihu.com/pin/1105962164963282944
https://blog.csdn.net/qq_30643885/article/details/91886448
https://blog.csdn.net/u012437781/article/details/78356037
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正