文章目录
  1. 1. 项目搭建
  2. 2. Realm
    1. 2.1. 子类
    2. 2.2. 自定义Realm
    3. 2.3. 认证信息的缓存
    4. 2.4. 密码加密认证
  3. 3. 缓存管理器(CacheManager)
    1. 3.1. 清除缓存
    2. 3.2. 实现原理
  4. 4. 会话管理器(SessionManager)
    1. 4.1. 自定义SessionMananger
    2. 4.2. 自定义SessionDao
    3. 4.3. 自定义SessionId生成策略
    4. 4.4. 自定义Session监听器
    5. 4.5. 完成上述配置
    6. 4.6. 优化
    7. 4.7. 会话验证
      1. 4.7.1. 如何的Session是失效的
      2. 4.7.2. 何时是失效的
  5. 5. SSM+Shiro整合

项目搭建

  • 添加Shiro的依赖
1
2
3
4
5
6
7
8
9
10
11
<!--Spring整合的shiro依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>

Realm

  • Shiro的组件之一
  • 其各个子类分别有不同职责

子类

  • CachingRealm:提供了缓存的功能,其中配置了缓存管理器
  • AuthenticatingRealm:负责认证的Realm,继承了CachingRealm,因此对认证的信息也是有缓存的功能,默认是关闭的,其中有一个重要的方法,如下:
    • protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token):用于子类实现认证的真正的逻辑
  • AuthorizingRealm:负责授权的Realm,不过继承了AuthenticatingRealm,因此具有认证和缓存的功能

自定义Realm

  • 通常我们只需要完成认证授权,因此只需要继承AuthorizingRealm即可,如下:
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
/**
* 自定义的Realm,完成认证和授权
*/
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private RolePermissionMapper rolePermissionMapper;
@Override
public String getName() {
return "userRealm";
}
/**
* 完成授权,主要的作用就是从数据库中查询出用户的角色和权限封装在AuthorizationInfo返回即可
* @param principals 在认证的过程中返回的Principal,可以是一个User对象,也可以是userId等标志用户信息
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("授权。。。。");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
final HashSet<String> pers = Sets.newHashSet();
final HashSet<String> roles = Sets.newHashSet();
//获取用户信息
User user= (User) principals.getPrimaryPrincipal();
List<UserRole> userRole = userRoleMapper.selectByUserId(user.getId());
//如果userRole存在
if (CollectionUtils.isNotEmpty(userRole)){
//获取权限
userRole.stream().forEach(o->{
rolePermissionMapper.selectByRoleId(o.getRoleId()).stream().forEach(item->{
pers.add(item.getDesc());
});
//获取角色
Role role = roleMapper.selectById(o.getRoleId());
roles.add(role.getRoleName());
});
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(pers);
}
return authorizationInfo;
}
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("认证。。。。");
UsernamePasswordToken upToken= (UsernamePasswordToken) token;
//用户名
String userName = (String) upToken.getPrincipal();
User user = userMapper.selectByUserName(userName);
if (Objects.isNull(user)){
throw new AuthenticationException("用户不存在");
}
//该构造器还可以使用加密算法
return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
}
/**
* 清除CacheManager中的缓存,可以在用户权限改变的时候调用,这样再次需要权限的时候就会重新查询数据库不走缓存了
*/
public void clearCache() {
Subject subject = SecurityUtils.getSubject();
//此处调用父类的方法,不仅会清除授权缓存,如果认证信息也缓存了,那么也会删除认证的缓存
super.clearCache(subject.getPrincipals());
}
}
  • 将其配置到安全管理器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 配置UserRealm,完成认证和授权的两个流程
*/
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
/**
* 配置安全管理器
*/
@Bean
public SecurityManager securityManager(){
//使用web下的安全管理器,构造参数传入Realm
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(userRealm());
//设置缓存管理器
securityManager.setCacheManager(cacheManager());
//设置会话管理器
securityManager.setSessionManager(sessionManager());
return securityManager;
}

