1.问题场景:在dev和test环境开发时候,分配的账号是多人共用的,当一个人修改权限后,调用shiro的清楚服务器sesionId后,当其他人再次修改权限信息时候,由于服务器的sessionId已经被全部清空,就会报 There is no session with id "XXX"的问题
2.解决方式:网上说的一般是由于SESSIONID和比如tomcat/jetty等使用的sessionId同名导致的,这个是一个原因。不过我的原因是由于服务器所有的sessionId被清空了导致的,所以做了限制:当一个用户登录时候,我会先清空这个账号的所有缓存信息,这样不会导致一个账号在多个地方登录。不过有些简单
package com.sq.transportmanage.gateway.service.common.shiro.realm;
import com.google.common.collect.Maps;
import com.sq.transportmanage.gateway.dao.entity.driverspark.CarAdmUser;
import com.sq.transportmanage.gateway.dao.entity.driverspark.SaasPermission;
import com.sq.transportmanage.gateway.dao.mapper.driverspark.ex.SaasPermissionExMapper;
import com.sq.transportmanage.gateway.dao.mapper.driverspark.ex.SaasRoleExMapper;
import com.sq.transportmanage.gateway.service.auth.MyDataSourceService;
import com.sq.transportmanage.gateway.service.common.constants.Constants;
import com.sq.transportmanage.gateway.service.common.constants.SaasConst;
import com.sq.transportmanage.gateway.service.common.dto.SaasPermissionDTO;
import com.sq.transportmanage.gateway.service.common.shiro.session.RedisSessionDAO;
import com.sq.transportmanage.gateway.service.util.BeanUtil;
import com.sq.transportmanage.gateway.service.util.MD5Utils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**认证 与 权限 **/
/**
* 这个就是shiro SSOLogin 的用户获取的属性配置
*/
@Component
public class UsernamePasswordRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(UsernamePasswordRealm.class);
@Autowired
private MyDataSourceService myDataSourceService;
@Autowired
private SaasPermissionExMapper saasPermissionExMapper;
@Autowired
private SaasRoleExMapper saasRoleExMapper;
@Autowired
@Qualifier("sessionDAO")
private RedisSessionDAO redisSessionDAO;
/**重写:获取用户的身份认证信息**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
logger.info( "[获取用户的身份认证信息开始]authenticationToken="+authenticationToken);
try {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
CarAdmUser adMUser = myDataSourceService.queryByAccount(token.getUsername());
//处理session 防止一个账号多处登录
try {
redisSessionDAO.clearRelativeSession(null,null,adMUser.getUserId());
} catch (Exception e) {
logger.info("=========清除session异常============");
}
SSOLoginUser loginUser = new SSOLoginUser();
loginUser.setId( adMUser.getUserId() );
loginUser.setLoginName( adMUser.getAccount() );
loginUser.setMobile( adMUser.getPhone() );
loginUser.setName( adMUser.getUserName() );
loginUser.setEmail(adMUser.getEmail());
loginUser.setType(null); //
loginUser.setStatus( adMUser.getStatus() );
loginUser.setAccountType( adMUser.getAccountType() );
loginUser.setLevel(adMUser.getLevel());
loginUser.setMerchantId(adMUser.getMerchantId());
loginUser.setSupplierIds(adMUser.getSuppliers());
String md5= null;
try {
md5 = MD5Utils.getMD5DigestBase64(loginUser.getMerchantId().toString());
} catch (NoSuchAlgorithmException e) {
logger.info("sign error" + e);
}
if(Constants.MANAGE_MD5.equals(md5)){
loginUser.setSuper(true);
}else {
loginUser.setSuper(false);
}
List<String> menuUrlList = saasPermissionExMapper.queryPermissionMenussOfUser(adMUser.getUserId());
loginUser.setMenuUrlList(menuUrlList);
/**当前用户所拥有的菜单权限**/
List<Integer> permissionIds = saasPermissionExMapper.queryPermissionIdsOfUser(adMUser.getUserId());
List<Byte> permissionTypes = Arrays.asList( new Byte[] { SaasConst.PermissionType.MENU });
Map<Integer,List<SaasPermissionDTO>> mapPermission = Maps.newHashMap();
/**查询所有的一级菜单**/
if(!CollectionUtils.isEmpty(permissionIds)){
List<SaasPermission> permissionList = saasPermissionExMapper.queryModularPermissions(permissionIds);
Map<Integer,String> map = Maps.newHashMap();
permissionList.forEach(list ->{
map.put(list.getPermissionId(),list.getPermissionName());
//查询所有一级菜单下的子菜单 以树形结果返回
List<SaasPermissionDTO> menuPerms = this.getChildren( permissionIds , list.getPermissionId(), permissionTypes);
mapPermission.put(list.getPermissionId(),menuPerms);
});
loginUser.setMenuPermissionMap(map);
loginUser.setMapPermission(mapPermission);
}
//
//---------------------------------------------------------------------------------------------------------数据权限BEGIN
logger.info( "[获取用户的身份认证信息]="+loginUser);
return new SimpleAuthenticationInfo(loginUser, authenticationToken.getCredentials() , this.getName() );
} catch (Exception e) {
logger.error("获取用户的身份认证信息异常",e);
return null;
}
}
/**
* 查询每个一级菜单下的子菜单
* @param permissionIds
* @param parentPermissionId
* @param permissionTypes tree 树形,list 列表
* @return
*/
private List<SaasPermissionDTO> getChildren( List<Integer> permissionIds, Integer parentPermissionId, List<Byte> permissionTypes ){
List<SaasPermission> childrenPos = saasPermissionExMapper.queryPermissions(permissionIds, parentPermissionId, null, permissionTypes, null, null);
if(childrenPos==null || childrenPos.size()==0) {
return null;
}
//递归
List<SaasPermissionDTO> childrenDtos = BeanUtil.copyList(childrenPos, SaasPermissionDTO.class);
Iterator<SaasPermissionDTO> iterator = childrenDtos.iterator();
while (iterator.hasNext()) {
SaasPermissionDTO childrenDto = iterator.next();
List<SaasPermissionDTO> childs = this.getChildren( permissionIds, childrenDto.getPermissionId() , permissionTypes );
childrenDto.setChildPermissions(childs);
}
return childrenDtos;
}
/**
* 查询角色登录进来所拥有的菜单时候shiro实现
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SSOLoginUser loginUser = (SSOLoginUser) principalCollection.getPrimaryPrincipal();
String account = loginUser.getLoginName(); //登录名
List<String> perms_string = saasPermissionExMapper.queryPermissionCodesOfUser( loginUser.getId() );
List<String> roles_string = saasRoleExMapper.queryRoleCodesOfUser( loginUser.getId() );
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet<String>( roles_string );
authorizationInfo.setRoles( roles );
logger.info( "[获取用户授权信息(角色)] "+account+"="+roles);
Set<String> perms = new HashSet<String>( perms_string );
authorizationInfo.setStringPermissions(perms);
logger.info( "[获取用户授权信息(权限)] "+account+"="+perms);
return authorizationInfo;
}
@Override
public Object getAuthorizationCacheKey(PrincipalCollection principals) {
SSOLoginUser loginUser = (SSOLoginUser) principals.getPrimaryPrincipal();
String account = loginUser.getLoginName(); //登录名
return "-AuthInfo-"+account;
}
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
}
/**二、当权限信息、角色信息、用户信息发生变化时,同时清理与之相关联的会话**/
@MyDataSource(value = DataSourceType.DRIVERSPARK_MASTER)
public void clearRelativeSession( final Integer permissionId, final Integer roleId, final Integer userId ) {
final Cache<Serializable, Session> cache = super.getActiveSessionsCache();
//final Cache<Serializable, Session> cache = activeSessions;
new Thread(new Runnable() {
@SuppressWarnings("unchecked")
@Override
public void run() {
try{
//A:如果当权限发生变化时,查询所关联的全部角色ID
List<Integer> roleIds = new ArrayList<Integer>();
if( permissionId!=null ) {
roleIds = myDataSourceService.queryRoleIdsOfPermission( permissionId );
}
//B:如果当角色发生变化时,查询所关联的用户ID
if( roleId !=null ) {
roleIds.add(roleId);
}
List<Integer> userIds = new ArrayList<Integer>();
if( roleIds.size()>0 ) {
userIds = myDataSourceService.queryUserIdsOfRole( roleIds );
}
//C:如果当用户发生变化时,查询出这些用户的登录账户名称
if( userId != null ) {
logger.info("当用户发生变化时清除缓存userId={}",userId);
userIds.add(userId);
}
List<String> accounts = new ArrayList<String>();
if(userIds.size()>0) {
accounts = myDataSourceService.queryAccountsOfUsers(userIds);
}
//D:汇总需要清理的REDIS KEY 和 sessionId
if(accounts.size() ==0) {
return;
}
Set<String> redisKeysNeedDelete = new HashSet<String>();//这是需要清除的所有REDIS KEY
Set<String> allSessionIds = new HashSet<String>();//这是需要清除的所有的sessionId
for( String account : accounts) {
redisKeysNeedDelete.add( KEY_PREFIX_OF_SESSIONID + account );
Set<String> sessionIds = (Set<String>) redisTemplate.opsForValue().get(KEY_PREFIX_OF_SESSIONID+account);
if(sessionIds!=null && sessionIds.size()>0) {
allSessionIds.addAll(sessionIds);
}
}
//E1:执行清除执久化的会话(这里是保存在REDIS中的)
for( String sessionId : allSessionIds) {
logger.info("执行清除REDIS的会话缓存sessionId={}",sessionId);
redisKeysNeedDelete.add( KEY_PREFIX_OF_SESSION + sessionId );
}
redisTemplate.delete(redisKeysNeedDelete);
//E2:执行清理shiro会话缓存
if(cache!=null) {
for(String sessionId : allSessionIds ){
SimpleSession session = (SimpleSession)cache.get(sessionId);
if(session!=null) {
session.setExpired(true);
}
logger.info("执行清理shiro会话缓存sessionId={}",sessionId);
cache.remove(sessionId);
}
}
//E3:执行清理shiro 认证与授权缓存
for( String account : accounts) {
logger.info("执行清理shiro 认证与授权缓存account={}",account);
//todo 此处不合理,应该用下面的代码 这个是临时方案:执行退出的操作 相当于手动点击退出 这个触发条件太广泛了 要加很多逻辑判断那些人需要退出
/*Subject subject = SecurityUtils.getSubject();
if(subject.isAuthenticated()) {
subject.logout();
}*/
SSOLoginUser principal = new SSOLoginUser();
principal.setLoginName( account );
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection( );
simplePrincipalCollection.add(principal, authorizingRealm.getName() );
((UsernamePasswordRealm)authorizingRealm).clearCache( simplePrincipalCollection );
}
}catch(Exception ex) {
logger.error("清除缓存异常",ex);
}finally {
//DynamicRoutingDataSource.setDefault("mdbcarmanage-DataSource");
}
}
}).start();
}