微服务操作模型

基于Spring Cloud和Netflix OSS 构建微服务-Part 1

基于Spring Cloud和Netflix OSS构建微服务,Part 2

在本文中,我们将使用OAuth 2.0,创建一个的安全API,可供外部访问Part 1和Part 2完成的微服务。

关于OAuth 2.0的更多信息,可以访问介绍文档:Parecki - OAuth 2 Simplified 和 Jenkov - OAuth 2.0 Tutorial ,或者规范文档 IETF RFC 6749

我们将创建一个新的微服务,命名为product-api,作为一个外部API(OAuth 术语为资源服务器-Resource Server),并通过之前介绍过的Edge Server暴露为微服务,作为Token Relay,也就是转发Client端的OAuth访问令牌到资源服务器(Resource Server)。另外添加OAuth Authorization Server和一个OAuth Client,也就是服务消费方。

继续完善Part 2的系统全貌图,添加新的OAuth组件(标识为红色框):

我们将演示Client端如何使用4种标准的授权流程,从授权服务器(Authorization Server)获取访问令牌(Access Token),接着使用访问令牌对资源服务器发起安全访问,如API。

备注:

1/ 保护外部API并不是微服务的特殊需求,因此本文适用于任何使用OAuth 2.0保护外部API的架构;

2/ 我们使用的轻量级OAuth授权系统仅适用于开发和测试环境。在实际应用中,需要替换为一个API平台,或者委托给社交网络Facebook或Twitter的登录、授权流程。

3/ 为了降低复杂度,我们特意采用了HTTP协议。在实际的应用中,OAuth通信需要使用TLS,如HTTPS保护通信数据。

4/ 在前面的文章中,我们为了强调微服务和单体应用的差异性,每一个微服务单独运行在独立的进程中。

1. 编译源码

和在Part 2中一样,我们使用Java SE 8、Git和Gradle访问源代码,并进行编译:

git clone https://github.com/callistaenterprise/blog-microservices.git

cd blog-microservices

git checkout -b B3 M3.1

./build-all.sh

如果运行在Windows平台,则执行相应的bat文件-build-all.bat。

在Part 2的基础中,新增了2个组件源码,分别为OAuth Authorization Server,项目名为auth-server;另一个为OAuth Resource Server,项目名为product-api-service。

编译输出10条log消息:

BUILD SUCCESSFUL

2. 分析源代码

查看2个新组件是如何实现的,以及Edge Server是如何更新并支持传递OAuth访问令牌的。我们也会修改API的URL,以便于使用。

2.1 Gradle 依赖

为了使用OAuth 2.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")

compile("org.springframework.security.oauth:spring-security-oauth2:2.0.6.RELEASE")

完整代码,可以查看product-api-service/build.gradle文件。

2.2 AUTH-SERVER

授权服务器(Authorization Server)的实现比较简单直接。可直接使用@EnableAuthorizationServer标注。接着使用一个配置类注册已批准的Client端应用,指定client-id、client-secret、以及允许的授予流程和范围:

@EnableAuthorizationServer

protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

@Override

public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

clients.inMemory()

.withClient("acme")

.secret("acmesecret")

.authorizedGrantTypes("authorization_code", "refresh_token", "implicit", "password", "client_credentials")

.scopes("webshop");

}

}

显然这一方法仅适用于开发和测试场景模拟Client端应用的注册流程,实际应用中采用OAuth Authorization Server,如LinkedIn或GitHub。

完整的代码,可以查看AuthserverApplication.java。

模拟真实环境中Identity Provider的用户注册(OAuth术语称为Resource Owner),通过在文件application.properties中,为每一个用户添加一行文本,如:

security.user.password=password

完整代码,可以查看application.properties文件。

实现代码也提供了2个简单的web用户界面,用于用户认证和用户准许,详细可以查看源代码:

https://github.com/callistaenterprise/blog-microservices/tree/B3/microservices/support/auth-server/src/main/resources/templates

2.3 PRODUCT-API-SERVICE

为了让API代码实现OAuth Resource Server的功能,我们只需要在main方法上添加@EnableOAuth2Resource标注:

@EnableOAuth2Resource

