pigx微服务开发平台认证与受权系统研究

1、简述

权限系统的设计通常分为:权限设计 = 功能权限 + 数据权限html

本文主要对pigx平台在认证与受权方面的功能权限进行解析,而对于数据权限,通常是根据业务场景具体作特殊的设计,且必须在项目前期就作好规划,不像功能权限那样能够在后期完成,pigx对数据权限作了必定支持,具体请参考pigx数据权限设计前端

那么对于pigx的功能权限:git

咱们把请求按来源分为:外部请求和内部请求,其中外部请求分为登陆请求非登陆请求github

按目标资源分为:无注解@Inner注解(仅内部请求)、@PreAuthorize注解(带权限控制)redis

下面是来源与资源的对应关系:spring

image-20200507093410641.png

(请求类型与资源控制之间的关系)sql

对于请求而言,主要是外部内部的区别,而外部请求必须通过网关(Gateway),网关对于请求的处理主要有登陆非登陆的区别,所以上面表格中将外部请求细分为登陆与非登陆,这样对比资源的控制就更清晰、更细化后端

对于目标资源而言,主要有无注解@Inner注解@PreAuthorize注解三种:api

无注解:通常用于对外公开资源,如商品浏览、官网等互联网接口。安全

对应的服务须要添加白名单配置:security.oauth2.client.ignore-urls后接口才可访问(或不引入依赖pigx-common-security、或采用@Inner(false)注解)

@Inner注解:通常用于被内部应用请求的接口,如日志、定时任务、文件存储等支持型服务,被注解后该接口将没法被外部请求访问到(须要网关提供保护,后面会讲到网关是如何保护内部应用请求的)

@PreAuthorize注解:用于外部请求非登陆请求,该类请求须带token,所以是登陆后对用户访问资源接口的权限控制,微服务依赖pigx-common-security之后就有认证(spring security oauth2)控制了,认证控制负责的是对token的鉴定,而对接口自己是否有权限访问是由pigx中的用户权限系统所控制,该注解就是在token鉴定成功之后,pigx用户权限系统再基于token内容进行的权限控制

下面经过如下部分对pigx平台认证与受权系统进行分析:

  1. 与网关相关的权限功能设计
  2. 与外部请求相关的权限功能设计
  3. 与内部请求相关的权限功能设计

2、与网关相关的权限功能设计

网关服务(Gateway)是全部服务的入口,起到了重要的做用,目前在pigx系统架构中主要有如下特殊做用的过滤器(Filter),他们都对权限系统的工做起到了必定的做用:

过滤器 做用
HttpBasicGatewayFilter 自定义basic认证,针对特殊场景使用
JiyupRequestGlobalFilter 清洗请求头中from 参数,用于防止外部模拟内部请求
PasswordDecoderFilter 对登陆请求的密码参数进行解密处理
PreviewGatewayFilter 提供测试环境的支持
ValidateCodeGatewayFilter 对登陆请求进行验证码检验

PigxRequestGlobalFilter分析:

内部服务请求一般不须要再经过auth服务进行一次鉴权,如A请求B时,若是B须要对A请求鉴权的话,A就须要拿到token,且B接送token后还须要请求auth服务鉴定token有效性,若是B在处理过程当中还须要请求C,则C一样须要如此过程,不但复杂且给auth服务增添很多压力,通常的作法是网关请求A时,A进行一次鉴权,A到B,B到C的内部请求过程不须要再鉴权

为了实现B接口不鉴权,通常会将B所在服务中配置security.oauth2.client.ignore-urls,接口地址将不会鉴权

但单纯添加白名单是不行的,由于网关外部请求就能够直接获取到该接口资源

为了实现接口内部请求容许请求,外部请求不容许请求的目的,pigx引入了注解@Inner

该注解的接口请被切面PigxSecurityInnerAspect控制,控制逻辑很简单,只有请求头部带"from"标志时才容许访问:

@SneakyThrows
@Around("@annotation(inner)")
public Object around(ProceedingJoinPoint point, Inner inner) {
   String header = request.getHeader(SecurityConstants.FROM);
   if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
      log.warn("访问接口 {} 没有权限", point.getSignature().getName());
      throw new AccessDeniedException("Access is denied");
   }
   return point.proceed();
}

(PigxSecurityInnerAspect关键源码)

由此一来,内部请求时就须要添加SecurityConstants.FROM_IN参数,保证不会被PigxSecurityInnerAspect切面所拒绝,好比下面这段用户受权(Auth)的代码,请求用户服务(upms)时带上了此参数来获取用户信息:

