第十一章 分布式统一权限系统
1、系统需求
【1】前后端分离
在第十章中我们已经实现,使用jwt的令牌实现,重写DefaultWebSessionManager,从ServletRequest获得jwtToken作为会话sessionId
package com.itheima.shiro.core.impl;
import com.itheima.shiro.utils.EmptyUtil;
import io.jsonwebtoken.Claims;
import org.apache.shiro.web.servlet.ShiroHttpServlet request ;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans. factory .annotation.Autowired;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* @Description 重写Jwt会话管理
*/
public class ShiroSessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "jwtToken";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public ShiroSessionManager(){
super();
}
@Autowired
JwtTokenManager jwtTokenManager;
@ Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response){
String jwtToken = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if(EmptyUtil.isNullOrEmpty(jwtToken)){
//如果没有携带id参数则按照父类的方式在cookie进行获取
return super.getSessionId(request, response);
}else{
//如果请求头中有 authToken 则其值为jwtToken,然后解析出会话session
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE);
Claims decode = jwtTokenManager.decodeToken(jwtToken);
String id = (String) decode.get("jti");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE);
return id;
}
}
}
【2】集中式会话
在第七章中RedisSessionDao继承AbstractSessionDAO,重写了会话的创建、读取、修改等操作,全部缓存于redis中
package com.itheima.shiro.core.impl;
import com.itheima.shiro.constant.CacheConstant;
import com.itheima.shiro.utils.ShiroRedissionSerialize;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.redisson. api .RBucket;
import org.redisson.api.RedissonClient;
import javax.annotation. Resource ;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @Description 实现shiro session的memcached集中式管理~
*/@Log4j2
public class RedisSessionDao extends AbstractSessionDAO {
@Resource(name = "redissonClientForShiro")
RedissonClient redissonClient;
private Long globalSessionTimeout;
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
// log.info("=============创建sessionId:{}",sessionId);
RBucket<String> sessionIdRBucket = redissonClient.getBucket(CacheConstant.GROUP_CAS+sessionId. toString ());
sessionIdRBucket.trySet(ShiroRedissionSerialize.serialize(session), globalSessionTimeout, TimeUnit.SECONDS);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
RBucket<String> sessionIdRBucket = redissonClient.getBucket(CacheConstant.GROUP_CAS+sessionId.toString());
Session session = (Session) ShiroRedissionSerialize.deserialize(sessionIdRBucket.get());
// log.info("=============读取sessionId:{}",session.getId().toString());
return session;
}
@Override
public void delete(Session session) {
// log.info("=============删除sessionId:{}",session.getId().toString());
RBucket<String> sessionIdRBucket = redissonClient.getBucket(CacheConstant.GROUP_CAS+session.getId().toString());
sessionIdRBucket.delete();
}
@Override
public Collection<Session> getActiveSessions() {
return Collections.emptySet();
}
@Override
public void update(Session session) {
RBucket<String> sessionIdRBucket = redissonClient.getBucket(CacheConstant.GROUP_CAS+session.getId().toString());
sessionIdRBucket.set(ShiroRedissionSerialize.serialize(session), globalSessionTimeout, TimeUnit.SECONDS);
// log.info("=============修改sessionId:{}",session.getId().toString());
}
public void setGlobalSessionTimeout(Long globalSessionTimeout) {
this.globalSessionTimeout = globalSessionTimeout;
}
}
【3】认证与 鉴权 服务化
第六章中,我们实现了realm的缓存机制,这里我们会把UserBridgeService使用dubbo服务化
其目的使得实际项目中的认证与鉴权走dubbo,减少服务器压力
【4】动态过滤器链
在第十章中,我们加载过滤器链的方式
#静态资源不过滤
/static/**=anon
#登录链接不过滤
/login/**=anon
#访问/resource/**需要有admin的角色
#/resource/**=role-or[MangerRole,SuperAdmin]
#/role/** =jwt-roles[SuperAdmin]
/resource/** =jwt-perms[role:listInitialize]
#其他链接是需要登录的
/**=kicked-out,jwt-authc
在统一鉴权系统中,我们不可能每次发布新的过滤器链,就去重启服务器,我们更希望可以动态管理过滤器链
【5】权限客户端
shiro-client作为jar的依赖,满足以下需求:
1、非侵入式:使用者只需要对jar依赖和做少量的配置,就可以达到统一鉴权的目标
2、可扩展性:用户除使用提供的过滤器外,可以轻松按自己的业务去定义过滤器
3、集中式管理:依赖jar之后,shiro-mgt后台可以同时管控多个平台的权限的认证、鉴权、及动态配置过滤器链
【6】网关平台
springboot-shiro-gateway:
1、依赖shiro-client项目作为权限的被控制层
2、实现dubbo传输协议到HTTP传输协议的转化,当然这里提供的为通用的转换方式。
3、可复制、复制后只需要在shiro-mgt后台中做简单的配置,就可以实现一个新网关的接入
2、架构设计
【1】系统网络通讯
1、网关服务集群性,同时实现会话的统一管理
2、鉴权服务集群化,提供统一鉴权服务
3、管理后台集群化
【2】模块依赖关系
【1.1】springboot-shiro-parent
springboot-shiro-parent:项目统一jar和plugIn的POM定义
【1.2】springboot-shiro-gateway-handler
1、dubbo业务服务转换http通讯
2、认证与鉴权服务化消费者
3、生成业务服务化消费者
【1.3】springboot-shiro-producer
认证与鉴权服务化的生成者
【1.4】springboot-shiro-mgt
认证与鉴权服务化消费者
【1.5】springboot-shiro-dubbo-app-handler
生产业务服务化生产者
3、认证鉴权服务化
上面的图解中我们可以看到,这里服务化的为UserAdapterFace
模块springboot-shiro-face中的接口定义UserAdapterFace
package com.itheima.shiro.face;
import com.itheima.shiro.vo.ResourceVo;
import com.itheima.shiro.vo.RoleVo;
import com.itheima.shiro.vo.UserVo;
import java.util.List;
/**
* @Description:用户服务接口定义
*/public interface UserAdapterFace {
/**
* @Description 按用户名查找用户
* @param loginName 登录名
* @return
*/ UserVo findUserByLoginName(String loginName);
/**
* @Description 查找用户所有角色
* @param userId 用户Id
* @return
*/ List<RoleVo> findRoleByUserId(String userId);
/**
* @Description 查询用户有那些资源
* @param userId 用户Id
* @return
*/ List<ResourceVo> findResourceByUserId(String userId);
}
springboot-shiro-producer模块中的生产者UserAdapterFaceImpl
package com.itheima.shiro.faceImpl;
import com.itheima.shiro.adapter.UserAdapter;
import com.itheima.shiro.face.UserAdapterFace;
import com.itheima.shiro.pojo.Resource;
import com.itheima.shiro.pojo.Role;
import com.itheima.shiro.pojo.User;
import com.itheima.shiro.utils.BeanConv;
import com.itheima.shiro.utils.EmptyUtil;
import com.itheima.shiro.vo.ResourceVo;
import com.itheima.shiro.vo.RoleVo;
import com.itheima.shiro.vo.UserVo;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* @Description:
*/@Service(version = "1.0.0", retries = 3,timeout = 5000)
public class UserAdapterFaceImpl implements UserAdapterFace {
@Autowired
UserAdapter userAdapter;
@Override
public UserVo findUserByLoginName(String loginName) {
User user = userAdapter.findUserByLoginName(loginName);
if (!EmptyUtil.isNullOrEmpty(user)){
return BeanConv.toBean(user,UserVo.class);
}
return null;
}
@Override
public List<RoleVo> findRoleByUserId(String userId) {
List<Role> list = userAdapter.findRoleByUserId(userId);
if (!EmptyUtil.isNullOrEmpty(list)){
return BeanConv.toBeanList(list, RoleVo.class);
}
return null;
}
@Override
public List<ResourceVo> findResourceByUserId(String userId) {
List<Resource> list = userAdapter.findResourceByUserId(userId);
if (!EmptyUtil.isNullOrEmpty(list)){
return BeanConv.toBeanList(list, ResourceVo.class);
}
return null;
}
}
springboot-shiro-handler模块下的消费者UserBridgeServiceImpl
package com.itheima.shiro.client;
import com.itheima.shiro.constant.CacheConstant;
import com.itheima.shiro.core.SimpleCacheManager;
import com.itheima.shiro.core.base.ShiroUser;
import com.itheima.shiro.core.base.Simple Map Cache;
import com.itheima.shiro.core.base.SimpleToken;
import com.itheima.shiro.core.bridge.UserBridgeService;
import com.itheima.shiro.face.UserAdapterFace;
import com.itheima.shiro.utils.BeanConv;
import com.itheima.shiro.utils.EmptyUtil;
import com.itheima.shiro.utils.ShiroUserUtil;
import com.itheima.shiro.vo.ResourceVo;
import com.itheima.shiro.vo.RoleVo;
import com.itheima.shiro.vo.UserVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.Reference;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.util.ByteSource;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Description 权限桥接器
*/@Slf4j
@Component("userBridgeService")
public class UserBridgeServiceImpl implements UserBridgeService {
@Reference(version = "1.0.0")
private UserAdapterFace userAdapterFace;
@Autowired
private SimpleCacheManager simpleCacheManager;
@javax.annotation.Resource(name = "redissonClientForShiro")
private RedissonClient redissonClient;
public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken,String realmName) {
SimpleToken token = (SimpleToken)authcToken;
UserVo user = this.findUserByLoginName(token.getUsername());
if(EmptyUtil.isNullOrEmpty(user)){
throw new UnknownAccountException("账号不存在");
}
ShiroUser shiroUser = BeanConv.toBean(user, ShiroUser.class);
String sessionId = ShiroUserUtil.getShiroSessionId();
String cacheKeyResourcesIds = CacheConstant.RESOURCES_KEY_IDS+sessionId;
shiroUser.setResourceIds(this.findResourcesIdsList(cacheKeyResourcesIds,user.getId()));
String salt = user.getSalt();
String password = user.getPassWord();
return new SimpleAuthenticationInfo(shiroUser, password, ByteSource.Util.bytes(salt), realmName);
}
@Override
public SimpleAuthorizationInfo getAuthorizationInfo(ShiroUser shiroUser) {
UserVo user = BeanConv.toBean(shiroUser, UserVo.class);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
String sessionId = ShiroUserUtil.getShiroSessionId();
//查询用户拥有的角色
String cacheKeyRole = CacheConstant.ROLE_KEY + sessionId;
info.addRoles(this.findRoleList(cacheKeyRole, user.getId()));
//查询用户拥有的资源
String cacheKeyResources = CacheConstant.RESOURCES_KEY + sessionId;
info.addStringPermissions(this.findResourcesList(cacheKeyResources, user.getId()));
return info;
}
@Override
public List<String> findRoleList(String cacheKeyRole, String userId) {
List<RoleVo> roles = new ArrayList<RoleVo>();
if (simpleCacheManager.getCache(cacheKeyRole) != null) {
roles = (List<RoleVo>) simpleCacheManager.getCache(cacheKeyRole).get(cacheKeyRole);
} else {
roles = userAdapterFace.findRoleByUserId(userId);
if (roles.size() > 0) {
//用户角色存放到map
Map< Object , Object> mapRole = new HashMap<Object, Object>();
mapRole.put(cacheKeyRole, roles);
//新建SimpleMapCache实例并放入缓存管理器
SimpleMapCache cacheRole = new SimpleMapCache(cacheKeyRole, mapRole);
simpleCacheManager.createCache(cacheKeyRole, cacheRole);
}
}
List<String> rolesLabel = new ArrayList<String>();
for (RoleVo role : roles) {
rolesLabel.add(role.getLabel());
}
return rolesLabel;
}
@Override
public List<String> findResourcesList(String cacheKeyResources,String userId) {
List<ResourceVo> resourcesList = new ArrayList<ResourceVo>();
if (simpleCacheManager.getCache(cacheKeyResources) != null) {
resourcesList = (List<ResourceVo>) simpleCacheManager.getCache(cacheKeyResources).get(cacheKeyResources);
} else {
resourcesList = userAdapterFace.findResourceByUserId(userId);
if (resourcesList.size() > 0) {
//用户资源存放到map
Map<Object, Object> mapResource = new HashMap<Object, Object>();
mapResource.put(cacheKeyResources, resourcesList);
//新建SimpleMapCache实例并放入缓存管理器
SimpleMapCache cacheResource = new SimpleMapCache(cacheKeyResources, mapResource);
simpleCacheManager.createCache(cacheKeyResources, cacheResource);
}
}
List<String> resourcesLabel = new ArrayList<String>();
for (ResourceVo resources : resourcesList) {
resourcesLabel.add(resources.getLabel());
}
return resourcesLabel;
}
@Override
public UserVo findUserByLoginName(String loginName) {
String key = CacheConstant.FIND_USER_BY_LOGINNAME+loginName;
RBucket<UserVo> rBucket = redissonClient.getBucket(key);
UserVo user = rBucket.get();
if (!EmptyUtil.isNullOrEmpty(user)) {
return user;
}else {
user = userAdapterFace.findUserByLoginName(loginName);
if (!EmptyUtil.isNullOrEmpty(user)) {
rBucket.set(user, 300, TimeUnit.SECONDS);
return user;
}
}
rBucket.set(new UserVo(), 3, TimeUnit.SECONDS);
return null;
}
@Override
public List<String> findResourcesIdsList(String cacheKeyResources,String userId) {
List<ResourceVo> resourcesList = new ArrayList<ResourceVo>();
if (simpleCacheManager.getCache(cacheKeyResources) != null) {
resourcesList = (List<ResourceVo>) simpleCacheManager.getCache(cacheKeyResources).get(cacheKeyResources);
} else {
resourcesList = userAdapterFace.findResourceByUserId(userId);
if (resourcesList.size() > 0) {
//用户资源存放到map
Map<Object, Object> mapResource = new HashMap<Object, Object>();
mapResource.put(cacheKeyResources, resourcesList);
//新建SimpleMapCache实例并放入缓存管理器
SimpleMapCache cacheResource = new SimpleMapCache(cacheKeyResources, mapResource);
simpleCacheManager.createCache(cacheKeyResources, cacheResource);
}
}
List<String> resourcesLabel = new ArrayList<String>();
for (ResourceVo resources : resourcesList) {
resourcesLabel.add(resources.getId());
}
return resourcesLabel;
}
@Override
public void loadUserAuthorityToCache(ShiroUser user) {
String sessionId = user.getSessionId();
List<RoleVo> roles = userAdapterFace.findRoleByUserId(user.getId());
//创建角色cachaeKey
String cacheKeyRole = CacheConstant.ROLE_KEY + sessionId;
//用户角色存放到map
Map<Object, Object> mapRole = new HashMap<Object, Object>();
mapRole.put(cacheKeyRole, roles);
//新建SimpleMapCache实例并放入缓存管理器
SimpleMapCache cacheRole = new SimpleMapCache(cacheKeyRole, mapRole);
simpleCacheManager.createCache(cacheKeyRole, cacheRole);
List<ResourceVo> resourcesList = userAdapterFace.findResourceByUserId(user.getId());
if (resourcesList.size() > 0) {
//创建资源cachaeKey
String cacheKeyResources = CacheConstant.RESOURCES_KEY + sessionId;
//用户资源存放到map
Map<Object, Object> mapResource = new HashMap<Object, Object>();
mapResource.put(cacheKeyResources, resourcesList);
//新建SimpleMapCache实例并放入缓存管理器
SimpleMapCache cacheResource = new SimpleMapCache(cacheKeyResources, mapResource);
simpleCacheManager.createCache(cacheKeyResources, cacheResource);
}
}
}
通过上面的改造,我们可以发现:用户在认证与鉴权时走的都是dubbo的服务,而在实际业务项目中不会再去操作鉴权相关的内容