第十一章 分布式统一权限系统
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.ShiroHttpServletRequest;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集中式管理~ */@Log4j2public 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 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 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 sessionIdRBucket = redissonClient.getBucket(CacheConstant.GROUP_CAS+session.getId().toString()); sessionIdRBucket.delete(); } @Override public Collection getActiveSessions() { return Collections.emptySet(); } @Override public void update(Session session) { RBucket 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 findRoleByUserId(String userId); /** * @Description 查询用户有那些资源 * @param userId 用户Id * @return */ List 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 findRoleByUserId(String userId) { List list = userAdapter.findRoleByUserId(userId); if (!EmptyUtil.isNullOrEmpty(list)){ return BeanConv.toBeanList(list, RoleVo.class); } return null; } @Override public List findResourceByUserId(String userId) { List 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.SimpleMapCache;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 findRoleList(String cacheKeyRole, String userId) { List roles = new ArrayList(); if (simpleCacheManager.getCache(cacheKeyRole) != null) { roles = (List) simpleCacheManager.getCache(cacheKeyRole).get(cacheKeyRole); } else { roles = userAdapterFace.findRoleByUserId(userId); if (roles.size() > 0) { //用户角色存放到map Map mapRole = new HashMap(); mapRole.put(cacheKeyRole, roles); //新建SimpleMapCache实例并放入缓存管理器 SimpleMapCache cacheRole = new SimpleMapCache(cacheKeyRole, mapRole); simpleCacheManager.createCache(cacheKeyRole, cacheRole); } } List rolesLabel = new ArrayList(); for (RoleVo role : roles) { rolesLabel.add(role.getLabel()); } return rolesLabel; } @Override public List findResourcesList(String cacheKeyResources,String userId) { List resourcesList = new ArrayList(); if (simpleCacheManager.getCache(cacheKeyResources) != null) { resourcesList = (List) simpleCacheManager.getCache(cacheKeyResources).get(cacheKeyResources); } else { resourcesList = userAdapterFace.findResourceByUserId(userId); if (resourcesList.size() > 0) { //用户资源存放到map Map mapResource = new HashMap(); mapResource.put(cacheKeyResources, resourcesList); //新建SimpleMapCache实例并放入缓存管理器 SimpleMapCache cacheResource = new SimpleMapCache(cacheKeyResources, mapResource); simpleCacheManager.createCache(cacheKeyResources, cacheResource); } } List resourcesLabel = new ArrayList(); 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 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 findResourcesIdsList(String cacheKeyResources,String userId) { List resourcesList = new ArrayList(); if (simpleCacheManager.getCache(cacheKeyResources) != null) { resourcesList = (List) simpleCacheManager.getCache(cacheKeyResources).get(cacheKeyResources); } else { resourcesList = userAdapterFace.findResourceByUserId(userId); if (resourcesList.size() > 0) { //用户资源存放到map Map mapResource = new HashMap(); mapResource.put(cacheKeyResources, resourcesList); //新建SimpleMapCache实例并放入缓存管理器 SimpleMapCache cacheResource = new SimpleMapCache(cacheKeyResources, mapResource); simpleCacheManager.createCache(cacheKeyResources, cacheResource); } } List resourcesLabel = new ArrayList(); for (ResourceVo resources : resourcesList) { resourcesLabel.add(resources.getId()); } return resourcesLabel; } @Override public void loadUserAuthorityToCache(ShiroUser user) { String sessionId = user.getSessionId(); List roles = userAdapterFace.findRoleByUserId(user.getId()); //创建角色cachaeKey String cacheKeyRole = CacheConstant.ROLE_KEY + sessionId; //用户角色存放到map Map mapRole = new HashMap(); mapRole.put(cacheKeyRole, roles); //新建SimpleMapCache实例并放入缓存管理器 SimpleMapCache cacheRole = new SimpleMapCache(cacheKeyRole, mapRole); simpleCacheManager.createCache(cacheKeyRole, cacheRole); List resourcesList = userAdapterFace.findResourceByUserId(user.getId()); if (resourcesList.size() > 0) { //创建资源cachaeKey String cacheKeyResources = CacheConstant.RESOURCES_KEY + sessionId; //用户资源存放到map Map mapResource = new HashMap(); mapResource.put(cacheKeyResources, resourcesList); //新建SimpleMapCache实例并放入缓存管理器 SimpleMapCache cacheResource = new SimpleMapCache(cacheKeyResources, mapResource); simpleCacheManager.createCache(cacheKeyResources, cacheResource); } }}
通过上面的改造,我们可以发现:用户在认证与鉴权时走的都是dubbo的服务,而在实际业务项目中不会再去操作鉴权相关的内容