如何将我的服务开放给用户:构建 API 接口和用户认证的实践 | 青训营

72 阅读9分钟

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风格接口, 我们可以在主函数中设置对应功能

  1. /users + GET 方法:getUsers

    当收到 /users 的 GET 请求时,将会调用 getUsers 函数进行处理。

  2. /users + POST 方法:createUser

    当收到 /users 的 POST 请求时,将会调用 createUser 函数进行处理。

  3. /users/{id} + GET 方法:getUser

    当收到 /users/{id} 的 GET 请求时,将会调用 getUser 函数进行处理。其中 {id} 是一个占位符,表示用户的唯一标识符。

  4. /users/{id} + PUT 方法:updateUser

    当收到 /users/{id} 的 PUT 请求时,将会调用 updateUser 函数进行处理。其中 {id} 是一个占位符,表示用户的唯一标识符。

  5. /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:8080http://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可以参照这篇

[blog.csdn.net/weixin_4016…]

如何使用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)。

  1. 头部(Header): 它是一个包含描述令牌类型和签名算法的 JSON 对象。常见的算法包括 HMAC SHA256、RSA 或 ECDSA。头部通常类似于以下形式:

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

    在这个示例中,"alg" 表示签名算法使用的是 HMAC SHA256,"typ" 表示令牌类型为 JWT。

  2. 载荷(Payload): 它包含了一些声明(Claim)信息,用于存储有关实体(例如用户)和其他元数据的数据。声明可以是预定义的标准声明,也可以是自定义的声明。常见的标准声明包括 "iss"(签发者)、"exp"(过期时间)、"sub"(主题)等。您可以根据需要自定义其他声明字段。载荷通常类似于以下形式:

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    

    在这个示例中,"sub" 表示主题是 "1234567890","name" 表示名称是 "John Doe","iat" 表示令牌的签发时间。

  3. 签名(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 函数来处理该请求。

  1. 发起 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一致。

  1. JWT验证:如果请求头中没有提供 JWT,服务器将返回一个 http.StatusUnauthorized 状态码(401)和一条错误消息 Missing authorization header
  2. 如果请求头中提供了 JWT,服务器将调用 validateJWT 函数来验证 JWT 的有效性。
  3. 如果验证过程中出现错误,服务器将返回一个 http.StatusUnauthorized 状态码(401)和错误信息。
  4. 如果 JWT 验证失败,服务器将返回一个 http.StatusUnauthorized 状态码(401)和错误信息 Invalid authorization token
  5. 如果 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))
}