image-20200507155144028.png

而用户信息接口上对应加入了@Inner注解:

image-20200507154927157.png

但外部请求能够经过网关访问白名单接口,一样也能够模拟头部带“from”的内部请求

所以PigxRequestGlobalFilter的做用就是防止外部模拟头部带“from”的请求来访问内部资源,从源码中能够看到将请求头部对“from”统一进行了去除:

ServerHttpRequest request = exchange.getRequest().mutate()
   .headers(httpHeaders -> httpHeaders.remove(SecurityConstants.FROM))
   .build();

PasswordDecoderFilter分析:

考虑到登陆请求密码参数在传输过程当中的安全性,前端对密码文本进行了加密处理:

image-20200506170852874.png

PasswordDecoderFilter用于对登陆密码中的密码参数进行解密处理:

@Override
public GatewayFilter apply(Object config) {
   return (exchange, chain) -> {
      ServerHttpRequest request = exchange.getRequest();
      // 不是登陆请求,直接向下执行
      if (!StrUtil.containsAnyIgnoreCase(request.getURI().getPath(), SecurityConstants.OAUTH_TOKEN_URL)) {
         return chain.filter(exchange);
      }

      // 刷新token,直接向下执行
      String grantType = request.getQueryParams().getFirst("grant_type");
      if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {
         return chain.filter(exchange);
      }

      Class inClass = String.class;
      Class outClass = String.class;
      ServerRequest serverRequest = ServerRequest.create(exchange,
            messageReaders);

      // 解密生成新的报文
      Mono<?> modifiedBody = serverRequest.bodyToMono(inClass)
            .flatMap(decryptAES());

      BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
      HttpHeaders headers = new HttpHeaders();
      headers.putAll(exchange.getRequest().getHeaders());
      headers.remove(HttpHeaders.CONTENT_LENGTH);

      headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
      CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(
            exchange, headers);
      return bodyInserter.insert(outputMessage, new BodyInserterContext())
            .then(Mono.defer(() -> {
               ServerHttpRequest decorator = decorate(exchange, headers,
                     outputMessage);
               return chain
                     .filter(exchange.mutate().request(decorator).build());
            }));
   };
}

(PasswordDecoderFilter关键源码)

ValidateCodeGatewayFilter分析:

网关提供了验证码的实现,在RouterFunctionConfiguration中对/code接口提供了imageCodeHandler对象,用于生成验证码:

@Bean
public RouterFunction routerFunction() {
   return RouterFunctions.route(
         RequestPredicates.path("/code")
               .and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), imageCodeHandler)
         .andRoute(RequestPredicates.GET("/swagger-resources")
               .and(RequestPredicates.accept(MediaType.ALL)), swaggerResourceHandler)
         .andRoute(RequestPredicates.GET("/swagger-resources/configuration/ui")
               .and(RequestPredicates.accept(MediaType.ALL)), swaggerUiHandler)
         .andRoute(RequestPredicates.GET("/swagger-resources/configuration/security")
               .and(RequestPredicates.accept(MediaType.ALL)), swaggerSecurityHandler);

}

(RouterFunctionConfiguration关键源码)

ValidateCodeGatewayFilter的做用是在登陆请求中获取用户输入的验证证参数,验证用户输入是否正确

3、与外部请求相关的权限功能设计

3.1 服务层面的外部请求权限控制

对于外部请求内部资源,除非是不须要权限控制的资源接口,不然咱们开发的新微服务模块都应该依赖平台的pigx-common-security组件:

<dependency>
   <groupId>com.pig4cloud</groupId>
   <artifactId>pigx-common-security</artifactId
</dependency>

该组件结合pigx-upms-api与spring security oauth2框架进行了封装,从而实现系统用户与权限的通关:

<dependencies>
   <!--工具类核心包-->
   <dependency>
      <groupId>com.pig4cloud</groupId>
      <artifactId>pigx-common-core</artifactId>
   </dependency>
   <!--安全模块 -->
   <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-oauth2</artifactId>
   </dependency>
   <!--feign-->
   <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-core</artifactId>
   </dependency>
   <!--UPMS API-->
   <dependency>
      <groupId>com.pig4cloud</groupId>
      <artifactId>pigx-upms-api</artifactId>
   </dependency>
</dependencies>

(pigx-common-security的pom.xml依赖)