认证信息的缓存

  • 默认是关闭的,只需要开启即可。
1
2
3
4
5
6
7
8
9
10
11
@Bean
public UserRealm userRealm(){
UserRealm userRealm = new UserRealm();
//开启认证信息的缓存,默认关闭,key是UserNamePasswordToken,value就是principle
userRealm.setAuthenticationCachingEnabled(true);
//开启授权信息的缓存,默认开启
userRealm.setAuthorizationCachingEnabled(true);
return userRealm;
}
//SecurityManager中设置缓存管理器
  • 认证信息缓存也需要在用户的个人信息改变的时候清除缓存

密码加密认证

  • 在正常的场景中都会涉及到对密码的加密,在Shiro中也提供了密码加密的认证,只需要配置一个凭证匹配器即可,步骤如下:

    • 在自定义的UserRealm中配置凭证匹配器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Bean
    public UserRealm userRealm(){
    UserRealm userRealm = new UserRealm();
    //开启认证信息的缓存,默认关闭,key是UserNamePasswordToken,value就是principle
    userRealm.setAuthenticationCachingEnabled(false);
    //开启授权信息的缓存,默认开启
    userRealm.setAuthorizationCachingEnabled(true);
    //配置凭证匹配器,加密方式MD5
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("MD5");
    //加密两次
    credentialsMatcher.setHashIterations(2);
    //设置凭证匹配器
    userRealm.setCredentialsMatcher(credentialsMatcher);
    return userRealm;
    }
    • 在认证的方法中构建使用加密的AuthenticationInfo,如下:
    1
    2
    //第一个参数是principle,第二个参数是加密之后的密码,第三个参数是加密的盐,第四个参数是UserRealm的名称
    return new SimpleAuthenticationInfo(user,user.getPassword(),ByteSource.Util.bytes(user.getSalt()),getName());

缓存管理器(CacheManager)

  • 在每一次请求需要权限的时候总是会调用授权的方法查询数据库,这样的话性能很低,因此我们可以使用缓存管理器,来达到这种要求,在Shiro中有一个内存缓存管理器,内部就是使用Map实现的,但是这种缓存并不能实现跨JVM(分布式),因此我们可以使用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
    /**
    * Redis的Cache
    */
    public class RedisCache<K,V> implements Cache<K,V> {
    private RedisTemplate redisTemplate;
    /**
    * 存储在redis中的hash中的key
    */
    private String name;
    private final static String COMMON_NAME="shiro-demo";
    public RedisCache(RedisTemplate redisTemplate, String name) {
    this.redisTemplate = redisTemplate;
    this.name=COMMON_NAME+":"+name;
    }
    /**
    * 获取指定的key的缓存
    * @param k
    * @return
    * @throws CacheException
    */
    @Override
    public V get(K k) throws CacheException {
    return (V) redisTemplate.opsForHash().get(name,k);
    }
    /**
    * 添加缓存
    * @param k
    * @param v
    * @return
    * @throws CacheException
    */
    @Override
    public V put(K k, V v) throws CacheException {
    redisTemplate.opsForHash().put(name, k, v);
    //设置过期时间
    return v;
    }
    /**
    * 删除指定key的缓存
    * @param k 默认是principle对象,在AuthorizingRealm中设置
    */
    @Override
    public V remove(K k) throws CacheException {
    V v = this.get(k);
    redisTemplate.opsForHash().delete(name, k);
    return v;
    }
    /**
    * 删除所有的缓存
    */
    @Override
    public void clear() throws CacheException {
    redisTemplate.delete(name);
    }
    /**
    * 获取总数
    * @return
    */
    @Override
    public int size() {
    return redisTemplate.opsForHash().size(name).intValue();
    }
    @Override
    public Set<K> keys() {
    return redisTemplate.opsForHash().keys(name);
    }
    @Override
    public Collection<V> values() {
    return redisTemplate.opsForHash().values(name);
    }
    }
    • 实现RedisManager,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * Redis的CacheManager
    */
    public class RedisCacheManager implements CacheManager {
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
    return new RedisCache<K,V>(redisTemplate, s);
    }
    }
    • 在配置类中配置上缓存管理器,需要设置到Shiro的安全管理器中才能生效,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * 配置缓存管理器,使用自定义的Redis缓存管理器
    */
    @Bean
    public CacheManager cacheManager(){
    return new RedisCacheManager();
    }
    /**
    * 配置安全管理器
    */
    @Bean
    public SecurityManager securityManager(){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(userRealm());
    //设置缓存管理器
    securityManager.setCacheManager(cacheManager());
    return securityManager;
    }

