前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在网关zuul中对所有下游服务权限做控制,覆盖到所有接口,权限控制到角色、菜单、按钮、方法

在网关zuul中对所有下游服务权限做控制,覆盖到所有接口,权限控制到角色、菜单、按钮、方法

作者头像
天涯泪小武
发布2019-08-26 17:30:55
2K0
发布2019-08-26 17:30:55
举报
文章被收录于专栏:SpringCloud专栏SpringCloud专栏

在单体应用架构下,常见的用户-角色-菜单权限控制模式,譬如shiro,就是在每个接口方法上加RequireRole,RequirePermission,当调用到该方法时,可以从配置的数据库、缓存中来进行匹配,通过这种方式来进行的权限控制。

而在微服务架构下,我们会使用网关来作为所有服务的入口,由网关来完成鉴权、分发、限流等功能。

也就是从前由各个单体服务完成的各自的权限验证,现在全部交给zuul来统一管理,这样能够将权限控制到单点里,便于统一管理,也能避免大量的非法请求、权限不足的请求落到后面的微服务里,从而减少对网关后面的服务造成冲击。

针对这种情况,很多方案是采用上图的方式。具体的我也思考过,首先问题比较明显:

1:zuul作为集群的入口,要承担大量的请求,还要保证性能,如果每个请求都去和另一个服务做交互,必然会有性能损失,至少在网络开销上会不小。

2:AuthServer是否能够完成精确的权限控制?大部分情况下,都是用户-角色-菜单这种模型,关键在于菜单这块,现实情况是很多接口并不是菜单,也不是按钮,在界面上没有任何体现,就是个接口而已。我想对接口的权限进行控制,譬如只允许某个角色的用户才能访问。倘若将全部接口都写入菜单管理里,明显是不合适的,也很容易遗漏,工作量也很大。

比较理想的状态还是shiro的那种写法,譬如直接在controller或接口方法上加role、permission的注解,标注该接口的所需权限,然后在菜单管理里添加一些重要的接口Permission权限,而不是全部的接口。然后呢,每个微服务都完成好自己的权限标注后,当有用户请求时,就在网关层进行鉴别,由网关来控制是否放行。这样,在每个微服务里,就不需要做权限控制了。

这种该怎么实现呢,单个微服务的权限信息如何告知网关,并且如何保持权限信息的同步?

我的实现方式如图,首先各个微服务在启动后,就上传自己的所有权限信息到redis,zuul监听redis的变化,及时将各微服务的接口权限变更信息更新到内存。然后auth这个微服务就是用户、角色、菜单的控制台,也将相应的信息更新到redis中,zuul也监听用户、角色、菜单的变更信息,存入内存。

当有用户请求时,zuul就根据自己缓存的信息,对请求的接口地址进行匹配,判断用户角色、权限是否和各微服务里映射的权限信息相符,然后决定是否放行。

这一套结构我已封装为一个框架,可以直接在pom里添加依赖并使用,源码在地址

代码语言:javascript
复制
<repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>
代码语言:javascript
复制
<dependency>
            <groupId>com.github.tianyaleixiaowu</groupId>
            <artifactId>zuulauth</artifactId>
            <version>13a6001c25</version>
        </dependency>

微服务端

使用方法很简单,添加好依赖,配置好redis的连接地址,然后在代码里启用权限控制,加上@EnableClientAuth注解即可。当应用启动后就会自动上传所有的权限信息到redis里。

authServer端

该端是负责用户、角色、菜单的增删改查的,并且要负责把这些信息放到redis里。

第一步:添加依赖,配置redis地址

第二步:通过AuthCache类来完成信息的存储和删除

譬如当添加了role-menu的映射后,就用authCache来save一下。当删除了role时,就remove掉即可。

zuul端

第一步:添加好依赖在pom.xml,配置redis连接地址

第二步:创建好一个zuulFilter,在里面做权限控制。

在zuul里,用户发起请求后,譬如使用的是jwt或其他,我们需要先取到userId或者roleId。

然后调用AuthInfoHolder.findxxx方法,来获取用户的角色roleSet,codeSet(某个角色的权限集合),

之后调用AuthCheck的check方法,来确定用户权限是否匹配。

check方法需要几个参数,分别是微服务的名字,该请求的方法(get、post、put、delete),请求的地址(/menu/add),该用户的角色(或角色集合,Set<String>),该用户的权限集合(Set<String>).

由于获取用户角色和角色权限,都是基于内存获取,倘若用户在authServer端修改了某个role的权限,那么在二次查询前,事实上redis里是没有这个role的权限的,只有当调用了authServer的查询该role的权限接口后,从redis获取失败,那么就会走数据库查询获取,并缓存到redis,然后zuul的内存才能知道。所以在89行,判断读取不到时,就调用authServer的接口来获取。那么之后,就已经缓存了。

实例代码:

代码语言:javascript
复制
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<String> 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<Route> 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;
    }
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年08月15日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 微服务端
  • authServer端
  • zuul端
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档