Templates and Options in Go

这前的例子只是用于演示,从本章开始,我们将讨论实际使用到代码,甚至可以用于产品阶段。

为了做到这一点,我们将在这一章重点解决一些事情。在上章中,我们看到了整个social network网络应用APP的主要功能。现在,我们需要从REST的角度来落实这些功能的可能性。

  • 使用OPTIONS提供内置的文档,解释我们每个资源端点的用途
  • 考虑不同的输出格式,并且介绍如何实现它们
  • 实现我们API的安全性
  • 允许用户注册和使用安全密码
  • 允许用户从基于网页登录
  • 实现基于OAuth类似的认证系统
  • 允许其它的应用程序发送代表其它用户的请求

当所有的这些事情都做完后,我们将实际了整个服务的基础功能,包括充许用户使用界面,也可以直接使用API,或者通过第三方服务来使用这些功能。

Sharing our OPTIONS

依据HTTP规范,OPTIONS表示可选和用途,这是REST的最佳实践.

根据RFC 2616, HTTP/1.1规范,对于OPTIONS请求的响应应该返回资源可以做哪些事情或者请求的端点。

你可以通过https://www.ietf.org/rfc/rfc2616.txt了解RFC2616

换句话说,在我们之前的例子中,我们通过OPTIONS调用/api/users, 它应该返回一个REST资源请求,可用的GET, POST, PUT,DELETE操作。

以下的代码只是简单的修改了我们之前的 API,它包含了OPTIONS请求。

func Init() {
    Routes = mux.NewRouter()
    Routes.HandleFunc("/api/users", UserCreate).Methods("POST")
    Routes.HandleFunc("/api/users", UsersRetrieve).Methods("GET")
    Routes.HandleFunc("/api/users/{id:[0-9]+}",UsersUpdate).Methods("PUT")
    Routes.HandleFunc("/api/users", UsersInfo).Methods("OPTIONS")
}

然后我们添加一个处理函数

func UsersInfo(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Allow","DELETE,GET,HEAD,OPTIONS,POST,PUT")
}

调用cURL进行测试,将返回以下的内容

以上的OPTIONS基本能够满足我们的要求。但是它没有包含对内容的格式,我们应该尽可能的表现它们。

一种方式是通过package的文档。但请记住,这完全是一个可选的。现在让我们看看如何实现应用程序自己的API文档

package specification
type MethodPOST struct {
    POST EndPoint
} 
type MethodGET struct {
    GET EndPoint
} 
type MethodPUT struct {
PUT EndPoint
} 
type MethodOPTIONS struct {
    OPTIONS EndPoint
}
type EndPoint struct {
    Description string `json:"description"`
    Parameters []Param `json:"parameters"`
} 
type Param struct {
    Name string "json:name"
    ParameterDetails Detail `json:"details"`
} 
type Detail struct {
    Type string "json:type"
    Description string `json:"description"`
    Required bool "json:required"
}

