Spring Cloud Alibaba微服务实战二十五 - 网关Restful接口拦截

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

前言

之前在 集成RBAC授权 的文章中提到了SpringCloud可以 「基于路径匹配器授权」 在网关层进行用户权限校验,这种方式的实现原理是Springcloud Gateway接受到请求后根据 ReactiveAuthorizationManager#check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) 方法基于 AntPathMatcher校验当前访问的URL是否在用户拥有的权限URL中,如果能匹配上则说明拥有访问权限并放行到后端服务,否则提示用户无访问权限。

不过之前的实现方式有个问题,就是不支持restful风格的url路径。

例如一个微服务有如下API
GET /v1/pb/user
POST /v1/pb/user
PUT /v1/pb/user

这样在网关通过 request.getURI().getPath()方法获取到用户请求路径的时候都是同一个地址,给一个用户授予 /v1/pb/user权限后他就拥有了 GETPUTPOST三种不同权限,很显然这样不能满足精细权限控制。本章内容我们就来解决这个Restful接口拦截的问题,使其能支持精细化的权限控制。

场景演示

我们看下实际的案例,演示下这种场景。在 account-service模块下增加一个博客用户管理功能,有如下的接口方法:

接口URLHTTP方法接口说明
/blog/userPOST保存用户
/blog/user/{id}GET查询用户
/blog/user/{id}DELETE删除用户
/blog/user/{id}PUT更新用户信息

然后我们在 sys_permission表中添加2个用户权限,再将其授予给用户角色

在网关层的校验方法中可以看到已经增加了2个权限

由于DELETE 和 PUT对应的权限路径都是 /blog/user/{id},这样就是当给用户授予了查询权限后此用户也拥有了删除和更新的权限。

解决方案

看到这里大部分同学应该想到了,要想实现Restful风格的精细化权限管理单单通过URL路径是不行的,需要搭配Method一起使用。

最关键的点就是 「需要给权限表加上方法字段,然后在网关校验的时候即判断请求路径又匹配请求方法。」 实现步骤如下:

  • 修改权限表,新增方法字段

  • loadUserByUsername()方法构建用户权限的时候将权限对应的Method也拼接在权限上,关键代码如下:
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
     //获取本地用户
     SysUser sysUser = sysUserMapper.selectByUserName(userName);
     if(sysUser != null){
      //获取当前用户的所有角色
      List<SysRole> roleList = sysRoleService.listRolesByUserId(sysUser.getId());
      sysUser.setRoles(roleList.stream().map(SysRole::getRoleCode).collect(Collectors.toList()));
      List<Integer> roleIds = roleList.stream().map(SysRole::getId).collect(Collectors.toList());
      //获取所有角色的权限
      List<SysPermission> permissionList = sysPermissionService.listPermissionsByRoles(roleIds);
      //拼接method
      List<String> permissionUrlList = permissionList.stream()
                        .map(item -> "["+item.getMethod()+"]"+item.getUrl())
                        .collect(Collectors.toList());
      sysUser.setPermissions(permissionUrlList);
      //构建oauth2的用户
      return buildUserDetails(sysUser);
     }else{
      throw  new UsernameNotFoundException("用户["+userName+"]不存在");
     }
    }

通过上面的代码构建的用户权限如下:

[GET]/account-service/blog/user/{id}

[POST]/account-service/blog/user

可以通过代码调试查看:

  • 权限校验方法AccessManager#check(),校验[MEHOTD]RequestPath 格式
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) {
     ServerWebExchange exchange = authorizationContext.getExchange();
     ServerHttpRequest request = exchange.getRequest();
     //请求资源
     String requestPath = request.getURI().getPath();
    
     //拼接method
     String methodPath = "["+request.getMethod()+"]" + requestPath;
    
     // 1. 对应跨域的预检请求直接放行
     if(request.getMethod() == HttpMethod.OPTIONS){
      return Mono.just(new AuthorizationDecision(true));
     }
    
     // 是否直接放行
     if (permitAll(requestPath)) {
      return Mono.just(new AuthorizationDecision(true));
     }
    
     return authenticationMono.map(auth -> new AuthorizationDecision(checkAuthorities(auth, methodPath)))
       .defaultIfEmpty(new AuthorizationDecision(false));
    
    }

校验方法 checkAuthorities()

    private boolean checkAuthorities(Authentication auth, String requestPath) {
     if(auth instanceof OAuth2Authentication){
      OAuth2Authentication authentication = (OAuth2Authentication) auth;
      String clientId = authentication.getOAuth2Request().getClientId();
      log.info("clientId is {}",clientId);
      //用户的权限集合
      Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
    
      return authorities.stream()
        .map(GrantedAuthority::getAuthority)
        //ROLE_开头的为角色,需要过滤掉
        .filter(item -> !item.startsWith(CloudConstant.ROLE_PREFIX))
        .anyMatch(permission -> ANT_PATH_MATCHER.match(permission, requestPath));
     }
    
     return true;
    }
  • 这样当请求Delete方法时就会提示没有权限


这里还有另外一种方案,实现的原理跟上面差不多,只简单提一下。

首先还是得在权限表中新增METHOD字段,这是必须的。

然后项目中使用的权限类是 SimpleGrantedAuthority,这个只能存储一个权限字段,我们可以自定义一个权限实体类,让其可以存储url 和 method。

    @Data
    public class MethodGrantedAuthority implements GrantedAuthority {
    
        private String method;
        private String url;
    
        public MethodGrantedAuthority(String method, String url){
            this.method = method;
            this.url = url;
        }
    
        @Override
        public String getAuthority() {
            return "["+method+"]" + url;
        }
    }

在 UserDetailServiceImpl中构建用户权限时使用自定义的 MethodGrantedAuthority

网关层校验的方法还是需要跟上面一样,既校验Method 又 校验 URL。

以上就解决了在网关层校验Restful风格的用户权限校验,希望对你有所帮助!

  • 10
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值