在本文中,我们将使用OAuth2.0,创建一个的安全API,可供外部访问Part1和Part2完成的微服务。
我们将创建一个新的微服务,命名为product-api,作为一个外部API(OAuth术语为资源服务器-ResourceServer),并通过之前介绍过的EdgeServer暴露为微服务,作为TokenRelay,也就是转发Client端的OAuth访问令牌到资源服务器(ResourceServer)。另外添加OAuthAuthorizationServer和一个OAuthClient,也就是服务消费方。
继续完善Part2的系统全貌图,添加新的OAuth组件(标识为红色框):
备注:
1/保护外部API并不是微服务的特殊需求,因此本文适用于任何使用OAuth2.0保护外部API的架构;
3/为了降低复杂度,我们特意采用了HTTP协议。在实际的应用中,OAuth通信需要使用TLS,如HTTPS保护通信数据。
和在Part2中一样,我们使用JavaSE8、Git和Gradle访问源代码,并进行编译:
cdblog-microservices
gitcheckout-bB3M3.1
./build-all.sh
如果运行在Windows平台,则执行相应的bat文件-build-all.bat。
在Part2的基础中,新增了2个组件源码,分别为OAuthAuthorizationServer,项目名为auth-server;另一个为OAuthResourceServer,项目名为product-api-service。
编译输出10条log消息:
BUILDSUCCESSFUL
查看2个新组件是如何实现的,以及EdgeServer是如何更新并支持传递OAuth访问令牌的。我们也会修改API的URL,以便于使用。
2.1Gradle依赖
为了使用OAuth2.0,我们将引入开源项目:spring-cloud-security和spring-security-oauth2,添加如下依赖。
auth-server项目:
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.security.oauth:spring-security-oauth2:2.0.6.RELEASE")
完整代码,可查看auth-server/build.gradle文件。
product-api-service项目:
compile("org.springframework.cloud:spring-cloud-starter-security:1.0.0.RELEASE")
完整代码,可以查看product-api-service/build.gradle文件。
2.2AUTH-SERVER
@EnableAuthorizationServer
protectedstaticclassOAuth2ConfigextendsAuthorizationServerConfigurerAdapter{
@Override
publicvoidconfigure(ClientDetailsServiceConfigurerclients)throwsException{
clients.inMemory()
.withClient("acme")
.secret("acmesecret")
.authorizedGrantTypes("authorization_code","refresh_token","implicit","password","client_credentials")
.scopes("webshop");
}
显然这一方法仅适用于开发和测试场景模拟Client端应用的注册流程,实际应用中采用OAuthAuthorizationServer,如LinkedIn或GitHub。
完整的代码,可以查看AuthserverApplication.java。
模拟真实环境中IdentityProvider的用户注册(OAuth术语称为ResourceOwner),通过在文件application.properties中,为每一个用户添加一行文本,如:
security.user.password=password
完整代码,可以查看application.properties文件。
实现代码也提供了2个简单的web用户界面,用于用户认证和用户准许,详细可以查看源代码:
2.3PRODUCT-API-SERVICE
为了让API代码实现OAuthResourceServer的功能,我们只需要在main方法上添加@EnableOAuth2Resource标注:
@EnableOAuth2Resource
publicclassProductApiServiceApplication{
完整代码,可以查看ProductApiServiceApplication.java。
API服务代码的实现和Part2中的组合服务代码的实现很相似。为了验证OAuth工作正常,我们添加了user-id和accesstoken的日志输出:
@RequestMapping("/{productId}")
@HystrixCommand(fallbackMethod="defaultProductComposite")
publicResponseEntity
@PathVariableintproductId,
@RequestHeader(value="Authorization")StringauthorizationHeader,
PrincipalcurrentUser){
LOG.info("ProductApi:User={},Auth={},calledwithproductId={}",
currentUser.getName(),authorizationHeader,productId);
...
1/SpringMVC将自动填充额外的参数,如currentuser和authorizationheader。
2/为了URL更简洁,我们从@RequestMapping中移除了/product。当使用EdgeServer时,它会自动添加一个/product前缀,并将请求路由到正确的服务。
3/在实际的应用中,不建议在log中输出访问令牌(accesstoken)。
2.4更新EdgeServer
最后,我们需要让EdgeServer转发OAuth访问令牌到API服务。非常幸运的是,这是默认的行为,我们不必做任何事情。
为了让URL更简洁,我们修改了Part2中的路由配置:
zuul:
ignoredServices:"*"
prefix:/api
routes:
productapi:/product/**
我们也替换了到composite-service的路由为到api-service的路由。
完整的代码,可以查看application.yml文件。
首先启动RabbitMQ:
$~/Applications/rabbitmq_server-3.4.3/sbin/rabbitmq-server
如在Windows平台,需要确认RabbitMQ服务已经启动。
接着启动基础设施微服务:
$cdsupport/auth-server;./gradlewbootRun
$cdsupport/discovery-server;./gradlewbootRun
$cdsupport/edge-server;./gradlewbootRun
$cdsupport/monitor-dashboard;./gradlewbootRun
$cdsupport/turbine;./gradlewbootRun
最后,启动业务微服务:
$cdcore/product-service;./gradlewbootRun
$cdcore/recommendation-service;./gradlewbootRun
$cdcore/review-service;./gradlewbootRun
$cdcomposite/product-composite-service;./gradlewbootRun
$cdapi/product-api-service;./gradlewbootRun
如在Windows平台,可以执行相应的bat文件-start-all.bat。
一旦微服务启动完成,并注册到服务发现服务器(ServiceDiscoveryServer),会输出如下日志:
DiscoveryClient...-registrationstatus:204
现在已经准备好尝试获取访问令牌,并使用它安全地调用API接口。
OAuth2.0规范定义了4种授予方式,获取访问令牌:
备注:AuthorizationCode和Implicit是最常用的2种方式。如前面2种方式不使用,其他2种适用于一个特殊场景。
接下来看看每一个授予流程是如何获取访问令牌的。
首先,我们通过浏览器获取一个代码许可:
code=IyJh4Y&
state=97536
备注:在请求中state参数设置为一个随机值,在响应中进行检查,避免cross-siterequestforgery攻击。
从重定向的URL中获取code参数,并保存在环境变量中:
CODE=IyJh4Y
现在作为一个安全的web服务器,使用codegrant获取访问令牌:
curlacme:acmesecret@localhost:9999/uaa/oauth/token\
-dgrant_type=authorization_code\
-dclient_id=acme\
-dcode=$CODE-s|jq.
{
"access_token":"eba6a974-3c33-48fb-9c2e-5978217ae727",
"token_type":"bearer",
"refresh_token":"0eebc878-145d-4df5-a1bc-69a7ef5a0bc3",
"expires_in":43105,
"scope":"webshop"
在环境变量中保存访问令牌,为随后访问API时使用:
TOKEN=eba6a974-3c33-48fb-9c2e-5978217ae727
再次尝试使用相同的代码获取访问令牌,应该会失败。因为code实际上是一次性密码的工作方式。
"error":"invalid_grant",
"error_description":"Invalidauthorizationcode:IyJh4Y"
4.2隐式许可(ImplicitGrant)
通过ImplicitGrant,可以跳过前面的CodeGrant。可通过浏览器直接请求访问令牌。在浏览器中使用如下URL地址:
access_token=00d182dc-9f41-41cd-b37e-59de8f882703&
token_type=bearer&
state=48532&
expires_in=42704
备注:在请求中state参数应该设置为一个随机,以便在响应中检查,避免cross-siterequestforgery攻击。
在环境变量中保存访问令牌,以便随后访问API时使用:
TOKEN=00d182dc-9f41-41cd-b37e-59de8f882703
4.3资源所有者密码凭证许可(ResourceOwnerPasswordCredentialsGrant)
在这一场景下,用户不必访问web浏览器,用户在Client端应用中输入凭证,通过该凭证获取访问令牌(从安全角度而言,如果你不信任Client端应用,这不是一个好的办法):
curl-sacme:acmesecret@localhost:9999/uaa/oauth/token\
-dgrant_type=password\
-dscope=webshop\
-dusername=user\
-dpassword=password|jq.
"access_token":"62ca1eb0-b2a1-4f66-bcf4-2c0171bbb593",
"refresh_token":"920fd8e6-1407-41cd-87ad-e7a07bd6337a",
"expires_in":43173,
在环境变量中保存访问令牌,以便在随后访问API时使用:
TOKEN=62ca1eb0-b2a1-4f66-bcf4-2c0171bbb593
4.4Client端凭证许可(ClientCredentialsGrant)
-dgrant_type=client_credentials\
-dscope=webshop|jq.
"access_token":"8265eee1-1309-4481-a734-24a2a4f19299",
"expires_in":43189,
TOKEN=8265eee1-1309-4481-a734-24a2a4f19299
现在,我们已经获取到了访问令牌,可以开始访问实际的API了。
首先在没有获取到访问令牌时,尝试访问API,将会失败:
"error":"unauthorized",
"error_description":"Fullauthenticationisrequiredtoaccessthisresource"
OK,这符合我们的预期。
接着,我们尝试使用一个无效的访问令牌,仍然会失败:
-H"Authorization:Bearerinvalid-access-token"-s|jq.
"error":"access_denied",
"error_description":"Unabletoobtainanewaccesstokenforresource'null'.Theprovidermanagerisnotconfiguredtosupportit."
再一次如期地拒绝了访问请求。
现在,我们尝试使用许可流程返回的访问令牌,执行正确的请求:
-H"Authorization:Bearer$TOKEN"-s|jq.
"productId":123,
"name":"name",
"weight":123,
"recommendations":[...],
"reviews":[...]
OK,这次工作正常了!
可以查看一下api-service(product-api-service)输出的日志记录。
2015-04-2318:39:59.030INFO79321---[ctApiService-10]s.c.m.a.p.service.ProductApiService:ProductApi:User=user,Auth=Bearera0f91d9e-00a6-4b61-a59f-9a084936e474,calledwithproductId=123
我们看到API联系AuthorizationServer,获取用户信息,并在log中打印出用户名和访问令牌。
最后,我们尝试使访问令牌失效,模拟它过期了。可以通过重启auth-server(仅在内存中存储了该信息)来进行模拟,接着再次执行前面的请求:
如我们的预期一样,之前可以接受的访问令牌现在被拒绝了。
多谢开源项目spring-cloud-security和spring-security-auth,我们可以基于OAuth2.0轻松设置安全API。然后,请记住我们使用的AuthorizationServer仅适用于开发和测试环境。