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

124 阅读12分钟

引子

通过 Gin 和 Gorm 的学习,项目的后端代码已经基本实现。终于到了要和前端对接,提供接口的时候了

接口文档

在我们这次APP开发中,采用的是前后端分离开发,我们后端工程师需要通过查询前端提供的接口文档,参与共同定义接口(分为四部分:方法、uri、请求参数、返回参数),并根据这个接口进行后端代码的实现。

1 构建API接口

1.1 基础API实现

老样子,我们先完成一个简单处理HTTP请求的服务器,然后逐行解释一下。

package main

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

func start(w http.ResponseWriter, r *http.Request){
    fmt.Fprintf(w, "Hello!")
}

func router() {
    http.HandleFunc("/", start)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func main() {
    router()
}

3 ~ 7 - 导入所需的包。fmt 包用于格式化输出,log 包用于记录错误和消息,net/http 包用于处理HTTP请求和响应

9 ~ 11 - 这是一个名为 start 的函数,它是一个处理HTTP请求的处理程序。它接受两个参数:w 是一个 http.ResponseWriter,用于向客户端发送响应,r 是一个 http.Request,包含了客户端发送的请求信息。

在这个函数中,它使用 fmt.Fprintf 向响应写入 "Hello!" 消息,并将可能的错误存储在 err 中。如果写入过程中出现错误,函数会提前返回。

小插一嘴 ( fmt.Fprintf )

fmt.Fprintffmt.Printf 类似,但不是将输出写入标准输出,而是将输出写入指定的 io.Writer 接口。

它的第一个参数是一个实现了 io.Writer 接口的对象,通常是一个文件、网络连接、缓冲区等。

后续的参数与 fmt.Printf 相同,包括格式化字符串和要插入的数据。

例子:fmt.Fprintf(file, "Hello, %s!\n", "World")

13 ~ 16 - 这是一个名为 router 的函数,它设置了路由和启动HTTP服务器。

http.HandleFunc("/", start) 为根路径 "/" 注册了 start 函数作为处理程序。这意味着当浏览器或其他客户端访问根路径时,将调用此函数来处理请求。

http.ListenAndServe(":8080", nil) 开启一个HTTP服务器,监听在端口8080上。第一个参数是要监听的地址和端口,第二个参数是处理程序,由于我们在前面使用 http.HandleFunc 注册了处理程序,这里传入 nil 即可。

点击运行,打开http://localhost:8080/,应该看到“Hello!” ,现在已经成功创建了一个基础的API。

1.2 RESTful API

RESTful API 是一种设计用于在网络上进行通信的API架构风格。它基于 "Representational State Transfer"(表现层状态转化)的原则,旨在提供一种简单、统一的方法来构建和管理网络资源。RESTful API 的设计使得客户端可以通过HTTP请求对服务器资源执行 CRUD 操作(创建、读取、更新、删除),并且以一种符合REST原则的方式进行交互。

以下是一些关键的 RESTful API 设计原则和特征:

  1. 资源(Resources) :API 暴露的是资源,例如用户、文章、订单等。每个资源都有一个唯一的标识符,通常用URL来表示。
  2. HTTP 方法:RESTful API 使用不同的HTTP方法来表示不同的操作,如 GET(读取)、POST(创建)、PUT(更新)、DELETE(删除)。
  3. 状态无关性(Statelessness) :服务器不会保存客户端的状态,每个请求都应该包含足够的信息以便服务器理解和处理请求。
  4. 统一接口:API 的接口应该是统一的,以便客户端能够理解和预测操作。
  5. 客户端-服务器分离:客户端和服务器彼此分离,使得它们可以独立演进和扩展。
  6. 无缓存性(Cacheability) :服务器应该为资源响应提供缓存相关的信息,以便客户端可以缓存响应,提高性能。
  7. 按需可选性(Layered System) :可以在客户端和服务器之间添加中间层,以提供更高级别的功能,而不影响客户端和服务器之间的通信。
  8. 可读性的资源标识(Readable Resource Identifiers) :URL应该是有意义的,以便人们能够理解资源的含义。
  9. 请求和响应的格式:API 使用常见的媒体类型(例如 JSON、XML)来传输请求和响应数据。
  10. 错误处理:API 应该提供一致的错误处理机制,使客户端能够理解发生了什么问题。

RESTful API 在Web开发中得到广泛应用,它提供了一种标准化的方式来设计和构建API,使不同的系统和应用程序可以相互交互。

1.3 使用Gin优化接口

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

type User struct {
	ID       int    `json:"id"`
	Username string `json:"username"`
	Email    string `json:"email"`
}

var users []User

func main() {
	router := gin.Default()

	// 创建新用户
	router.POST("/users", func(c *gin.Context) {
		var newUser User
		if err := c.ShouldBindJSON(&newUser); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// 分配唯一ID
		newUser.ID = len(users) + 1
		users = append(users, newUser)

		c.JSON(http.StatusCreated, newUser)
	})

	// 查询所有用户
	router.GET("/users", func(c *gin.Context) {
		c.JSON(http.StatusOK, users)
	})

	// 通过ID寻找特定用户
	router.GET("/users/:id", func(c *gin.Context) {
		id := c.Param("id")
		// Convert id to int
		// Handle errors here...

		if user, found := getUserByID(id); found {
			c.JSON(http.StatusOK, user)
		} else {
			c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
		}
	})

	router.Run(":8080")
}

func getUserByID(id string) (User, bool) {
	// Convert id to int
	// Handle errors here...

	for _, user := range users {
		if user.ID == id {
			return user, true
		}
	}
	return User{}, false
}

详细的说明在前一篇提及过了,感兴趣可以去看看

初识Gin,项目没你可不行 | 青训营 - 掘金 (juejin.cn)

2 用户认证

token是什么

Token是用来确保用户安全登录的一个短字符串,它可以用来表示用户、应用程序或设备的身份,并被用作访问资源或执行特定操作的凭证。Token 在各种应用中被广泛使用,包括身份验证、API访问、单点登录、令牌授权等。

Token通过设计不同的令牌来实现安全和授权。

JWT(JSON Web Token)是一种用于安全地在不同实体之间传递信息的开放标准。它通常用于身份验证和授权,以及在应用程序之间安全地传输声明(claims)。JWT是一种紧凑且自包含的格式,以JSON格式表示,可以通过网络传输,并且在受信任的实体之间进行验证。

一个 JWT 通常由三个部分组成,通过点号 . 分隔:

  1. Header(头部) :包含了令牌的类型(通常是 "JWT")和使用的加密算法,例如 HMAC SHA256 或 RSA。
  2. Payload(负载) :包含了一些声明(claims),如令牌的主题(subject)、到期时间(expiration time)、发布者(issuer)等。这些信息是关于令牌的附加信息。
  3. Signature(签名) :使用头部和负载以及一个密钥(secret)来创建,以确保令牌没有被篡改。

比方说:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • Header 部分解码后:{"alg": "HS256", "typ": "JWT"}
  • Payload 部分解码后:{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
  • Signature 是用算法和密钥生成的,用于验证令牌的完整性。

JWT通常会经过Base64编码,以便在网络中传输。令牌的内容和结构可以根据使用的令牌类型和加密算法而有所不同。

Token有哪些

  1. 身份验证令牌(Authentication Tokens) :用于验证用户身份,通常在登录时颁发,以便用户在会话期间无需重新输入用户名和密码。例如,JSON Web Token (JWT) 是一种常见的身份验证令牌,它可以在客户端和服务器之间进行安全的信息交换。
  2. 访问令牌(Access Tokens) :用于授权客户端访问某些资源,例如访问API的权限。当用户登录后,系统会生成一个访问令牌,允许他们访问受保护的资源,同时可以限制其权限。OAuth 2.0 是一种常见的协议,用于生成和管理访问令牌。
  3. 刷新令牌(Refresh Tokens) :通常与访问令牌一起使用,用于在访问令牌过期后获取新的访问令牌,而无需重新输入用户凭据。刷新令牌通常具有更长的生命周期,用于在不频繁要求用户重新登录的情况下更新访问令牌。
  4. 令牌授权(Token Authorization) :用于授权对特定资源或操作的访问。例如,OAuth 2.0 的授权码流程会颁发一个授权码,客户端可以使用这个授权码来获取访问令牌。
  5. 会话令牌(Session Tokens) :在Web应用程序中,会话令牌通常用来跟踪用户会话状态。它可以存储在Cookie中,用于在用户访问多个页面时维护会话状态。

Token简单实现

我们通过项目中的接口来设计一个简单的Token

接口文档如下:

image.png

写个简单的表现层代码(controller)

package main  
  
import (  
  "crypto/sha256"  
  "github.com/dgrijalva/jwt-go"  
  "github.com/gin-gonic/gin"  
  "net/http"  
  "strconv"  
  "time"  
)  
  
// 定义 Request 和 Response 结构体  
type Request struct {  
  Token string `json:"token"`  
  ToUserID int64 `json:"to_user_id"`  
  ActionType int32 `json:"action_type"`  
  Content string `json:"content"`  
}  
  
type Response struct {  
  StatusCode int32 `json:"status_code"`  
  StatusMsg string `json:"status_msg,omitempty"` //omitempty标签控制是否在值为空或零值的情况下省略该字段  
}  
  
// 定义 CustomClaims 结构体  
type CustomClaims struct {  
  UserID int64 `json:"user_id"`  
  jwt.StandardClaims  
}  
  
// JWT 配置 —— Token过期时间
var jwtExpiration = 24 * time.Hour
  
// 使用SHA-256密码学哈希函数生成密钥  
func generateKey(userID int64) []byte {  
  hash := sha256.Sum256([]byte(strconv.FormatInt(userID, 10)))  
  return hash[:]  
}  
  
  
func main() {  
  r := gin.Default()  
  
  r.POST("/douyin/message/action/", sendMessageHandler)  
  
  err := r.Run(":8080")  
  if err != nil {  
    return  
  }  
}  
  
func validateToken(tokenString string, userID int64) bool {  
  key := generateKey(userID)  
  token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {  
    return key, nil  
  })  
  if err != nil {  
    return false  
  }  
  
  if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {  
    return claims.UserID == userID  
  }  
  
  return false  
}  
  
func sendMessageHandler(c *gin.Context) {  
  token := c.Query("token")  
  //userID由其他方式获取,一下仅举例  
  UserID := c.Query("user_id")  
  userID, _ := strconv.Atoi(UserID)  
  
  if !validateToken(token, int64(userID)) {  
    c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token or user_id"})  
    return  
  }  
  
  var req Request  
  if err := c.ShouldBindJSON(&req); err != nil {  
    c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"})  
    return  
  }  
  
  
  resp := Response{  
    StatusCode: 0,  
    StatusMsg: "Message sent successfully",  
  }  
  c.JSON(http.StatusOK, resp)  
}

25~29 - 定义CustomClaims 结构体

    UserID 字段用于存储用户的 ID,而 StandardClaims 是一个内置的字段,包含了 JWT 的标准声明,包括过期时间等。这些信息将在生成和验证 JWT token 时被使用。

32 - JWT 的过期时间 jwtExpiration,这里设置为 24 小时

34~38 - 使用SHA-256密码学哈希函数生成密钥

在代码中,hash[:] 表示将一个数组(或切片)的所有元素提取出来并转换为一个新的切片。 在这里,hash 是一个数组(长度为 32,因为 SHA-256 生成的哈希值是 32 字节长),hash[:] 就是将整个数组的内容提取出来并转换成一个新的切片。这么做的目的是为了将数组转换为切片,以便在 generateKey 函数中返回一个切片作为密钥。

实际上,hash[:] 等同于 hash[0:len(hash)],但是因为切片的长度是可变的,所以可以简写为 hash[:]。这样可以方便地将数组转换为切片,而无需显式指定长度。

41~50 - 这是 main 函数

    它初始化一个默认的 Gin 路由引擎,将 POST 请求路径 /douyin/message/action/ 关联到 sendMessageHandler 处理器函数,然后启动 HTTP 服务器监听在 8080 端口。

52~66 - validateToken函数

func validateToken(tokenString string, userID int64) bool {
	key := generateKey(userID)
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return key, nil
	})
	if err != nil {
		return false
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims.UserID == userID
	}

	return false
}

    验证 JWT token。它使用 generateKey 生成的密钥,将传入的 token 字符串解析为 JWT,并验证其合法性和有效性。如果解析和验证成功,它会检查 token 中的声明,确认用户 ID 是否匹配,然后返回验证结果。

  • jwt.ParseWithClaims 函数来解析和验证传入的 tokenString。第一个参数是要解析的 token 字符串,第二个参数是一个空的 CustomClaims 对象的指针(用于填充解析后的声明),第三个参数是一个回调函数,它返回用于解析的密钥和可能的错误。

  • if err != nil { return false }: 如果解析 tokenString 时发生了错误,说明 token 不合法,此时函数会返回 false 表示验证失败。

  • if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { return claims.UserID == userID }: 在这个条件语句中,首先检查解析出的声明是否是 *CustomClaims 类型的,并且验证 token 是否有效。如果两个条件都满足,那么会进一步检查解析出的声明中的 UserID 是否等于传入的 userID。如果都满足,则返回 true 表示验证通过。

  • return false: 默认情况下,如果以上任何条件不满足,函数将返回 false,表示验证失败。

