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请求的响应应该返回资源可以做哪些事情或者请求的端点。
换句话说,在我们之前的例子中,我们通过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一个随机产生的字符串。在真实环境中,这是不能满足的。