In this post, we will not only cover how to use Go to create a RESTful
JSON API, but we will also talk about good RESTful design.

三、JWT介绍

Things We Didn’t Do

1、版本控制
API版本迭代 &
跨版本资源访问。常用做法是将版本号放在URL,较为简洁,例如:https://localhost:8080/v1/
另一种做法是将版本号放在HTTP头信息中。

2、授权验证:涉及到OAuth和JWT。
(1)OAuth 2.0,OAuth2 is an authentication
framework,RFC
6749
OAuth2是一种授权框架,提供了一套详细的、可供实践的指导性解决方案。OAuth
2.0定义了四种授权方式。授权码模式(authorization
code)、简化模式(implicit)、密码模式(resource owner password
credentials)、客户端模式(client credentials)。

(2)JSON web tokens,JWT is an authentication
protocol,RFC
7519
JWT是一种安全协议。基本思路就是用户提供用户名和密码给认证服务器,服务器验证用户提交信息信息的合法性;如果验证成功,会产生并返回一个Token(令牌),用户可以使用这个token访问服务器上受保护的资源。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

header:定义算法(alg:ALGORITHM)和TOKEN TYPE(typ)

{
  "alg": "HS256",
  "typ": "JWT"
}

Data:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

3、 eTags:关于缓存、性能和用户标识和追踪。

Token 存储在 Cookie 中,这样客户端注销时,自然可以清空掉 
注销时,将 Token 存放到分布式缓存中,每次校验 Token 时区检查下该 Token
是否已注销。不过这样也就失去了快速校验 Token 的优点。 
多采用短期令牌,比如令牌有效期是 20 分钟,这样可以一定程度上降低注销后
Token 可用性的风险。

增加路径分发功能

路径又称”终点”(endpoint),表示API的具体网址。在RESTful架构中,每个网址代表一种资源(resource)。
第三方组件(Gorilla Mux package): “github.com/gorilla/mux”

package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/gorilla/mux"
)

func main() {
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/", Index)
    router.HandleFunc("/todos", TodoIndex)
    router.HandleFunc("/todos/{todoId}", TodoShow)

    log.Fatal(http.ListenAndServe(":8080", router))
}

func Index(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Welcome!")
}

func TodoIndex(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Todo Index!")
}

func TodoShow(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    todoId := vars["todoId"]
    fmt.Fprintln(w, "Todo show:", todoId)
}

访问测试:

$ curl http://localhost:8080/todo
404 page not found
$ curl http://localhost:8080/todos
Todo Index! ,"/todos"
$ curl http://localhost:8080/todos/{123}
TodoShow: ,"123"

简单:不管是 OAuth 服务提供者还是应用开发者,都很容易于理解与使用; 
安全:没有涉及到用户密钥等信息,更安全更灵活; 
开放:任何服务提供商都可以实现 OAuth,任何软件开发商都可以使用 OAuth;

增强功能:日志

2017/05/23 15:57:23 http: multiple response.WriteHeader calls
2017/05/23 15:57:23 GET /todos  TodoIndex   6.945807ms
2017/05/23 16:18:40 http: multiple response.WriteHeader calls
2017/05/23 16:18:40 GET /todos  TodoIndex   2.127435ms

因为 Request 中没有包含 Authorization header,服务器会返回一个 401
Unauthozied 给客户端,并且在 Response 的 Header “WWW-Authenticate”
中添加信息。

启动入口是不是清爽很多!

Main.go

Main.go
package main

import (
    "log"
    "net/http"
)

func main() {
    router := NewRouter()
    log.Fatal(http.ListenAndServe(":8080", router))
}

web
access:http://localhost:8080/todos

Todo Index! ,”/todos”
[
{
“id”:0,
“name”:”Write sth ….”,
“completed”:false,
“due”:”0001-01-01T00:00:00
},
{
“id”:1,
“name”:”Host meetup ….”,
“completed”:false,
“due”:”0001-01-01T00:00:00Z”
}
]