清除缓存

  • 在CachingRelam中有一个清除缓存的方法org.apache.shiro.realm.CachingRealm#clearCache,在我们自定义的Realm中覆盖该方法即可,这样就能在退出或者在业务逻辑中用户的权限改变的时候能够清除缓存的数据,如下:
1
2
3
4
5
6
7
8
/**
* 清除CacheManager中的缓存,可以在用户权限改变的时候调用,这样再次需要权限的时候就会重新查询数据库不走缓存了
*/
public void clearCache() {
Subject subject = SecurityUtils.getSubject();
//调用父类的清除缓存的方法
super.clearCache(subject.getPrincipals());
}
  • 除了重写或者覆盖CachingRelam中的方法,根据源码可以知道,真正起作用的方法是AuthorizingRealm中的方法clearCachedAuthorizationInfo,因此我们也可以重写或者覆盖这个方法,这里不再演示。

实现原理

  • 在Shiro中一旦有地方调用Subject.hasRole等校验权限的地方,那么就会检测授权信息,在org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo的方法中会先缓存中查询是否存在,否则调用授权的方法从数据库中查询,查询之后放入缓存中,源码如下:
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
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
return null;
}
AuthorizationInfo info = null;
if (log.isTraceEnabled()) {
log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
}
//获取可用的缓存管理器
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
if (cache != null) {
if (log.isTraceEnabled()) {
log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
}
//获取缓存的key,这里获取的就是principal主体信息
Object key = getAuthorizationCacheKey(principals);
//从缓存中获取数据
info = cache.get(key);
if (log.isTraceEnabled()) {
if (info == null) {
log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
} else {
log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
}
}
}
//如果缓存中没有查到
if (info == null) {
//调用重写的授权方法,从数据库中查询
info = doGetAuthorizationInfo(principals);
//如果查询到了,添加到缓存中
if (info != null && cache != null) {
if (log.isTraceEnabled()) {
log.trace("Caching authorization info for principals: [" + principals + "].");
}
//获取缓存的key
Object key = getAuthorizationCacheKey(principals);
//放入缓存
cache.put(key, info);
}
}
return info;
}

会话管理器(SessionManager)

  • Shiro在开启Web功能的时候默认的会话管理器是DefaultWebSessionManager,这种管理器是针对cookie进行存储的,将sessionId存储在cookie中,但是现在的主流方向是前后端分离,我们不能再依赖Cookie,因此我们必须自定义的会话管理器,实现跨JVM,前后端分离。

