• 构建微服务-使用OAuth 2.0保护API接口


    微服务操作模型

    基于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/

  • 相关阅读:
    善待自己的恻隐之心
    FormLayout and FormData
    jquery获取元素索引值index()方法
    Kohana 之ORM文档篇
    css 圆角相框
    Kohana 之 request
    firefox通过XUL实现textoverflow:ellipsis的效果
    jquery 插件开发备注
    Kohana 之ORM实际使用篇
    PHP扩展编写与编译
  • 原文地址:https://www.cnblogs.com/rickie/p/6579636.html
Copyright © 2020-2023  润新知