下面会重点介绍两种基于 Token 的认证方案 JWT/Oauth2.0。

抽象数据模型

创建一个数据模型“Todo”、“Routes”。在其它语言中,使用类(class)实现。
在Go语言中,没有class,必须使用结构(struct)。

Todo.go

package main

import "time"

type Todo struct {
      Id        int       `json:"id"`
      Name      string    `json:"name"`
      Completed bool      `json:"completed"`
      Due       time.Time `json:"due"`
}

type Todos []Todo

Routes.go

package main

import (
    "net/http"
    "github.com/gorilla/mux"
)

type Route struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.HandlerFunc
}

type Routes []Route

JWT
是由三段信息构成的,第一段为头部(Header),第二段为载荷(Payload),第三段为签名(Signature)。每一段内容都是一个
JSON 对象,将每一段 JSON 对象采用 BASE64 编码,将编码后的内容用.
链接一起就构成了 JWT 字符串。如下: 
header.payload.signature

What is a JSON API?

JSON API
是数据交互规范,用以定义客户端如何获取与修改资源,以及服务器如何响应对应请求。JSON
API设计用来最小化请求的数量,以及客户端与服务器间传输的数据量。通过遵循共同的约定,可以提高开发效率,利用更普遍的工具,基于
JSON API 的客户端还能够充分利用缓存,以提升性能。

示例:

{
  "links": {
    "posts.author": {
      "href": "http://example.com/people/{posts.author}",
      "type": "people"
    },
    "posts.comments": {
      "href": "http://example.com/comments/{posts.comments}",
      "type": "comments"
    }
  },
  "posts": [{
    "id": "1",
    "title": "Rails is Omakase",
    "links": {
      "author": "9",
      "comments": [ "5", "12", "17", "20" ]
    }
  }]
}

公共的声明 : 
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.
但不建议添加敏感信息,因为该部分在客户端可解密。

增强功能:持久化

func TodoCreate(w http.ResponseWriter, r *http.Request) {
    var todo Todo
    //add Todo instance
}

私有的声明 : 
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为
base64 是对称解密的,意味着该部分信息可以归类为明文信息。

重构:Handlers & Router

Handlers.go

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "github.com/gorilla/mux"
)

func Index(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Welcome!")
}

func TodoIndex(w http.ResponseWriter, r *http.Request) {
    todos := Todos{
        Todo{Name: "Write presentation"},
        Todo{Name: "Host meetup"},
    }

    if err := json.NewEncoder(w).Encode(todos); err != nil {
        panic(err)
    }
}

func TodoShow(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    todoId := vars["todoId"]
    fmt.Fprintln(w, "Todo show:", todoId)
}

Router.go

package main

import (
    "net/http"
    "github.com/gorilla/mux"
)

func NewRouter() *mux.Router {
    router := mux.NewRouter().StrictSlash(true)
    for _, route := range routes {
        var handler http.Handler
        handler = route.HandlerFunc
        handler = Logger(handler, route.Name)

        router.
            Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            Handler(handler)

    }
    return router
}

基于 Token 的认证

启动一个RESTful服务

$ go run main.go

$ curl http://localhost:8080
Hello,"/"

package main

import (
    "fmt"
    "html"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
    })

    log.Fatal(http.ListenAndServe(":8080", nil))

}

四大角色

参考文献

  1. 阮一峰:RESTful API
    设计指南
  2. CORY LANOU:Making a RESTful JSON API in
    Go,2014Nov
  3. InfoQ:使用ETags减少Web应用带宽和负载
  4. Stackoverflow:jwt vs oauth
    authentication
  5. OAuth 2 VS JSON Web Tokens:How to secure an
    API,20160605
  6. 阮一峰:理解OAuth
    2.0,201405

授权流程