自定义SessionMananger

  • 在原有的DefaultWebSessionManager进行扩展,否则从头实现将会要写大量代码。默认的Web的会话管理器是从cookie中获取SessionId,我们只需要重写其中的方法即可。
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
/**
* 自定义的会话管理器
*/
@Slf4j
public class RedisSessionManager extends DefaultWebSessionManager {
@Autowired
private RedisTemplate redisTemplate;
/**
* 前后端分离不存在cookie,因此需要重写getSessionId的逻辑,从请求参数中获取
* 此处的逻辑:在登录成功之后会将sessionId作为一个token返回,下次请求的时候直接带着token即可
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取上传的token,这里的token就是sessionId
return request.getParameter("token");
}
/**
* 重写该方法,在SessionManager中只要涉及到Session的操作都会获取Session,获取Session主要是从缓存中获取,父类的该方法执行逻辑如下:
* 1、先从RedisCache中获取,调用get方法
* 2、如果RedisCache中不存在,在从SessionDao中获取,调用get方法
* 优化:我们只需要从SessionDao中获取即可
* @param sessionKey Session的Key
*/
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
//获取SessionId
Serializable sessionId = getSessionId(sessionKey);
if (sessionId == null) {
log.debug("Unable to resolve session ID from SessionKey [{}]. Returning null to indicate a " +
"session could not be found.", sessionKey);
return null;
}
//直接调用SessionDao中的get方法获取
Session session = ((RedisSessionDao) sessionDAO).doReadSession(sessionId);
if (session == null) {
//session ID was provided, meaning one is expected to be found, but we couldn't find one:
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
}
return session;
}
/**
* 该方法是作用是当访问指定的uri的时候会更新Session中的执行时间,用来动态的延长失效时间。
* 在父类的实现方法会直接调用SessionDao中的更新方法更新缓存中的Session
* 此处并没有其他的逻辑,后续可以补充
* @param key
*/
@Override
public void touch(SessionKey key) throws InvalidSessionException {
super.touch(key);
}
}

自定义SessionDao

  • SessionDao的作用是Session持久化的手段,默认的SessionDao是缓存在内存中的,此处使用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
/**
* 自定义RedisSessionDao,继承CachingSessionDAO
*/
@Slf4j
public class RedisSessionDao extends CachingSessionDAO {
@Autowired
private RedisTemplate redisTemplate;
/**
* 更新session
* @param session
*/
@Override
protected void doUpdate(Session session) {
log.info("执行redisdao的doUpdate方法");
redisTemplate.opsForValue().set(session.getId(), session);
}
/**
* 删除session
* @param session
*/
@Override
protected void doDelete(Session session) {
log.info("执行redisdao的doDelete方法");
redisTemplate.delete(session.getId());
}
/**
* 创建一个Session,添加到缓存中
* @param session Session信息
* @return 创建的SessionId
*/
@Override
protected Serializable doCreate(Session session) {
log.info("执行redisdao的doCreate方法");
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(session.getId(), session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
log.info("执行redisdao的doReadSession方法");
return (Session) redisTemplate.opsForValue().get(sessionId);
}
}

自定义SessionId生成策略

  • 默认的Shiro的生成策略是JavaUuidSessionIdGenerator,此处也可以自定义自己的生成策略,如下:
1
2
3
4
5
6
7
8
9
/**
* 自定义的SessionId的生成策略
*/
public class RedisSessionIdGenerator implements SessionIdGenerator {
@Override
public Serializable generateId(Session session) {
return UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
}
}

自定义Session监听器

