文章目录
<!--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>
CachingRealm
:提供了缓存的功能,其中配置了缓存管理器AuthenticatingRealm
:负责认证的Realm,继承了CachingRealm,因此对认证的信息也是有缓存的功能,默认是关闭的,其中有一个重要的方法,如下:protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
:用于子类实现认证的真正的逻辑AuthorizingRealm
:负责授权的Realm,不过继承了AuthenticatingRealm
,因此具有认证和缓存的功能/**
* 自定义的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());
}
}
/**
* 配置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;
}
@Bean
public UserRealm userRealm(){
UserRealm userRealm = new UserRealm();
//开启认证信息的缓存,默认关闭,key是UserNamePasswordToken,value就是principle
userRealm.setAuthenticationCachingEnabled(true);
//开启授权信息的缓存,默认开启
userRealm.setAuthorizationCachingEnabled(true);
return userRealm;
}
//SecurityManager中设置缓存管理器
@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;
}
//第一个参数是principle,第二个参数是加密之后的密码,第三个参数是加密的盐,第四个参数是UserRealm的名称
return new SimpleAuthenticationInfo(user,user.getPassword(),ByteSource.Util.bytes(user.getSalt()),getName());
/**
* 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);
}
}
/**
* 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);
}
}
/**
* 配置缓存管理器,使用自定义的Redis缓存管理器
*/
@Bean
public CacheManager cacheManager(){
return new RedisCacheManager();
}
/**
* 配置安全管理器
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(userRealm());
//设置缓存管理器
securityManager.setCacheManager(cacheManager());
return securityManager;
}
org.apache.shiro.realm.CachingRealm#clearCache
,在我们自定义的Realm中覆盖该方法即可,这样就能在退出或者在业务逻辑中用户的权限改变的时候能够清除缓存的数据,如下:/**
* 清除CacheManager中的缓存,可以在用户权限改变的时候调用,这样再次需要权限的时候就会重新查询数据库不走缓存了
*/
public void clearCache() {
Subject subject = SecurityUtils.getSubject();
//调用父类的清除缓存的方法
super.clearCache(subject.getPrincipals());
}
AuthorizingRealm
中的方法clearCachedAuthorizationInfo
,因此我们也可以重写或者覆盖这个方法,这里不再演示。Subject.hasRole
等校验权限的地方,那么就会检测授权信息,在org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo
的方法中会先缓存中查询是否存在,否则调用授权的方法从数据库中查询,查询之后放入缓存中,源码如下: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;
}
/**
* 自定义的会话管理器
*/
@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);
}
}
/**
* 自定义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的生成策略
*/
public class RedisSessionIdGenerator implements SessionIdGenerator {
@Override
public Serializable generateId(Session session) {
return UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
}
}
/**
* 自定义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("超时");
}
}
/**
* 配置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的缓存,那么是没必要查询两次的,因此需要重写其中的方法,此时我们自己需要写一个抽象类覆盖其中的增删改查方法即可,如下:/**
* 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,继承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();
}
}
//禁用Session清除器,使用定时器清除
sessionManager.setSessionValidationSchedulerEnabled(false);
org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal
中的一个updateSessionLastAccessTime(request, response);
方法用来更新Session的最后执行时间为当前时间,最终调用的就是org.apache.shiro.session.mgt.SimpleSession#touch
。org.apache.shiro.session.mgt.AbstractValidatingSessionManager#doValidate
方法,在其中真正调用的是org.apache.shiro.session.mgt.SimpleSession#validate
来验证是否过期或者停止/**
* 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);
}
});
}