public class ProductApiServiceApplication {

完整代码,可以查看ProductApiServiceApplication.java。

API服务代码的实现和Part 2中的组合服务代码的实现很相似。为了验证OAuth工作正常,我们添加了user-id和access token的日志输出:

@RequestMapping("/{productId}")

@HystrixCommand(fallbackMethod = "defaultProductComposite")

public ResponseEntity<String> getProductComposite(

@PathVariable int productId,

@RequestHeader(value="Authorization") String authorizationHeader,

Principal currentUser) {

LOG.info("ProductApi: User={}, Auth={}, called with productId={}",

currentUser.getName(), authorizationHeader, productId);

...

备注:

1/ Spring MVC 将自动填充额外的参数,如current user和authorization header。

2/ 为了URL更简洁,我们从@RequestMapping中移除了/product。当使用Edge Server时,它会自动添加一个/product前缀,并将请求路由到正确的服务。

3/ 在实际的应用中,不建议在log中输出访问令牌(access token)。

2.4 更新Edge Server

最后,我们需要让Edge Server转发OAuth访问令牌到API服务。非常幸运的是,这是默认的行为,我们不必做任何事情。

为了让URL更简洁,我们修改了Part 2中的路由配置:

zuul:

ignoredServices: "*"

prefix: /api

routes:

productapi: /product/**

这样,可以使用URL:http://localhost:8765/api/product/123,而不必像前面使用的URL:http://localhost:8765/productapi/product/123

我们也替换了到composite-service的路由为到api-service的路由。

完整的代码,可以查看application.yml文件。

3. 启动系统

首先启动RabbitMQ:

$ ~/Applications/rabbitmq_server-3.4.3/sbin/rabbitmq-server

如在Windows平台,需要确认RabbitMQ服务已经启动。

接着启动基础设施微服务:

$ cd support/auth-server;       ./gradlew bootRun

$ cd support/discovery-server;  ./gradlew bootRun

$ cd support/edge-server;       ./gradlew bootRun

$ cd support/monitor-dashboard; ./gradlew bootRun

$ cd support/turbine;           ./gradlew bootRun

最后,启动业务微服务:

$ cd core/product-service;                ./gradlew bootRun

$ cd core/recommendation-service;         ./gradlew bootRun

$ cd core/review-service;                 ./gradlew bootRun

$ cd composite/product-composite-service; ./gradlew bootRun

$ cd api/product-api-service;             ./gradlew bootRun

如在Windows平台,可以执行相应的bat文件-start-all.bat。

一旦微服务启动完成,并注册到服务发现服务器(Service Discovery Server),会输出如下日志:

DiscoveryClient ... - registration status: 204

现在已经准备好尝试获取访问令牌,并使用它安全地调用API接口。

4. 尝试4种OAuth授权流程

OAuth 2.0规范定义了4种授予方式,获取访问令牌:

更详细信息,可查看Jenkov - OAuth 2.0 Authorization

备注:Authorization Code 和Implicit是最常用的2种方式。如前面2种方式不使用,其他2种适用于一个特殊场景。

接下来看看每一个授予流程是如何获取访问令牌的。

4.1 授权代码许可(Authorization Code Grant)

首先,我们通过浏览器获取一个代码许可:

http://localhost:9999/uaa/oauth/authorize? response_type=code& client_id=acme& redirect_uri=http://example.com& scope=webshop& state=97536

先登录(user/password),接着重定向到类似如下URL:

http://example.com/?

code=IyJh4Y&

state=97536

备注:在请求中state参数设置为一个随机值,在响应中进行检查,避免cross-site request forgery攻击。

从重定向的URL中获取code参数,并保存在环境变量中:

CODE=IyJh4Y

现在作为一个安全的web服务器,使用code grant获取访问令牌:

curl acme:acmesecret@localhost:9999/uaa/oauth/token \

-d grant_type=authorization_code \

-d client_id=acme \

-d redirect_uri=http://example.com \

-d code=$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实际上是一次性密码的工作方式。

curl acme:acmesecret@localhost:9999/uaa/oauth/token \

-d grant_type=authorization_code \

-d client_id=acme \

-d redirect_uri=http://example.com \

-d code=$CODE -s | jq .

{

"error": "invalid_grant",

"error_description": "Invalid authorization code: IyJh4Y"

}

4.2 隐式许可(Implicit Grant)

通过Implicit Grant,可以跳过前面的Code Grant。可通过浏览器直接请求访问令牌。在浏览器中使用如下URL地址:

http://localhost:9999/uaa/oauth/authorize? response_type=token& client_id=acme& redirect_uri=http://example.com& scope=webshop& state=48532

登录(user/password)并验证通过,浏览器重定向到类似如下URL:

http://example.com/#

access_token=00d182dc-9f41-41cd-b37e-59de8f882703&

token_type=bearer&

state=48532&

expires_in=42704

备注:在请求中state参数应该设置为一个随机,以便在响应中检查,避免cross-site request forgery攻击。

在环境变量中保存访问令牌,以便随后访问API时使用:

TOKEN=00d182dc-9f41-41cd-b37e-59de8f882703

4.3 资源所有者密码凭证许可(Resource Owner Password Credentials Grant)

在这一场景下,用户不必访问web浏览器,用户在Client端应用中输入凭证,通过该凭证获取访问令牌(从安全角度而言,如果你不信任Client端应用,这不是一个好的办法):

curl -s acme:acmesecret@localhost:9999/uaa/oauth/token  \

-d grant_type=password \

-d client_id=acme \

-d scope=webshop \

-d username=user \

-d password=password | jq .

{

"access_token": "62ca1eb0-b2a1-4f66-bcf4-2c0171bbb593",

"token_type": "bearer",

"refresh_token": "920fd8e6-1407-41cd-87ad-e7a07bd6337a",

"expires_in": 43173,

"scope": "webshop"

}

在环境变量中保存访问令牌,以便在随后访问API时使用:

TOKEN=62ca1eb0-b2a1-4f66-bcf4-2c0171bbb593

4.4 Client端凭证许可(Client Credentials Grant)

在最后一种情况下,我们假定用户不必准许就可以访问API。在这种情况下,Client端应用进行验证自己的授权服务器,并获取访问令牌:

curl -s acme:acmesecret@localhost:9999/uaa/oauth/token  \

-d grant_type=client_credentials \

-d scope=webshop | jq .

{

"access_token": "8265eee1-1309-4481-a734-24a2a4f19299",

"token_type": "bearer",

"expires_in": 43189,

"scope": "webshop"

}

在环境变量中保存访问令牌,以便在随后访问API时使用:

TOKEN=8265eee1-1309-4481-a734-24a2a4f19299

5.访问API

现在,我们已经获取到了访问令牌,可以开始访问实际的API了。

首先在没有获取到访问令牌时,尝试访问API,将会失败:

curl 'http://localhost:8765/api/product/123' -s | jq .

{

"error": "unauthorized",

"error_description": "Full authentication is required to access this resource"

}

OK,这符合我们的预期。

接着,我们尝试使用一个无效的访问令牌,仍然会失败:

curl 'http://localhost:8765/api/product/123' \

-H  "Authorization: Bearer invalid-access-token" -s | jq .

{

"error": "access_denied",

"error_description": "Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it."

}

再一次如期地拒绝了访问请求。

现在,我们尝试使用许可流程返回的访问令牌,执行正确的请求:

curl 'http://localhost:8765/api/product/123' \

-H  "Authorization: Bearer $TOKEN" -s | jq .

{

"productId": 123,

"name": "name",

"weight": 123,

"recommendations": [...],

"reviews": [... ]

}

OK,这次工作正常了!

可以查看一下api-service(product-api-service)输出的日志记录。

2015-04-23 18:39:59.014  INFO 79321 --- [ XNIO-2 task-20] o.s.c.s.o.r.UserInfoTokenServices        : Getting user info from: http://localhost:9999/uaa/user

2015-04-23 18:39:59.030  INFO 79321 --- [ctApiService-10] s.c.m.a.p.service.ProductApiService      : ProductApi: User=user, Auth=Bearer a0f91d9e-00a6-4b61-a59f-9a084936e474, called with productId=123

2015-04-23 18:39:59.381  INFO 79321 --- [ctApiService-10] s.c.m.a.p.service.ProductApiService      : GetProductComposite http-status: 200

我们看到API 联系Authorization Server,获取用户信息,并在log中打印出用户名和访问令牌。

最后,我们尝试使访问令牌失效,模拟它过期了。可以通过重启auth-server(仅在内存中存储了该信息)来进行模拟,接着再次执行前面的请求:

curl 'http://localhost:8765/api/product/123' \

-H  "Authorization: Bearer $TOKEN" -s | jq .

{

"error": "access_denied",

"error_description": "Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it."

}

如我们的预期一样,之前可以接受的访问令牌现在被拒绝了。

6. 总结

多谢开源项目spring-cloud-security和spring-security-auth,我们可以基于OAuth 2.0轻松设置安全API。然后,请记住我们使用的Authorization Server仅适用于开发和测试环境。

7. 下一步

在随后的文章中,将使用ELK 技术栈(Elasticsearch、LogStash和Kibana)实现集中的log管理。

英文原文链接:

构建微服务(Blog Series - Building Microservices)

http://callistaenterprise.se/blogg/teknik/2015/05/20/blog-series-building-microservices/

构建微服务-使用OAuth 2.0保护API接口的更多相关文章

  1. Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

    技术背景 前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢. 当然,因为是REST API接口,外部客户端直接调用各个微服务是没有问 ...

  2. SpringBoot微服务电商项目开发实战 --- api接口安全算法、AOP切面及防SQL注入实现

    上一篇主要讲了整个项目的子模块及第三方依赖的版本号统一管理维护,数据库对接及缓存(Redis)接入,今天我来说说过滤器配置及拦截设置.接口安全处理.AOP切面实现等.作为电商项目,不仅要求考虑高并发带 ...

  3. 构建微服务(Building Microservices)-PDF 文档

    闲时翻译了几篇基于Spring Cloud.Netflix OSS 构建微服务的英文文章,为方便分享交流,整理为PDF文档. PDF 文档目录: 目录 一.微服务操作模型... 3 1.     前提 ...

  4. NodeJS 基于 Dapr 构建云原生微服务应用,从 0 到 1 快速上手指南

    Dapr 是一个可移植的.事件驱动的运行时,它使任何开发人员能够轻松构建出弹性的.无状态和有状态的应用程序,并可运行在云平台或边缘计算中,它同时也支持多种编程语言和开发框架.Dapr 确保开发人员专注 ...

  5. Spring Cloud 微服务中搭建 OAuth2.0 认证授权服务

    在使用 Spring Cloud 体系来构建微服务的过程中,用户请求是通过网关(ZUUL 或 Spring APIGateway)以 HTTP 协议来传输信息,API 网关将自己注册为 Eureka ...

  6. 基于Spring Cloud和Netflix OSS构建微服务,Part 2

    在上一篇文章中,我们已使用Spring Cloud和Netflix OSS中的核心组件,如Eureka.Ribbon和Zuul,部分实现了操作模型(operations model),允许单独部署的微 ...

  7. 使用Spring Cloud和Docker构建微服务架构

    原文:https://dzone.com/articles/microservice-architecture-with-spring-cloud-and-do 作者:Alexander Lukyan ...

  8. Spring Cloud构建微服务架构(五)服务网关

    通过之前几篇Spring Cloud中几个核心组件的介绍,我们已经可以构建一个简略的(不够完善)微服务架构了.比如下图所示: 我们使用Spring Cloud Netflix中的Eureka实现了服务 ...

  9. Spring Cloud构建微服务架构 - 服务网关

    通过之前几篇Spring Cloud中几个核心组件的介绍,我们已经可以构建一个简略的(不够完善)微服务架构了.比如下图所示: alt 我们使用Spring Cloud Netflix中的Eureka实 ...

随机推荐

  1. OGG学习笔记04-OGG复制部署快速参考

    OGG学习笔记04-OGG复制部署快速参考 源端:Oracle 10.2.0.5 RAC + ASM 节点1 Public IP地址:192.168.1.27 目标端:Oracle 10.2.0.5 ...

  2. TypeScript教程2

    在TS中,我们允许开发人员使用面向对象技术. 1.类让我们看看一个简单的基于类的例子: class Greeter { greeting: string; constructor(message: s ...

  3. EF6多线程与分库架构设计之Repository

    1.项目背景 这里简单介绍一下项目需求背景,之前公司的项目基于EF++Repository+UnitOfWork的框架设计的,其中涉及到的技术有RabbitMq消息队列,Autofac依赖注入等常用的 ...

  4. DllRegisterServer的调用失败的问题解决方法

    1'按键盘上的win+x键调出常用命令. 2'选择“命令提示符(管理员)“ 3'在”命令提示符“中输入”regsvr32 c:\Windows\SysWOW64\comdlg32.ocx“或其他ocx ...

  5. sonarqube+Scanner代码质量管理工具

    本文相关描述基于sonarqube 6.2版本. 下载地址: sonarqube下载网址:https://www.sonarqube.org/downloads/ Scanner下载网址(用于扫描项目 ...

  6. Android音频焦点详解(上)

    转载请注明出处:http://www.cnblogs.com/landptf/p/6384112.html 2017年开年第一篇博客,很早就想总结一下Android音频的相关知识.今天我们先来看一下音 ...

  7. 《经久不衰的Spring框架:SpringMVC 统括》

    前言:经久不衰的Spring 这几年,前端技术更新换代速度之快,每一年"最火的前端技术"排行榜都会换一番场景,本当に信じかねる.是"只闻新人笑不见旧人哭",还是 ...

  8. 我的JS 中级学习篇

    在codefordream上进入中级学习后,感觉立马从js的基础学习往前跳了好远,上面的东西好像都是第一次看到一样.这时候才发现,说来也曾接触过js,但是这时候才发现对js的认识就停在知道两点:js中 ...

  9. 使用git恢复未提交的误删数据

    不小心将项目中一个文件夹删除还未提交,或者已经提交, 此时想要恢复数据该怎么办? 答案是git reflog,使用git reflog命令可以帮助恢复git误操作,进行数据恢复. 操作过程: 打开终端 ...

  10. Omi教程-插件体系

    插件体系 Omi是Web组件化框架,怎么又来了个插件的概念? 可以这么理解: Omi插件体系可以赋予dom元素一些能力,并且可以和组件的实例产生关联. omi-drag 且看这个例子: 点击这里→在线 ...