  • Session监听器能够监听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
/**
* 自定义Session监听器
*/
@Slf4j
public class RedisSessionListener implements SessionListener {
@Override
public void onStart(Session session) {
log.info("开始");
}
/**
* Session无效【停止了,stopTime!=null】
* @param session
*/
@Override
public void onStop(Session session) {
log.info("session失效");
}
@Override
public void onExpiration(Session session) {
log.info("超时");
}
}

完成上述配置

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
/**
* 配置SessionDao,使用自定义的Redis缓存
*/
@Bean
public SessionDAO sessionDAO(){
RedisSessionDao sessionDao = new RedisSessionDao();
//设置自定义的Id生成策略
sessionDao.setSessionIdGenerator(new RedisSessionIdGenerator());
return sessionDao;
}
/**
* 配置会话监听器
* @return
*/
@Bean
public SessionListener sessionListener(){
return new RedisSessionListener();
}
/**
* 配置会话管理器
*/
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager = new RedisSessionManager();
//设置session的过期时间
sessionManager.setGlobalSessionTimeout(60000);
//设置SessionDao
sessionManager.setSessionDAO(sessionDAO());
//设置SessionListener
sessionManager.setSessionListeners(Lists.newArrayList(sessionListener()));
return sessionManager;
}
/**
* 配置安全管理器
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(userRealm());
//设置缓存管理器
securityManager.setCacheManager(cacheManager());
//设置会话管理器
securityManager.setSessionManager(sessionManager());
return securityManager;
}

优化

  • 在源码中可以看到AbstractSessionDAO中的增删改查方法的执行逻辑使用的双层缓存的,还设计到查询CacheManager中的缓存,但是我们的SessionDao既然是实现了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
/**
* RedisSessionDao的抽象类,重写其中的增删改查方法,原因如下:
* 1、AbstractSessionDAO中的默认方法是写查询CacheManager中的缓存,既然SessionDao实现了Redis的缓存
* 那么就不需要重复查询两次,因此重写了方法,直接使用RedisSessionDao查询即可。
*/
public abstract class AbstractRedisSessionDao extends AbstractSessionDAO {
/**
* 重写creat方法,直接执行sessionDao的方法,不再执行cacheManager
* @param session
* @return
*/
@Override
public Serializable create(Session session) {
Serializable sessionId = doCreate(session);
if (sessionId == null) {
String msg = "sessionId returned from doCreate implementation is null. Please verify the implementation.";
throw new IllegalStateException(msg);
}
return sessionId;
}
/**
* 重写删除操作
* @param session
*/
@Override
public void delete(Session session) {
doDelete(session);
}
/**
* 重写update方法
* @param session
* @throws UnknownSessionException
*/
@Override
public void update(Session session) throws UnknownSessionException {
doUpdate(session);
}
/**
* 重写查找方法
* @param sessionId
* @return
* @throws UnknownSessionException
*/
@Override
public Session readSession(Serializable sessionId) throws UnknownSessionException {
Session s = doReadSession(sessionId);
if (s == null) {
throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
}
return s;
}
protected abstract void doDelete(Session session);
protected abstract void doUpdate(Session session);
}
  • 此时的上面的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
/**
* 自定义RedisSessionDao,继承AbstractRedisSessionDao,达到只查一层缓存
*/
@Slf4j
public class RedisSessionDao extends AbstractRedisSessionDao {
@Autowired
private RedisTemplate redisTemplate;
private final static String HASH_NAME="shiro_user";
/**
* 更新session
* @param session
*/
@Override
protected void doUpdate(Session session) {
log.info("执行redisdao的doUpdate方法");
redisTemplate.opsForHash().put(HASH_NAME, session.getId(), session);
}
/**
* 删除session
* @param session
*/
@Override
protected void doDelete(Session session) {
log.info("执行redisdao的doDelete方法");
redisTemplate.opsForHash().delete(HASH_NAME, session.getId());
}
/**
* 创建一个Session,添加到缓存中
* @param session Session信息
* @return 创建的SessionId
*/
@Override
protected Serializable doCreate(Session session) {
log.info("执行redisdao的doCreate方法");
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
redisTemplate.opsForHash().put(HASH_NAME, session.getId(),session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
log.info("执行redisdao的doReadSession方法");
return (Session) redisTemplate.opsForHash().get(HASH_NAME,sessionId);
}
/**
* 获取所有的Session
*/
@Override
public Collection<Session> getActiveSessions() {
List values = redisTemplate.opsForHash().values(HASH_NAME);
if (CollectionUtils.isNotEmpty(values)){
return values;
}
return Collections.emptySet();
}
}

会话验证

