本文經過代碼實例演示如何經過UAA實現微服務之間的安全調用。
uaa: 身份認證服務,同時也做爲被調用的資源服務。服務端口9999。
microservice1: 調用uaa的消費者服務,服務端口8081。html
--| appstack |-- uaa |-- microservice1
爲了簡單起見,這裏都使用容器啓動相關組件,須要2個鏡像,最好提早下載好。java
$ 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
$ 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
3個微服務都是經過Jhipster生成。 工程代碼生成完以後,根據上一節啓動的組件的實際狀況,修改微服務配置文件中Eureka和database相關的配置。mysql
這裏使用的Jhipster版本爲5.1.0。具體生成和配置詳情,能夠參考這裏git
在uaa裏面新增一個controller類,提供一個GET方法,做爲被調用的API。github
$ 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."; } }
com.mycompany.appstack.config.client.AuthorizedFeignClient
生成的代碼中,這個類是默認存在的,不須要修改,除非你要修改這個默認的配置類名。web
Class<?>[] configuration() default OAuth2InterceptedFeignConfiguration.class;
com.mycompany.appstack.client.OAuth2InterceptedFeignConfiguration
生成的代碼中,這個類是默認存在的,須要修改以下:spring
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(); } }
com.mycompany.appstack.client.ServiceFeignClientInterceptor
這是一個新增的類,內容以下:sql
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())); } } }
com.mycompany.appstack.security.oauth2.OAuth2TokenEndpointClient
生成的代碼中,這個類是默認存在的,須要增長以下方法:docker
/** * Send a client grant to the token endpoint. * * @return */ OAuth2AccessToken sendClentCredentialsGrant();
d
的適配器類,增長對應的實現方法。com.company.appstack.security.oauth2.OAuth2TokenEndpointClientAdapter
生成的代碼中,這個類是默認存在的,須要增長以下方法:api
/** * 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; }
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)); } }
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(); }
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接口失敗!"; } }
# 防止第一次初始化restTemplate時超時 hystrix: share-security-context: true command: default: execution: isolation: thread: timeoutInMilliseconds: 10000
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的對應配置文件項保持一致
這個類提供一個測試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(); } }
若是一切正常,會看到Jhipster-Registry的Web UI中2個微服務已經註冊成功。
http://localhost:8081/test/servicecall
能夠看到uaa返回的結果:
說明microservice1從uaa獲取token以後,成功訪問了uaa的一個受限訪問的API。
註釋掉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())); //} }
http://localhost:8081/test/servicecall
能夠看到返回錯誤信息:
查看microservice1的日誌,報401錯誤:
org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
說明microservice沒有從uaa獲取token,因此沒法訪問uaa的受限訪問的API。