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解釋

相關文章
相關標籤/搜索