随着 Restful API、微服务的兴起,基于 Token
的认证现在已经越来越普遍。Token 和 Session ID 不同,并非只是一个
key。Token 一般会包含用户的相关信息,通过验证 Token
就可以完成身份校验。像 Twitter、微信、QQ、GitHub 等公有服务的 API
都是基于这种方式进行认证的,一些开发框架如 OpenStack、Kubernetes 内部
API 调用也是基于 Token 的认证。基于 Token 认证的一个典型流程如下:

JWT 认证流程

JWT 结构

OAuth 的官网介绍:An open protocol to allow secure API authorization in
a simple and standard method from desktop and web applications。OAuth
是一种开放的协议,为桌面程序或者基于 BS 的 web
应用提供了一种简单的,标准的方式去访问需要用户授权的 API 服务。OAUTH
认证授权具有以下特点:

亚洲城ca88 1

 

  1. 载荷(payload) 
    载荷就是存放有效信息的地方。有效信息包含三个部分:

客户端的授权模式

Session 复制依赖于应用服务器,需要应用服务器有 Session
复制能力,不过现在大部分应用服务器如 Tomcat、JBoss、WebSphere
等都已经提供了这个能力。

用户输入登录信息(或者调用 Token
接口,传入用户信息),发送到身份认证服务进行认证(身份认证服务可以和服务端在一起,也可以分离,看微服务拆分情况了)。 
身份验证服务验证登录信息是否正确,返回接口(一般接口中会包含用户基础信息、权限范围、有效时间等信息),客户端存储接口,可以存储在
Session 或者数据库中。 
用户将 Token 放在 HTTP 请求头中,发起相关 API 调用。 
被调用的微服务,验证 Token 权限。 
服务端返回相关资源和数据。

示例如下: 
亚洲城ca88 2

OAuth 2.0 是 OAuth 协议的下一版本,但不向后兼容 OAuth 1.0,即完全废止了
OAuth 1.0。 OAuth 2.0
关注客户端开发者的简易性。要么通过组织在资源拥有者和 HTTP
服务商之间的被批准的交互动作代表用户,要么允许第三方应用代表用户获得访问的权限。同时为
Web 应用,桌面应用和手机,和起居室设备提供专门的认证流程。2012 年 10
月,OAuth 2.0 协议正式发布为 RFC 6749。

HTTP 基本认证

  1. 签名(signature) 
    创建签名需要使用 Base64 编码后的 header 和 payload 以及一个秘钥。将
    base64 加密后的 header 和 base64 加密后的 payload 使用.
    连接组成的字符串,通过 header 中声明的加密方式进行加盐 secret
    组合加密,然后就构成了 jwt 的第三部分。 
    比如:HMACSHA256(base64UrlEncode(header) + “.” +
    base64UrlEncode(payload), secret)

在头部指明了签名算法是 HS256 算法。

四、OAuth 2.0 介绍

标准中注册的声明(建议但不强制使用): 
iss:JWT 签发者 
sub:JWT 所面向的用户 
aud:接收 JWT 的一方 
exp:JWT 的过期时间,这个过期时间必须要大于签发时间 
nbf:定义在什么时间之前,该 JWT 都是不可用的 
iat:JWT 的签发时间 
jti:JWT 的唯一身份标识,主要用来作为一次性 token, 从而回避重放攻击。

客户端把用户名和密码用 BASE64 加密后,放在 Authorization Header
中发送给服务器, 认证成功。

基于 Session 的认证

亚洲城ca88 3

基于 Token 认证的好处如下: 
服务端无状态:Token 机制在服务端不需要存储 session 信息,因为 Token
自身包含了所有用户的相关信息。 
性能较好,因为在验证 Token
时不用再去访问数据库或者远程服务进行权限校验,自然可以提升不少性能。 
支持移动设备。 
支持跨程序调用,Cookie 是不允许垮域访问的,而 Token 则不存在这个问题。

OAuth 2.0 的流程如下: 
亚洲城ca88 4

