博客 / 詳情

返回

pigx微服務開發平台認證與授權系統研究

一、簡述

權限系統的設計一般分為:權限設計 = 功能權限 + 數據權限

本文主要對pigx平台在認證與授權方面的功能權限進行解析,而對於數據權限,一般是根據業務場景具體做特殊的設計,且必須在項目前期就做好規劃,不像功能權限那樣可以在後期完成,pigx對數據權限做了一定支持,具體請參考pigx數據權限設計

那麼對於pigx的功能權限:

我們把請求按來源分為:外部請求和內部請求,其中外部請求分為登錄請求非登錄請求

按目標資源分為:無註解@Inner註解(僅內部請求)、@PreAuthorize註解(帶權限控制)

下面是來源與資源的對應關係:

image-20200507093410641.png

(請求類型與資源控制之間的關係)

對於請求而言,主要是外部內部的區別,而外部請求必須經過網關(Gateway),網關對於請求的處理主要有登錄非登錄的區別,因此上面表格中將外部請求細分為登錄與非登錄,這樣對比資源的控制就更清晰、更細化

對於目標資源而言,主要有無註解@Inner註解@PreAuthorize註解三種:

無註解:一般用於對外公開資源,如商品瀏覽、官網等互聯網接口。

對應的服務需要添加白名單配置: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. 與內部請求相關的權限功能設計

二、與網關相關的權限功能設計

網關服務(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.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並驗證通過、用户角色權限已添加

四、與內部請求相關的權限功能設計

假如我們當前開發的業務主體為某個微服務模塊,那麼我們編寫的接口服務將會接受到兩類請求:

  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時,做可為外部調用無須鑑權的接口(對外公開資源,如商品瀏覽、官網等互聯網接口)

五、登錄認證功能設計

以上都是授權以後的權限控制邏輯,Spring Security提出了兩個概念:認證授權,其中授權可以理解為認證成功以後為client頒發證明(token)以及鑑定證明,而下面介紹的認證就是client為了獲取證明(token)向Auth服務請求認證的過程:

認證過程場景

image-20200506152705555.png

pigx的網關(Gateway)並沒有對認證與授權過程做太多業務處理,只是簡單的將登錄請求進行了特殊的對待,配合內部請求權限去除SecurityConstants.FROM參數,其它請求處理都是一視同仁

Auth服務在基於Spring Security OAuth2基礎上對/oauth/authorize做了處理,大部分情況下只需要提供配置及一部分簡單實現就能實現授權與認證,Spring Security OAuth2的使用基本上是按官方標準方式來實現的,這裏就不再贅述了,有興趣的可自行研究

六、總結

總結一下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的編寫)

七、 參考

OAuth2協議官方介紹

阮大神對OAuth2講解

OAuth2官方建表sql

pigx官方文檔

pigx官方Inner解釋

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.