  • Shiro默认是在当前用户访问页面的时候检查Session是否停止或者过期,如果过期和停止了会调用的SessionDao中的相关方法删除缓存,但是如果这是在用户名操作的情况下,如果用户一直未操作,那么Session已经失效了,但是缓存中并没有删除,这样一来将会有大量无效的Session堆积,因此我们必须定时清理失效的Session。
  • 清理会话,有如下两种方法:
    • 自己写一个定时器,每隔半小时或者几分钟清除清除缓存
    • 自定义SessionValidationScheduler
    • 使用已经实现的ExecutorServiceSessionValidationScheduler
  • 在Shiro中默认会开启ExecutorServiceSessionValidationScheduler,执行时间是一个小时,但是如果想要使用定时器定时清除的话,那么需要关闭默认的清除器,如下:
1
2
//禁用Session清除器,使用定时器清除
sessionManager.setSessionValidationSchedulerEnabled(false);

如何的Session是失效的

  • Session是如何保活的?
    • org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal中的一个updateSessionLastAccessTime(request, response);方法用来更新Session的最后执行时间为当前时间,最终调用的就是org.apache.shiro.session.mgt.SimpleSession#touch
    • 在每次请求验证Session的时候实际调用的是org.apache.shiro.session.mgt.AbstractValidatingSessionManager#doValidate方法,在其中真正调用的是org.apache.shiro.session.mgt.SimpleSession#validate来验证是否过期或者停止
      • 核心逻辑就是验证当前的时间和最后执行时间的差值是否在设置的过期时间的范围内

何时是失效的

  • Session失效目前通过读源码总结出如下三点:
    • isValid判断,这个会在访问请求的时候shiro会自动验证,并且设置进去
    • 用户长期不请求,此时的isValid并不能验证出来,此时需要比较最后执行的时间和开始时间比较
    • 没有登录就访问的也会在redis中生成一个Session,但是此时的Session中是没有两个属性的,以下的两个属性只有在认证成功之后才会设置查到Session中
      • org.apache.shiro.subject.support.DefaultSubjectContext#PRINCIPALS_SESSION_KEY
      • org.apache.shiro.subject.support.DefaultSubjectContext#AUTHENTICATED_SESSION_KEY
  • 通过上面的分析,此时就能写出从缓存中删除失效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
/**
* session过期有三种可能,如下:
* 1、isValid判断,这个会在访问请求的时候shiro会自动验证,并且设置进去
* 2、用户长期不请求,此时的isValid并不能验证出来,此时需要比较最后执行的时间和开始时间
* 3、没有登录就访问的也会在redis中生成一个Session,但是此时的Session中是没有两个属性的
* 1、org.apache.shiro.subject.support.DefaultSubjectContext#PRINCIPALS_SESSION_KEY
* 2、org.apache.shiro.subject.support.DefaultSubjectContext#AUTHENTICATED_SESSION_KEY
*/
public static void clearExpireSession(){
//获取所有的Session
Collection<Session> sessions = redisSessionDao.getActiveSessions();
sessions.forEach(s->{
SimpleSession session= (SimpleSession) s;
//第一种可能
Boolean status1=!session.isValid();
//第二种可能用开始时间和过期时间比较
Boolean status2=session.getLastAccessTime().getTime()+session.getTimeout()<new Date().getTime();
//第三种可能
Boolean status3= Objects.isNull(session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY))&&Objects.isNull(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY));
if (status1||status2||status3){
//清楚session
redisSessionDao.delete(session);
}
});
}

SSM+Shiro整合

文章目录
  1. 1. 项目搭建
  2. 2. Realm
    1. 2.1. 子类
    2. 2.2. 自定义Realm
    3. 2.3. 认证信息的缓存
    4. 2.4. 密码加密认证
  3. 3. 缓存管理器(CacheManager)
    1. 3.1. 清除缓存
    2. 3.2. 实现原理
  4. 4. 会话管理器(SessionManager)
    1. 4.1. 自定义SessionMananger
    2. 4.2. 自定义SessionDao
    3. 4.3. 自定义SessionId生成策略
    4. 4.4. 自定义Session监听器
    5. 4.5. 完成上述配置
    6. 4.6. 优化
    7. 4.7. 会话验证
      1. 4.7.1. 如何的Session是失效的
      2. 4.7.2. 何时是失效的
  5. 5. SSM+Shiro整合