1. 程式人生 > >JHipster技術棧定制 - 基於UAA的微服務之間安全調用

JHipster技術棧定制 - 基於UAA的微服務之間安全調用

vid ide 情況 imp nth get img rose 工作

本文通過代碼實例演示如何通過UAA實現微服務之間的安全調用。
uaa: 身份認證服務,同時也作為被調用的資源服務。服務端口9999。
microservice1: 調用uaa的消費者服務,服務端口8081。

1 準備工作

1.1 工程目錄

--| appstack
  |-- uaa
  |-- microservice1

1.2 啟動相關組件

為了簡單起見,這裏都使用容器啟動相關組件,需要2個鏡像,最好提前下載好。

  • jhipster/jhipster-registry:v4.0.0
  • mysql:5
a, 啟動一個Jhipster-Registry
$ docker container run --name registry-app -e JHIPSTER.SECURITY.AUTHENTICATION.JWT.SECRET=dkk20dldkf0209342334 -e SPRING.PROFILES.ACTIVE=dev -d -p 8761:8761 jhipster/jhipster-registry:v4.0.0
b, 啟動2個MySql容器。
$ docker container run --name uaa-mysql --privileged -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -p 32900:3306 mysql:5
$ docker container run --name microservice1-mysql --privileged -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -p 32800:3306 mysql:5

1.3 生成微服務工程

3個微服務都是通過Jhipster生成。 工程代碼生成完之後,根據上一節啟動的組件的實際情況,修改微服務配置文件中Eureka和database相關的配置。

這裏使用的Jhipster版本為5.1.0。具體生成和配置詳情,可以參考這裏

2 核心代碼

2.1 uaa源碼

在uaa裏面新增一個controller類,提供一個GET方法,作為被調用的API。

$ vi com.mycompany.appstack.web.rest.Provider
# 這裏提供一個簡單的GET API

package com.mycompany.appstack.web.rest;

import org.springframework.web.bind.annotation.*;

/**
 * REST controller for managing the current user‘s account.
 */
@RestController
@RequestMapping("/api")
public class ProviderResource {

    public ProviderResource () {
    }
    
    /**
     * GET  /provider:
     */
    @GetMapping("/provider")
    public String provider() {
        return "Hello, I‘m uaa provider.";
    }
    
}

2.2 microservice源碼

a, 用於服務間調用的FeignClient註解類。

com.mycompany.appstack.config.client.AuthorizedFeignClient
生成的代碼中,這個類是默認存在的,不需要修改,除非你要修改這個默認的配置類名。

Class<?>[] configuration() default OAuth2InterceptedFeignConfiguration.class;
b, 將自定義OAuth2攔截器類註冊到當前服務中的配置類。

com.mycompany.appstack.client.OAuth2InterceptedFeignConfiguration
生成的代碼中,這個類是默認存在的,需要修改如下:

package com.mycompany.appstack.client;

import java.io.IOException;
import org.springframework.context.annotation.Bean;
import feign.RequestInterceptor;

public class OAuth2InterceptedFeignConfiguration {  
    @Bean(name = "serviceFeignClientInterceptor")
    public RequestInterceptor getFeignClientInterceptor() throws IOException {
        return new ServiceFeignClientInterceptor();
    }
}
c, 自定義OAuth2攔截器類。

com.mycompany.appstack.client.ServiceFeignClientInterceptor
這是一個新增的類,內容如下:

package com.mycompany.appstack.client;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.stereotype.Component;

import com.mycompany.appstack.security.oauth2.ServiceTokenEndpointClient;

import feign.RequestInterceptor;
import feign.RequestTemplate;

@Component
public class ServiceFeignClientInterceptor implements RequestInterceptor {

    private final Logger log = LoggerFactory.getLogger(ServiceFeignClientInterceptor.class);

    private static final String AUTHORIZATION_HEADER = "Authorization";

    private static final String BEARER_TOKEN_TYPE = "Bearer";

    @Autowired
    private ServiceTokenEndpointClient serviceTokenEndpointClient ;