在微服务架构下,每个微服务拆分的粒度会很细,并且不只有用户和微服务打交道,更多还有微服务间的调用。这个时候上述两个方案都无法满足,就要求必须要将
Session
从应用服务器中剥离出来,存放在外部进行集中管理。可以是数据库,也可以是分布式缓存,如
Memchached、Redis 等。这正是 David Borsos 建议的第二种方案,分布式
Session 方案。

JWT 的优点: 
跨语言,JSON 的格式保证了跨语言的支撑 
基于 Token,无状态 
占用字节小,便于传输

客户端:客户端是代表资源所有者对资源服务器发出访问受保护资源请求的应用程序。 
资源拥有者:资源拥有者是对资源具有授权能力的人。 
资源服务器:资源所在的服务器。 
授权服务器:为客户端应用程序提供不同的
Token,可以和资源服务器在统一服务器上,也可以独立出去。

除此之外,Session 复制的一大缺陷在于当节点数比较多时,大量的 Session
数据复制会占用较多网络资源。Session
粘滞是通过负载均衡器,将统一用户的请求都分发到固定的服务器节点上,这样就保证了对某一用户而言,Session
数据始终是正确的。不过这种方案依赖于负载均衡器,并且只能满足水平扩展的集群场景,无法满足应用分割后的分布式场景。

(A)用户打开客户端以后,客户端要求用户给予授权。(B)用户同意给予客户端授权。(C)客户端使用上一步获得的授权,向认证服务器申请令牌。(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。(E)客户端使用令牌,向资源服务器申请获取资源。(F)资源服务器确认令牌无误,同意向客户端开放资源。

服务器将 Authorization Header 中的用户名密码取出,进行验证,
如果验证通 
过,将根据请求,发送资源给客户端。

客户端调用登录接口(或者获取 token 接口),传入用户名密码。 
服务端请求身份认证中心,确认用户名密码正确。 
服务端创建 JWT,返回给客户端。 
客户端拿到
JWT,进行存储(可以存储在缓存中,也可以存储在数据库中,如果是浏览器,可以存储在
Cookie 中)在后续请求中,在 HTTP 请求头中加上 JWT。 
服务端校验 JWT,校验通过后,返回相关资源和数据。

由授权流程图中可以看到 OAuth 2.0
有四个角色:客户端、资源拥有者、资源服务器、授权服务器。

HTTP Basic Authentication(HTTP 基本认证)是 HTTP 1.0
提出的一种认证机制,这个想必大家都很熟悉了,我不再赘述。HTTP
基本认证的过程如下: 
客户端发送 HTTP Request 给服务器。

基于 Session
的认证应该是最常用的一种认证机制了。用户登录认证成功后,将用户相关数据存储到
Session 中,单体应用架构中,默认 Session 会存储在应用服务器中,并且将
Session ID 返回到客户端,存储在浏览器的 Cookie 中。

JSON Web Token(JWT)是为了在网络应用环境间传递声明而执行的一种基于 JSON
的开放标准(RFC 7519)。来自 JWT RFC 7519 标准化的摘要说明:JSON Web
Token 是一种紧凑的,URL 安全的方式,表示要在双方之间传输的声明。JWT
一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该
Token 也可直接被用于认证,也可被加密。

亚洲城ca88 5

  1. 头部(Header) 
    头部用于描述关于该 JWT
    的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个
    JSON 对象。

但是在分布式架构下,Session
存放于某个具体的应用服务器中自然就无法满足使用了,简单的可以通过 Session
复制或者 Session 粘制的方案来解决。

标准中注册的声明 
公共的声明 
私有的声明

客户端必须得到用户的授权(Authorization Grant),才能获得令牌(access
token)。OAuth 2.0
定义了四种授权方式:authorizationcode、implicit、resource owner password
credentials、client credentials。

关于 Token 注销: 
Token 的注销,由于 Token
不存储在服务端,由客户端存储,当用户注销时,Token
的有效时间还没有到,还是有效的。所以如何在用户注销登录时让 Token
注销是一个要关注的点。一般有如下几种方式:

相关文章