API接口的构建
API功能
通过 API 接口,可以将服务的功能暴露给其他应用程序或用户。这些应用程序或用户可以通过发送 HTTP 请求到您的 API 接口来获取数据、执行操作或与服务进行交互。
就像插上网线,我们就可以接入网络进行各种网络活动,通过该接口,我们可以获取并使用相对应的服务。
代码实例
用Go语言构建一个api接口需要事先引入
"encoding/json"以处理输入的JSON格式的文件
"github.com/gorilla/mux" 处理HTTP请求和URL匹配
"net/http"构建和处理HTTP请求和响应
"log"用来监视代理服务器端口
(视情况引入strconv包以转换数据类型为字符串)
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
首先我们构建一个表示一个用户对象的数据模型,其中定义了ID,Username,Email三个字段,
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
后续我们会构建相关函数对此进行初始化,添加和删除的操作 需要设置一个存储User的切片作为全局变量
var users []User
考虑到我们之前学习的Restful风格接口, 我们可以在主函数中设置对应功能
-
/users+ GET 方法:getUsers当收到
/users的 GET 请求时,将会调用getUsers函数进行处理。 -
/users+ POST 方法:createUser当收到
/users的 POST 请求时,将会调用createUser函数进行处理。 -
/users/{id}+ GET 方法:getUser当收到
/users/{id}的 GET 请求时,将会调用getUser函数进行处理。其中{id}是一个占位符,表示用户的唯一标识符。 -
/users/{id}+ PUT 方法:updateUser当收到
/users/{id}的 PUT 请求时,将会调用updateUser函数进行处理。其中{id}是一个占位符,表示用户的唯一标识符。 -
/users/{id}+ DELETE 方法:deleteUser当收到
/users/{id}的 DELETE 请求时,将会调用deleteUser函数进行处理。其中{id}是一个占位符,表示用户的唯一标识符。此外我们还需要调用
http.ListenAndServe启动了一个监听在:8080端口上的 HTTP 服务器。
func main() {
r := mux.NewRouter()
// 设置路由处理函数
r.HandleFunc("/users", getUsers).Methods("GET")
r.HandleFunc("/users", createUser).Methods("POST")
r.HandleFunc("/users/{id}", getUser).Methods("GET")
r.HandleFunc("/users/{id}", updateUser).Methods("PUT")
r.HandleFunc("/users/{id}", deleteUser).Methods("DELETE")
// 初始化用户数据
users = append(users, User{ID: 1, Username: "user1", Email: "user1@example.com"})
users = append(users, User{ID: 2, Username: "user2", Email: "user2@example.com"})
fmt.Println("Server started on port 8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
虽然我们还没有对方法对应的函数进行定义,但已经基本定义完框架,通过浏览器访问http://localhost:8080 或 http://127.0.0.1:8080,就会打印出相应的消息"Server started on port 8080",成功启动。
为了通过GET方法获取我们初始化定义的数据,我们需要对getuser函数进行定义
func getUsers(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(users)
}
在这段代码中,w http.ResponseWriter 是一个用于写入响应的接口,而 r *http.Request 则是包含客户端请求信息的结构体。
json.NewEncoder(w) 创建了一个新的 JSON 编码器,它将输出写入到 w 中,即写入到 HTTP 响应中的正文部分。
然后,.Encode(users) 将 users 结构体转换为 JSON 格式,并将其写入到 HTTP 响应中。
通过这个处理函数,当客户端发起 GET 请求到 "/users" 路径时,服务器将返回一个包含 users 数据的 JSON 响应。
通过curl命令发起请求,执行以下命令将会获取全部的用户数据
curl -X GET http://localhost:8080/users
(安装Curl可以参照这篇
如何使用Curl命令可以参照这篇
[www.ruanyifeng.com/blog/2019/0…]
配环境变量已经是日常了)
当返回以下 JSON 数据(即User初始化的数据)时,证明端口可以正常使用。
{
"id": 1,
"username": "user1",
"email": "user1@example.com"
},
{
"id": 2,
"username": "user2",
"email": "user2@example.com"
}
]
之后就可以继续构建其他函数完善功能.再定义完所有函数之后就可以方便的用Curl命令进行操作。
用户认证
为了验证用户身份的过程,确保用户具有访问特定资源或执行特定操作的权限,经常有多种方法比如用户名和密码认证,数字证书认证,JWT(JSON Web Token)等。
JWT简介
JWT 由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
-
头部(Header): 它是一个包含描述令牌类型和签名算法的 JSON 对象。常见的算法包括 HMAC SHA256、RSA 或 ECDSA。头部通常类似于以下形式:
{ "alg": "HS256", "typ": "JWT" }在这个示例中,"alg" 表示签名算法使用的是 HMAC SHA256,"typ" 表示令牌类型为 JWT。
-
载荷(Payload): 它包含了一些声明(Claim)信息,用于存储有关实体(例如用户)和其他元数据的数据。声明可以是预定义的标准声明,也可以是自定义的声明。常见的标准声明包括 "iss"(签发者)、"exp"(过期时间)、"sub"(主题)等。您可以根据需要自定义其他声明字段。载荷通常类似于以下形式:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }在这个示例中,"sub" 表示主题是 "1234567890","name" 表示名称是 "John Doe","iat" 表示令牌的签发时间。
-
签名(Signature): 签名用于验证令牌的真实性和完整性。它由头部、载荷、以及使用密钥进行加密或签名的字符串组成。签名的生成方法取决于所采用的算法。服务器接收到 JWT 后,会再次计算签名并与接收到的签名进行比较,以确保令牌未被篡改。
完整的 JWT 由这三部分使用 Base64 编码后以句点(.)连接而成,形式如下:
Base64UrlEncode(Header).Base64UrlEncode(Payload).Signature
//实际上它一般长这样(真长):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 的签名是使用保密的密钥进行计算的,只有拥有密钥的一方才能验证令牌的真实性。
这种将头部、载荷和签名组合在一起的结构使得 JWT 能够安全地传输和验证,并提供了一种无状态的身份验证机制。
代码实例
下面以JWT为例,实现用户的认证。
要使用JWT,注意需要引入
dgrijalva/jwt-go包来操作JWT,
time包来设置密钥时效。
import (
"fmt"
"log"
"net/http"
"time"
"github.com/dgrijalva/jwt-go" )
我们需要定义一个JWTkey密钥以签名和验证 JWT,演示代码时明文写在代码之中实际并不安全,可以考虑将其设置在环境变量中获取,也可以通过私钥公钥方式,这里只简单演示。
并构建一个声明结构体,声明中包含一个名为 "username" 的字段,用于存储用户名信息。结构体还使用了 jwt.StandardClaims,它是 JWT 包中预定义的标准声明结构体,用于存储一些常见的声明字段,如令牌的过期时间。
var jwtKey = []byte("your-secret-key")
type Claims struct { Username string `json:"username"` jwt.StandardClaims }
之后我们需要定义一个generateJWT函数用于生成JWT。
它设置了一个有效期为5分钟,并使用提供的用户名创建了一个Claims结构体。
然后,使用jwt.NewWithClaims方法创建了一个带有自定义声明的JWT对象,使用jwt.SigningMethodHS256指定了签名算法,并使用密钥jwtKey进行签名。最后,使用token.SignedString方法将JWT转换为字符串,并返回给调用者。
// 生成JWT
func generateJWT(username string) (string, error) {
expirationTime := time.Now().Add(5 * time.Minute) // 设置 token 有效期为 5 分钟
claims := &Claims{ Username: username, StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
在loginHandler中,通过获取用户名并进行简单的验证后,调用了generateJWT函数生成JWT,会将JWT作为响应返回给客户端。 也就是在客户端发送一个POST请求到 /login 路径时,http.HandleFunc("/login", loginHandler) 将调用 loginHandler 函数来处理该请求。
- 发起 POST 请求登录:
curl -X POST -H "Content-Type: application/json" -d '{"username":"user"}' http://localhost:8080/login
会得到如下响应:
{"token":"your-jwt-token"}
loginHandler函数实现代码
func loginHandler(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
if username == "" { //用户名为空时返回错误信息
http.Error(w, "Missing username", http.StatusBadRequest)
return }
if username != "admin" { //用户名不为允许的管理者返回错误信息
http.Error(w, "Invalid username", http.StatusUnauthorized)
return }
token, err := generateJWT(username)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return }
w.Write([]byte(token)) }
此时用户就可以通过用curl命令发送GET请求到受保护的路由/protected路径,通过在请求头中添加Authorization字段,并将JWT放入其中来包含JWT。
curl -X GET -H "Authorization: Bearer {JWT}" http://localhost:8080/protected
为了验证Authorization字段不正确,这个时候就需要定义函数以验证JWT。
可以定义 validateJWT函数,使用jwt.ParseWithClaims方法解析JWT,并验证签名和有效期。如果没有错误,并且JWT有效,则返回true,否则返回false。
// 验证JWT
func validateJWT(tokenString string) (bool, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token)
(interface{}, error) {
return jwtKey, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
return false, nil }
return false, err }
if !token.Valid {
return false, nil
}
return true, nil
}
之后我们可以调用该验证函数让不同情况下服务器返回不同信息,以此验证是否客户端成功访问受保护的路由。思路同验证JWT一致。
- JWT验证:如果请求头中没有提供 JWT,服务器将返回一个
http.StatusUnauthorized状态码(401)和一条错误消息Missing authorization header。 - 如果请求头中提供了 JWT,服务器将调用
validateJWT函数来验证 JWT 的有效性。 - 如果验证过程中出现错误,服务器将返回一个
http.StatusUnauthorized状态码(401)和错误信息。 - 如果 JWT 验证失败,服务器将返回一个
http.StatusUnauthorized状态码(401)和错误信息Invalid authorization token。 - 如果 JWT 验证成功,服务器将返回一个
http.StatusOK状态码(200)和一条消息You've accessed the protected route.表示客户端成功访问受保护的路由。
func protectedHandler(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" { //如果JWT字段为空,返回错误信息
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return }
tokenString := authHeader[len("Bearer "):]
valid, err := validateJWT(tokenString)
if err != nil { //发生异常错误,如解析失败,签名无效等
http.Error(w, err.Error(), http.StatusUnauthorized)
return }
if !valid
{//如果JWT字段不为有效值,返回错误信息
http.Error(w, "Invalid authorization token", http.StatusUnauthorized)
return }
w.Write([]byte("You've accessed the protected route."))
}
之后主函数设计好端口监听即可。
func main() {
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/protected", protectedHandler)
fmt.Println("Server started on port 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}