# zuulauth **Repository Path**: cfw/zuulauth ## Basic Information - **Project Name**: zuulauth - **Description**: 在网关zuul中对所有下游服务权限做控制,覆盖到所有接口,权限控制到角色、菜单、按钮、方法。基于zuul纯内存的方式,校验时性能无损耗 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 171 - **Created**: 2019-08-15 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 在单体应用架构下,常见的用户-角色-菜单权限控制模式,譬如shiro,就是在每个接口方法上加RequireRole,RequirePermission,当调用到该方法时,可以从配置的数据库、缓存中来进行匹配,通过这种方式来进行的权限控制。 而在微服务架构下,我们会使用网关来作为所有服务的入口,由网关来完成鉴权、分发、限流等功能。 ![输入图片说明](https://images.gitee.com/uploads/images/2019/0815/124941_76fd6aea_303698.png "1.png") 也就是从前由各个单体服务完成的各自的权限验证,现在全部交给zuul来统一管理,这样能够将权限控制到单点里,便于统一管理,也能避免大量的非法请求、权限不足的请求落到后面的微服务里,从而减少对网关后面的服务造成冲击。 ![输入图片说明](https://images.gitee.com/uploads/images/2019/0815/125019_43e112d2_303698.png "1.png") 针对这种情况,很多方案是采用上图的方式。具体的我也思考过,首先问题比较明显: 1:zuul作为集群的入口,要承担大量的请求,还要保证性能,如果每个请求都去和另一个服务做交互,必然会有性能损失,至少在网络开销上会不小。 2:AuthServer是否能够完成精确的权限控制?大部分情况下,都是用户-角色-菜单这种模型,关键在于菜单这块,现实情况是很多接口并不是菜单,也不是按钮,在界面上没有任何体现,就是个接口而已。我想对接口的权限进行控制,譬如只允许某个角色的用户才能访问。倘若将全部接口都写入菜单管理里,明显是不合适的,也很容易遗漏,工作量也很大。 比较理想的状态还是shiro的那种写法,譬如直接在controller或接口方法上加role、permission的注解,标注该接口的所需权限,然后在菜单管理里添加一些重要的接口Permission权限,而不是全部的接口。然后呢,每个微服务都完成好自己的权限标注后,当有用户请求时,就在网关层进行鉴别,由网关来控制是否放行。这样,在每个微服务里,就不需要做权限控制了。 这种该怎么实现呢,单个微服务的权限信息如何告知网关,并且如何保持权限信息的同步? ![输入图片说明](https://images.gitee.com/uploads/images/2019/0815/125120_bb669212_303698.png "1.png") 我的实现方式如图,首先各个微服务在启动后,就上传自己的所有权限信息到redis,zuul监听redis的变化,及时将各微服务的接口权限变更信息更新到内存。然后auth这个微服务就是用户、角色、菜单的控制台,也将相应的信息更新到redis中,zuul也监听用户、角色、菜单的变更信息,存入内存。 当有用户请求时,zuul就根据自己缓存的信息,对请求的接口地址进行匹配,判断用户角色、权限是否和各微服务里映射的权限信息相符,然后决定是否放行。 这一套结构我已封装为一个框架,可以直接在pom里添加依赖并使用。 ![输入图片说明](https://images.gitee.com/uploads/images/2019/0815/125226_25975312_303698.png "1.png") ``` jitpack.io https://jitpack.io ``` ``` com.github.tianyaleixiaowu zuulauth 13a6001c25 ``` ### 微服务端 使用方法很简单,添加好依赖,配置好redis的连接地址,然后在代码里启用权限控制,加上@EnableClientAuth注解即可。当应用启动后就会自动上传所有的权限信息到redis里。 ![输入图片说明](https://images.gitee.com/uploads/images/2019/0815/125328_15d9a85d_303698.png "1.png") ### authServer端 该端是负责用户、角色、菜单的增删改查的,并且要负责把这些信息放到redis里。 第一步:添加依赖,配置redis地址 第二步:通过AuthCache类来完成信息的存储和删除 譬如当添加了role-menu的映射后,就用authCache来save一下。当删除了role时,就remove掉即可。 ![输入图片说明](https://images.gitee.com/uploads/images/2019/0815/125413_438cf819_303698.png "1.png")   ### zuul端 第一步:添加好依赖在pom.xml,配置redis连接地址 第二步:创建好一个zuulFilter,在里面做权限控制。 在zuul里,用户发起请求后,譬如使用的是jwt或其他,我们需要先取到userId或者roleId。 然后调用AuthInfoHolder.findxxx方法,来获取用户的角色roleSet,codeSet(某个角色的权限集合), 之后调用AuthCheck的check方法,来确定用户权限是否匹配。 ![输入图片说明](https://images.gitee.com/uploads/images/2019/0815/125446_3dd06f90_303698.png "1.png") check方法需要几个参数,分别是微服务的名字,该请求的方法(get、post、put、delete),请求的地址(/menu/add),该用户的角色(或角色集合,Set),该用户的权限集合(Set). 由于获取用户角色和角色权限,都是基于内存获取,倘若用户在authServer端修改了某个role的权限,那么在二次查询前,事实上redis里是没有这个role的权限的,只有当调用了authServer的查询该role的权限接口后,从redis获取失败,那么就会走数据库查询获取,并缓存到redis,然后zuul的内存才能知道。所以在89行,判断读取不到时,就调用authServer的接口来获取。那么之后,就已经缓存了。 实例代码: ``` package com.mm.dmp.zuulnacos.filter; import com.mm.dmp.zuulnacos.exception.NoLoginException; import com.mm.dmp.zuulnacos.filter.feign.AuthFeignClient; import com.mm.dmp.zuulnacos.tool.JwtUtils; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import com.tianyalei.zuul.zuulauth.tool.FastJsonUtils; import com.tianyalei.zuul.zuulauth.zuul.AuthChecker; import com.tianyalei.zuul.zuulauth.zuul.AuthInfoHolder; import io.jsonwebtoken.Claims; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.netflix.zuul.filters.Route; import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.List; import java.util.Set; import static com.mm.dmp.zuulnacos.Constant.*; import static com.tianyalei.zuul.zuulauth.zuul.AuthChecker.*; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; import static org.springframework.http.HttpHeaders.AUTHORIZATION; /** * @author wuweifeng wrote on 2019/8/12. */ @Component public class PermissionFilter extends ZuulFilter { @Resource private JwtUtils jwtUtils; private Logger logger = LoggerFactory.getLogger(getClass()); @Resource private RouteLocator routeLocator; @Resource private AuthChecker authChecker; @Resource private AuthFeignClient authFeignClient; @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return 2; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest serverHttpRequest = ctx.getRequest(); String jwtToken = serverHttpRequest.getHeader(AUTHORIZATION); if (jwtToken == null) { //没有Authorization throw new NoLoginException(); } Claims claims = jwtUtils.getClaimByToken(jwtToken); if (claims == null) { throw new NoLoginException(); } logger.info("token的过期时间是:" + (claims.getExpiration())); if (jwtUtils.isTokenExpired(claims.getExpiration())) { throw new NoLoginException(); } //校验role String userId = claims.get(USER_ID) + ""; String roleId = claims.get(ROLE_ID) + ""; String userType = (String) claims.get(USER_TYPE); //从自己内存读取,可能为空,说明redis里没有,就需要从auth服务读取 Set userCodes = AuthInfoHolder.findByRole(roleId); if (CollectionUtils.isEmpty(userCodes)) { String codes = authFeignClient.findCodesByRole(Long.valueOf(roleId)); userCodes = FastJsonUtils.toBean(codes, Set.class); } //类似于 /zuuldmp/core/test String requestPath = serverHttpRequest.getRequestURI(); //获取请求的method String method = serverHttpRequest.getMethod().toUpperCase(); //获取所有路由信息,找到该请求对应的appName List routeList = routeLocator.getRoutes(); //Route{id='one', fullPath='/zuuldmp/auth/**', path='/**', location='auth', prefix='/zuuldmp/auth', String appName = null; String path = null; for (Route route : routeList) { if (requestPath.startsWith(route.getPrefix())) { //取到该请求对应的微服务名字 appName = route.getLocation(); path = requestPath.replace(route.getPrefix(), ""); } } if (appName == null) { throw new NoLoginException(404, "不存在的服务"); } //取到该用户的role、permission //访问 auth 服务的 GET /project/my 接口 int code = authChecker.check(appName, method, path, userType, userCodes); switch (code) { case CODE_NO_APP: throw new NoLoginException(code, "不存在的服务"); case CODE_404: throw new NoLoginException(code, "无此接口或GET POST方法不对"); case CODE_NO_ROLE: throw new NoLoginException(code, "用户无该接口所需role"); case CODE_NO_CODE: throw new NoLoginException(code, "用户无该接口所需权限"); case CODE_OK: ctx.addZuulRequestHeader(USER_ID, userId); ctx.addZuulRequestHeader(USER_TYPE, userType); ctx.addZuulRequestHeader(ROLE_ID, roleId); default: break; } return null; } }   ```