引入pigx-common-security组件之后,nacos配置中心须要配置client-id、client-secret、scope:

## spring security 配置
security:
  oauth2:
    client:
      client-id: ENC(gPFcUOmJm8WqM3k3eSqS0Q==)
      client-secret: ENC(gPFcUOmJm8WqM3k3eSqS0Q==)
      scope: server

咱们所写的每一个微服务都是一个client,对应在后台“终端管理”中进行设置:

image-20200507165135777.png

所以每一个微服务在引入pigx-common-security依赖之后,处理外部、非登陆请求时,除非请求地址已加入白名单,不然都须要在Auth中认证请求访问者的身份:

image-20200507165735217.png

(API访问过程当中,token的认证过程)

以访问Service服务请求为例,过程以下:

  1. 客户端经过带token字符串的请求经过网关(Gateway)访问后端API
  2. 网关将请求路由到具体对应业务服务(Service)
  3. 业务服务(Service)首先会请求认证服务(Auth)来验证token
  4. token验证成功后请求进入具体接口请求逻辑中

咱们系统中有不一样的服务会拿token去访问Auth服务进行认证,来判断请求是否合法:

image-20200507171126479.png

而判断请求是否合法(即/oauth/check_token)的过程当中,不一样服务中配置的不一样client_id与client_secret,就起到了目标应用认证用户请求时自己目标应用认证的做用,这是由于Auth服务是OAuth2协议的实现,OAuth2协议把全部对自身的请求作为不一样的client来源来对待,能够在sys_oauth_client_details表中看到client分布状况:

image-20200506154502475.png

如此一来,pigx中不一样目标应用对应与Auth中client的关系以下(注:以上只列出部分应用):

image-20200507174006354.png

值得一提的是,前端登陆时的认证请求经过网关直接访问Auth服务也是属于一种client来源(client_id : pigx)

3.2 功能层面的外部请求权限控制

外部请求经过了服务层面的权限控制之后,还有更细化的功能(接口)层面的权限控制

在pigx可设置用户->角色->菜单(权限)关系:

image-20200507183426136.png

在“用户管理”功能中,可对用户“编辑”操做,进行角色设定:

image-20200507175029969.png

在"角色管理"功能中,可对角色“+权限”操做,进行权限设置:

image-20200507175120252.png
每一个权限菜单(sys_menu)对应有一个“permission”字段,用于功能层面的权限控制

由于Spring Security Oauth2是基于Spring Security的,所以天然采用了Spring Security中的@PreAuthorize注解完成对接口访问权限的控制

@PreAuthorize经过指定PermissionService类的hasPermission()方法进行具体访问控制:

PermissionService关键代码片断:

public boolean hasPermission(String... permissions) {
   if (ArrayUtil.isEmpty(permissions)) {
      return false;
   }
   Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
   if (authentication == null) {
      return false;
   }
   Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
   return authorities.stream()
         .map(GrantedAuthority::getAuthority)
         .filter(StringUtils::hasText)
         .anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
}

所以在以上基础之上,只需接口方法添加@PreAuthorize注解,便可实现功能层面的权限控制,如:

image-20200507180902736.png

hasPermission()方法在第5行从SecurityContextHolder.getContext().getAuthentication()中取得了用户信息,该信息是由OAuth2AuthenticationProcessingFilter过滤器放入其中的,追溯操做权限获取过程以下:

image-20200506094325064.png

OAuth2AuthenticationProcessingFilter过滤器就是实现3.1节中讲到服务层面鉴权时的主要逻辑:

经过doFilter()方法对请求过滤处理,处理逻辑会访问OAuth2AuthenticationManager.authenticate()方法,authenticate()方法实际是访问RemoteTokenServices的loadAuthentication()方法,RemoteTokenServices是ResourceServerTokenServices接口的远程访问方式实现,实际请求到了Auth服务的/oauath/check_token接口,该接口专用于对token验证的支持

image.png

(RemoteTokenServices的loadAuthentication()方法)

/oauth/check_token 接口的checkToken()方法实现中:

  1. ResourceServerTokenServices接口采用DefaultTokenServices类实现,该类中包含TokenStore接口对象,该对象使用RedisTokenStore实现
  2. 经过跟踪发得在验证token后,会从redis中拿出authentication相关的信息,其中就附带了authorities信息,该信息是用户token对应的接口访问控制权限(80条)

image-20200505175235674.png