    @Override
    public void apply(RequestTemplate template) {

        OAuth2AccessToken oauthToken = serviceTokenEndpointClient .sendClentCredentialsGrant();
        if (oauthToken != null) {
            template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER_TOKEN_TYPE, oauthToken.getValue()));
        }

    }
}
d, 與UAA通訊的客戶端接口,增加一個抽象方法。

com.mycompany.appstack.security.oauth2.OAuth2TokenEndpointClient
生成的代碼中,這個類是默認存在的,需要增加如下方法:

    /**
     * Send a client grant to the token endpoint.
     * 
     * @return
     */
    OAuth2AccessToken sendClentCredentialsGrant();
e, d的適配器類,增加對應的實現方法。

com.company.appstack.security.oauth2.OAuth2TokenEndpointClientAdapter
生成的代碼中,這個類是默認存在的,需要增加如下方法:

   /**
     * Sends a credentials grant to the token endpoint.
     *
     * @return the access token.
     */
    @Override
    public OAuth2AccessToken sendClentCredentialsGrant() {
        HttpHeaders reqHeaders = new HttpHeaders();
        reqHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> formParams = new LinkedMultiValueMap<>();
        formParams.set("grant_type", "client_credentials");
        addAuthentication(reqHeaders, formParams);
        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(formParams, reqHeaders);
        log.debug("contacting OAuth2 token endpoint to authenticate internal service.");
        ResponseEntity<OAuth2AccessToken> responseEntity = restTemplate.postForEntity(getTokenEndpoint(), entity,
                OAuth2AccessToken.class);
        if (responseEntity.getStatusCode() != HttpStatus.OK) {
            log.debug("failed to authenticate user with OAuth2 token endpoint, status: {}",
                    responseEntity.getStatusCodeValue());
            throw new HttpClientErrorException(responseEntity.getStatusCode());
        }
        OAuth2AccessToken accessToken = responseEntity.getBody();
        return accessToken;
    }
    
    protected String getJhipsterClientSecret() {
        String clientSecret = jHipsterProperties.getSecurity().getClientAuthorization().getClientSecret();
        if (clientSecret == null) {
            throw new InvalidClientException("no client-secret configured in application properties");
        }
        return clientSecret;
    }

    protected String getJhipsterClientId() {
        String clientId = jHipsterProperties.getSecurity().getClientAuthorization().getClientId();
        if (clientId == null) {
            throw new InvalidClientException("no client-id configured in application properties");
        }
        return clientId;
    }
f, e的實現類,增加對應的實現方法。

com.mycompany.appstack.security.oauth2.ServiceTokenEndpointClient
這是一個新增的類,內容如下:

package com.mycompany.appstack.security.oauth2;

import com.mycompany.appstack.config.oauth2.OAuth2Properties;
import io.github.jhipster.config.JHipsterProperties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;

/**
 * Client talking to UAA‘s token endpoint to do different OAuth2 grants.
 */
@Component
public class ServiceTokenEndpointClient extends OAuth2TokenEndpointClientAdapter implements OAuth2TokenEndpointClient {

    public ServiceTokenEndpointClient(@Qualifier("loadBalancedRestTemplate") RestTemplate restTemplate,
                                  JHipsterProperties jHipsterProperties, OAuth2Properties oAuth2Properties) {
        super(restTemplate, jHipsterProperties, oAuth2Properties);
    }

    @Override
    protected void addAuthentication(HttpHeaders reqHeaders, MultiValueMap<String, String> formParams) {
        reqHeaders.add("Authorization", getAuthorizationHeader());
    }

    /**
     * @return a Basic authorization header to be used to talk to UAA.
     */
    protected String getAuthorizationHeader() {
        String clientId = getJhipsterClientId();
        String clientSecret = getJhipsterClientSecret();
        String authorization = clientId + ":" + clientSecret;
        return "Basic " + Base64Utils.encodeToString(authorization.getBytes(StandardCharsets.UTF_8));
    }

}
g, 調用uaa服務的Feign客戶端類