var UserOPTIONS = MethodOPTIONS{ OPTIONS: EndPoint{ Description: "This
page" }}

var UserPostParameters = []Param{ {Name: "Email", ParameterDetails:
Detail{Type:"string", Description: "A new user's email address", Required:
false} }}

var UserPOST = MethodPOST{ POST: EndPoint{ Description: "Create a user",
Parameters: UserPostParameters } }
var UserGET = MethodGET{ GET: EndPoint{ Description: "Access a user" }}

然后你可以在 api.go文件中引用这个文档。首先我们创建一个通用的接口数组(slice)来包含可用的方法

type DocMethod interface {
}

然后,我们可以在我们的UsersInfo方法中,引入各种各样的方法

func UsersInfo(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Allow","DELETE,GET,HEAD,OPTIONS,POST,PUT")
    UserDocumentation := []DocMethod{}
    UserDocumentation = append(UserDocumentation, Documentation.UserPOST)
    UserDocumentation = append(UserDocumentation, Documentation.UserOPTIONS)
    output := SetFormat(UserDocumentation)
    fmt.Fprintln(w,string(output))
}

实现不同的输出格式

在整个API格式世界中,使用最大的两个是XML和JSON. 在JSON之前,开发者和技术专家都使用XML, 但这并不意味着JSON没有缺点。如果它没有格外的空格缩进,它不易于人类可读。同时它增加了文档的大小。而且它不能添加注释。

这里还有其它的格式,YAML(YAML Ain't Markup Language),它采用空格缩进,使人类易读。如下所示

---
api:
  name: Social Network
  methods:
    - GET
    - POST
    - PUT
    - OPTIONS
    - DELETE

这种缩进跟Pythone中的代码块的缩进一样

Go中有很多第三方库实现了YAML, 最著名的是go-yaml,你可以通过https://github.com/go-yaml/yaml.阅读更多

TOML or Tom's Obvious, Minimal Language跟.ini 样式的配置文件很相似

Rolling our own data representation format

为了构建我们自己的数据格式,TOML是一种很好的格式。主要是有多种方式可以输出这种格式。

你可能想要立即通过Go的文本格式来输出TOML. 考虑以下的结构和循环:

type GenericData struct {
    Name string
    Options GenericDataBlock
}
type GenericDataBlock struct {
    Server string
    Address string
}
func main() {
    Data := GenericData{ Name: "Section", Options: GenericDataBlock{Server:
    "server01", Address: "127.0.0.1"}}
}

当我们通过text template来解析整个structure, 它将生成我们想要的格式

{{range $index, $value := Options}}
    $index = $value
{{end}}

对于这种方法,最大的问题是你没有内在的系统来解码数据,换言之,你可以生成这种数据的格式,但是你不能将它解析回Go structure.

另一个问题是,承着格式复杂性的增长,在Go模板库中有限的控制结构很难满足错综复杂和怪异的格式。

如果你想滚动(roll, 编码和解码)你自己的格式,你应该避免使用text templates. 而应考虑encoding package, 它们可以编码和解码结构化的数据格式.

我们将在之后的章节中详细了解这些编码包.

Introducing security and authentication

任何web service和API重要的一方而是保持信息的安全和允许特定的用户做特定的事情.

在历史上,有很多的方法可以做这个事情,最早的是HTTP digest 认证。

另一种方法是包含开发者证书,称为API 密钥. 这种方式已经不在推荐。主要是因为API的安全完全是依整 于这些开发者证书的安全。然而,很明显它用于认证和作为服务的提供者,它允许跟踪谁发起了请求,和限制这些请求.

现在使用最广的是OAuth, 我们在之后讲到。现在我们先讲最重要的, 我们需要确保我们API的访问是通过HTTPS来访问的。

Forcing HTTPS

到目前为此,我们的API已经可以让客户端或者用户来做一些事情了。比如,创建用户,更新它们的数据,包含这些用户的图片数据。但我们不得不考虑一些开放的真实环境下的情况。

安全的第一步是强制将https替换我们的http. Go语言通过TLS(而不是SSL)实现了HTTPS. 这是因为TLS在服务器端比SSL更安全。主要的一个原因是SSL3.0在2014暴露了一个很严重的bug.

你可能通过https://poodlebleed.com/ 了解这个Poodlebleed bug.

让我们看看如何将不安全的请求变更到对应的安全路由

package main
import
(
  "fmt"
  "net/http"
  "log"
  "sync"
)
const (
  serverName = "localhost"
  SSLport = ":443"
  HTTPport = ":8080"
  SSLprotocol = "https://"
  HTTPprotocol = "http://"
)
func secureRequest(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w,"You have arrived at port 443, but you are not yet
  secure.")
}

这是我们正确的一个节点(临时的). 正如信息所示,它还不是TSL(or SSL), 所以在实际中我们还不能监听HTTPS连接。

func redirectNonSecure(w http.ResponseWriter, r *http.Request) {
  log.Println("Non-secure request initiated, redirecting.")
  redirectURL := SSLprotocol + serverName + r.RequestURI
  http.Redirect(w, r, redirectURL, http.StatusOK)
}

以上的这个函数是一个转发处理函数,你可能会注意到, 这里使用http.StatusOk状态码——很明显,我们想要发送301 Move Permanently error(或者http.StatusMovedPermanently constant). 可是,你如果测试过,你会发现,你的浏览器将缓存这个状态,并且自动尝试帮你转向(redirect).

func main() {
  wg := sync.WaitGroup{}
  log.Println("Starting redirection server, try to access @ http:")

  wg.Add(1)
  go func() {
    http.ListenAndServe(HTTPport,http.HandlerFunc(redirectNonSecure))
    wg.Done()
  }()

  wg.Add(1)
  go func() {
    http.ListenAndServe(SSLport,http.HandlerFunc(secureRequest))
    wg.Done()
  }()
  wg.Wait()
}

为什么我们要将这些方法包含在匿名的goroutines中呢?这是因为ListenAndServe函数会发生阻塞。那么第二条语句将不会被执行。

当然,你也可以简单的设置第一个goroutine, 这就允许程序移动到第二个服务器上。我们在这里为了演示的目的,提供了更细粒的控制。

添加TLS支持

在前面的例子中,我们没有监听HTTPS连接。Go语言要做这个事情非常简单。可是像很多SSL/TLS一样,最麻烦的是处理你的证书.

对于本书的例子,我们都将使用自己签发的证书。Go语言做这个事情很容易,在crypto/tls包中,有一个generate_cert.go 的文件,你可以使用它来生成你证书的密钥。

go run generate_cert.go --host localhost --ca true

你可以将生成的文件移动到任何你想要放置的地方,通常为API运行的目录。

接下来,让我们移除掉http.ListenAndServe函数,将它改为http.ListenAndServeTLS. 它需要额外的参数,用来包含密钥的路径.

http.ListenAndServeTLS(SSLport, "cert.pem", "key.pem",
http.HandlerFunc(secureRequest))

我们secureRequest处理函数中,简单的使用以下的代码

fmt.Fprintln(w,"You have arrived at port 443, and now you are marginally
more secure.")

如果我们在浏览器中运行这段代码,我们将会看到警告。

如果你想获得一个第三方认证的证书,又可以一年免费的。你可以使用https://www.startssl.com

Letting users register and authenticate

你可能鹿得,作为我们应用程序的一问份,我们自己提供了界面,它允许我们提供HTML为API自身提供HTML接口。

最简单的实现用户认证的方法是通过存储和使用密码散列。之前,很多服务器是直接保存纯文本密码。

我们不仅仅存储用户的密码,还需要有一个盐值。这使得破坏字典和rainbow(公开的寄密码散列值)攻击。

为此,我们创建了一个package, 称为password, 它允许我们生成一个随机的盐值,然后使用它来加密密码。

我们可以使用 GenerateHas()用来创建和校验密码.

A quick hit – generating a salt

获得一个密码很简单,同时创建一个安全的hash值也同样简单,对于我们的认证安全来讲,缺少的是一个盐值。让我们看看,我们如何来做这件事情,首先,我们在数据库中添加password和一个盐值字段。

ALTER TABLE `users`
  ADD COLUMN `user_password` VARCHAR(1024) NOT NULL AFTER `user_nickname`,
  ADD COLUMN `user_salt` VARCHAR(128) NOT NULL AFTER `user_password`,
  ADD INDEX `user_password_user_salt` (`user_password`, `user_salt`);

接下来看我们看看password package, 这个package主要包含salt和hash的生成函数.

package password
import
(
  "encoding/base64"
  "math/rand"
  "crypto/sha256"
  "time"
)
const randomLength = 16
func GenerateSalt(length int) string {
  var salt []byte
  var asciiPad int64
  if length == 0 {
  length = randomLength
  }
  asciiPad = 32
  for i:= 0; i < length; i++ {
  salt = append(salt, byte(rand.Int63n(94) + asciiPad) )
  }
  return string(salt)
}

GenerateSalt()函数将产生随机字符,这些字符的范围是ASCII表的32(32是表示空格)到126个字母

func GenerateHash(salt string, password string) string {
  var hash string
  fullString := salt + password
  sha := sha256.New()
  sha.Write([]byte(fullString))
  hash = base64.URLEncoding.EncodeToString(sha.Sum(nil))
  return hash
}

在这里,我们基于盐值和密码生成hash. 它不仅可以用于创建一个密码,还能用于校验密码。 接下来的ReturnPassword()函数的主要操作就是包含其它函数。让你可以创建密码,并且返回它的hash值

func ReturnPassword(password string) (string, string) {
  rand.Seed(time.Now().UTC().UnixNano())
  salt := GenerateSalt(0)
  hash := GenerateHash(salt,password)
  return salt, hash
}

在我们的客户端,你可能会记得,我们发送的数据都是通过jQuery的AJAX完成。所以我们可以在interface.html中添加如下代码

function userCreate() {
  action = "https://localhost/api/users";
  postData = {};
  postData.email = $('#createEmail').val();
  postData.user = $('#createUsername').val();
  postData.first = $('#createFirst').val();
  postData.last= $('#createLast').val();
  postData.password = $('#createPassword').val();

下一步,我们要修改.ajax的response. 以应对不同的HTTP状态码。我们之前已经设置过对username和e-mail唯一性的错误提醒

var formData = new FormData($('form')[0]);
$.ajax({
  url: action, //Server script to process data
  dataType: 'json',
  type: 'POST',
  statusCode: {
  409: function() {
  $('#api-messages').html('Email address or nickname already
  exists!');
  $('#api-messages').removeClass('alert-success').addClass('alertwarning');
  $('#api-messages').show();
  },
  200: function() {
  $('#api-messages').html('User created successfully!');
  $('#api-messages').removeClass('alert-warning').addClass('alertsuccess');
  $('#api-messages').show();
  }
},

现在,如果我们获得200响应,则在API端,已经创建了一个用户,如果我们获得的是409, 则表示帐号或者邮箱已经存在。

Examining OAuth in Go

在第四章 Designing APIs in GO简单的介绍了OAuth. 它使得应用程序可以使用第三方APP来做用户验证。

现在流行的social media services中,Facebook, Twitter, GitHub都是使用OAuth2.0来验证用记。

接下来,让我们在我们的服务器上,实现类似的OAuth

Endpoint
/api/oauth/authorize
/api/oauth/token
/api/oauth/revoke

在正常情况下,我们需要对token设置一个有效期,我们可以通过memcache 系统或者过期时间库来完成。这使得这些token值,可以自然的无效,而不需要明确的销毁它们。

首先,我们需要先个为客户端证书,添加一个表

CREATE TABLE `api_credentials` (
  `user_id` INT(10) UNSIGNED NOT NULL,
  `consumer_key` VARCHAR(128) NOT NULL,
  `consumer_secret` VARCHAR(128) NOT NULL,
  `callback_url` VARCHAR(256) NOT NULL
  CONSTRAINT `FK__users` FOREIGN KEY (`user_id`) REFERENCES `users`
  (`user_id`) ON UPDATE NO ACTION ON DELETE NO ACTION
  )

一个访问控制令牌,可以是任何的格式,我们为了演示,只采用了最低安全的MD5 hash一个随机产生的字符串。在真实环境中,这是不能满足的。