68~88 - sendMessageHandler 处理器函数(表现层代码)

func sendMessageHandler(c *gin.Context) {  
  token := c.Query("token")  
  //userID由其他方式获取,一下仅举例  
  UserID := c.Query("user_id")  
  userID, _ := strconv.Atoi(UserID)  
  
  if !validateToken(token, int64(userID)) {  
    c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token or user_id"})  
    return  
  }  
  
  var req Request  
  if err := c.ShouldBindJSON(&req); err != nil {  
    c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"})  
    return  
  }  
  
  
  resp := Response{  
    StatusCode: 0,  
    StatusMsg: "Message sent successfully",  
  }  
  c.JSON(http.StatusOK, resp)  
}

    它处理 POST 请求 /douyin/message/action/。它从请求中获取 token,并且获取 user_id。然后,它使用 validateToken 函数验证 token 的有效性。如果验证失败,返回未授权错误。如果验证通过,它解析请求 JSON 数据并执行消息发送操作,然后返回成功响应。

  • if !validateToken(token, userID) { ... }: 这个条件语句调用了之前定义的 validateToken 函数,用于验证 JWT token 和 user ID 是否有效。如果验证不通过,它会返回一个未授权的 JSON 响应,并提前终止函数。
  • var req Request: 这一行创建了一个 Request 对象,用于解析请求 JSON 数据。
  • if err := c.ShouldBindJSON(&req); err != nil { ... }: 这个条件语句使用 ShouldBindJSON 方法尝试将请求中的 JSON 数据解析到 req 对象中。如果解析失败,它会返回一个请求错误的 JSON 响应,并提前终止函数。
  • resp := Response{ ... }: 这一行创建了一个 Response 对象,用于构建成功的响应。
  • c.JSON(http.StatusOK, resp): 这一行使用 c.JSON 方法将构建好的成功响应 resp 以 JSON 格式返回给客户端,状态码为 http.StatusOK

就写到这里了,好像可能还有错误,那我就在以后学习中回过头慢慢更正吧o(╥﹏╥)o