因而可知,3.1节中服务层面的权限鉴定操做(/oauth/check_token)完成后,从用户会话的上下文中即可以取得功能(接口)层面的权限信息(SecurityContextHolder.getContext().getAuthentication()),即功能层面的权限控制是基于服务层面权限控制之上的,其条件为:用户已登陆、请求带token并验证经过、用户角色权限已添加

4、与内部请求相关的权限功能设计

假如咱们当前开发的业务主体为某个微服务模块,那么咱们编写的接口服务将会接受到两类请求:

  1. 从外部通过网关路由而来的外部请求
  2. 从内部其它服务经过RestTemplate、Netty等方式而来的内部请求

其中外部请求是最常规的权限控制,以上第三节已经进行说明

而内部请求,在pigx中有结合网关对此作专门的设计,其中网关设计部分主要是PigxRequestGlobalFilter,已经进行说明,除此以外就是@Inner注解与PigxSecurityInnerAspect切面,下面进行说明:

@Inner注解定义以下:

/**
 * @author Pigx
 * @date 2019/4/13
 * <p>
 * 服务调用鉴权注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {

    /**
     * 是否AOP统一处理
     *
     * @return false, true
     */
    boolean value() default true;

    /**
     * 须要特殊判空的字段(预留)
     *
     * @return {}
     */
    String[] field() default {};
}

对于只容许内部系统访问的接口,应添加@Inner注解:

image-20200507192034855.png

此时若是咱们经过外部调用此接口,将会被拒绝:

image-20200507191931626.png

PigxSecurityInnerAspect负责对切面处理外部访问带有@Inner注解的接口时,作权限拒绝处理:

@Slf4j
@Aspect
@AllArgsConstructor
public class PigxSecurityInnerAspect {
    private final HttpServletRequest request;

    @SneakyThrows
    @Around("@annotation(inner)")
    public Object around(ProceedingJoinPoint point, Inner inner) {
        String header = request.getHeader(SecurityConstants.FROM);
        if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
            log.warn("访问接口 {} 没有权限", point.getSignature().getName());
            throw new AccessDeniedException("Access is denied");
        }
        return point.proceed();
    }

}

利用@Inner注解添加接口权限白名单(ignore urls)

@Inner注解除了能够用于防止外部请求访问,还能够为接口起到添加白名单的做用,只需在注解中加入false参数:

@Inner(false)

该参数默认为true时,作为内部调用接口,反之为false时,作可为外部调用无须鉴权的接口(对外公开资源,如商品浏览、官网等互联网接口)

5、登陆认证功能设计

以上都是受权之后的权限控制逻辑,Spring Security提出了两个概念:认证受权,其中受权能够理解为认证成功之后为client颁发证实(token)以及鉴定证实,而下面介绍的认证就是client为了获取证实(token)向Auth服务请求认证的过程:

认证过程场景

image-20200506152705555.png

pigx的网关(Gateway)并无对认证与受权过程作太多业务处理,只是简单的将登陆请求进行了特殊的对待,配合内部请求权限去除SecurityConstants.FROM参数,其它请求处理都是一视同仁

Auth服务在基于Spring Security OAuth2基础上对/oauth/authorize作了处理,大部分状况下只须要提供配置及一部分简单实现就能实现受权与认证,Spring Security OAuth2的使用基本上是按官方标准方式来实现的,这里就再也不赘述了,有兴趣的可自行研究

6、总结

总结一下pigx平台中的权限体系,按应用功能分为有如下三部分:

  1. Spring Security:认证与受权
  2. Spring Security OAuth2:基于Spring Security之上实现OAuth2协议
  3. pigx-common-security:基于Spring Security OAuth2之上,封装成pigx平台专用安全组件,并提供@Inner @PreAuthorizet等更细精的权限控制

或者按鉴权的不一样分为三类:

  1. 其它应用请求Auth服务功能时,Auth服务要求的应用提供的client级别鉴权(对应后台“终端管理”中添加)
  2. 应用从Auth认证成功拿到受权之后,再来请求后台服务接口时的会话级鉴权(token),/oauth/check_token接口
  3. 应用顺利经过上一步会话级鉴权之后,进入pigx提供的应用级鉴权(对应后台“用户、角色、权限”的配置、代码中@PreAuthorize@Inner的编写)

7、 参考

OAuth2协议官方介绍

阮大神对OAuth2讲解

OAuth2官方建表sql

pigx官方文档

pigx官方Inner解释

相关文章
相关标签/搜索