com.mycompany.appstack.client.feign.BaseUaaAuthFeignClient
這是一個新增的類,內容如下:


package com.mycompany.appstack.client.feign;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.mycompany.appstack.client.AuthorizedFeignClient;

@AuthorizedFeignClient(name = "uaa", fallback = CallUaaAuthFeignClientHystrix.class)
public interface CallUaaAuthFeignClient {

    @RequestMapping(value = "/api/provider", method = RequestMethod.GET)
    String callProvider();
}
h, g類的斷路器類

com.mycompany.appstack.client.feign.CallUaaAuthFeignClientHystrix
這是一個新增的類,內容如下:

package com.mycompany.appstack.client.feign;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class CallUaaAuthFeignClientHystrix implements CallUaaAuthFeignClient {
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Override
    public String callProvider() {
        log.error("調用uaa provider接口失敗!");
        return "調用uaa provider接口失敗!";
    }

}

2.3 microservice1配置文件

application.yml
# 防止第一次初始化restTemplate時超時
hystrix:
    share-security-context: true
    command:
        default:
            execution:
                isolation:
                    thread:
                        timeoutInMilliseconds: 10000
application-dev.yml
jhipster:
    security:
        client-authorization:
            access-token-uri: http://uaa/oauth/token   // 從uaa獲取token的uri
            token-service-id: uaa
            client-id: internal             // 和uaa的對應配置文件項保持一致
            client-secret: internal         // 和uaa的對應配置文件項保持一致

3 測試效果

3.1 通過UAA獲取安全令牌的訪問

a, 在microservice1中新增一個controller類

這個類提供一個測試API,我們通過瀏覽器訪問這個API,間接調用CallUaaAuthFeignClient。

package com.mycompany.appstack.web.rest;

import com.mycompany.appstack.client.feign.CallUaaAuthFeignClient;
import com.mycompany.appstack.service.RoleService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * REST controller for Test AuthFeignClient.
 */
@RestController
@RequestMapping("/test")
public class CallUaaResource {

    private final Logger log = LoggerFactory.getLogger(CallUaaResource.class);

    @Autowired
    private CallUaaAuthFeignClient callUaaAuthFeignClient;
    
    public CallUaaResource(RoleService roleService) {
    
    }   

    /**
     * GET  /servicecall :
     * 
     * @return 
     */
    @GetMapping("/servicecall")
    public String getProvider() {
        log.debug("REST request to get provider from uaa.");
        return callUaaAuthFeignClient.callProvider();
    }

}
b, 編譯運行uaa,microservice1

如果一切正常,會看到Jhipster-Registry的Web UI中2個微服務已經註冊成功。

技術分享圖片

c, 瀏覽器訪問microservice1的測試API

http://localhost:8081/test/servicecall

可以看到uaa返回的結果:

技術分享圖片

說明microservice1從uaa獲取token之後,成功訪問了uaa的一個受限訪問的API。

3.2 沒有通過UAA獲取安全令牌的訪問

a, 註釋掉從uaa獲取安全令牌的代碼

註釋掉ServiceFeignClientInterceptor中的代碼:

@Override
    public void apply(RequestTemplate template) {
        //OAuth2AccessToken oauthToken = uaaTokenEndpointServiceClient.sendClentCredentialsGrant();
        //if (oauthToken != null) {
            //template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER_TOKEN_TYPE, oauthToken.getValue()));
        //}

    }
b, 重新編譯運行microservice1
c, 瀏覽器訪問microservice1的測試API

http://localhost:8081/test/servicecall

可以看到返回錯誤信息:

技術分享圖片

查看microservice1的日誌,報401錯誤:

org.springframework.web.client.HttpClientErrorException: 401 Unauthorized

說明microservice沒有從uaa獲取token,所以無法訪問uaa的受限訪問的API。

參考

完整源碼

JHipster技術棧定制 - 基於UAA的微服務之間安全調用