引子
通过 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.Fprintf与fmt.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 设计原则和特征:
- 资源(Resources) :API 暴露的是资源,例如用户、文章、订单等。每个资源都有一个唯一的标识符,通常用URL来表示。
- HTTP 方法:RESTful API 使用不同的HTTP方法来表示不同的操作,如 GET(读取)、POST(创建)、PUT(更新)、DELETE(删除)。
- 状态无关性(Statelessness) :服务器不会保存客户端的状态,每个请求都应该包含足够的信息以便服务器理解和处理请求。
- 统一接口:API 的接口应该是统一的,以便客户端能够理解和预测操作。
- 客户端-服务器分离:客户端和服务器彼此分离,使得它们可以独立演进和扩展。
- 无缓存性(Cacheability) :服务器应该为资源响应提供缓存相关的信息,以便客户端可以缓存响应,提高性能。
- 按需可选性(Layered System) :可以在客户端和服务器之间添加中间层,以提供更高级别的功能,而不影响客户端和服务器之间的通信。
- 可读性的资源标识(Readable Resource Identifiers) :URL应该是有意义的,以便人们能够理解资源的含义。
- 请求和响应的格式:API 使用常见的媒体类型(例如 JSON、XML)来传输请求和响应数据。
- 错误处理: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 通常由三个部分组成,通过点号 . 分隔:
- Header(头部) :包含了令牌的类型(通常是 "JWT")和使用的加密算法,例如 HMAC SHA256 或 RSA。
- Payload(负载) :包含了一些声明(claims),如令牌的主题(subject)、到期时间(expiration time)、发布者(issuer)等。这些信息是关于令牌的附加信息。
- Signature(签名) :使用头部和负载以及一个密钥(secret)来创建,以确保令牌没有被篡改。
比方说:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Header 部分解码后:
{"alg": "HS256", "typ": "JWT"} - Payload 部分解码后:
{"sub": "1234567890", "name": "John Doe", "iat": 1516239022} - Signature 是用算法和密钥生成的,用于验证令牌的完整性。
JWT通常会经过Base64编码,以便在网络中传输。令牌的内容和结构可以根据使用的令牌类型和加密算法而有所不同。
Token有哪些
- 身份验证令牌(Authentication Tokens) :用于验证用户身份,通常在登录时颁发,以便用户在会话期间无需重新输入用户名和密码。例如,JSON Web Token (JWT) 是一种常见的身份验证令牌,它可以在客户端和服务器之间进行安全的信息交换。
- 访问令牌(Access Tokens) :用于授权客户端访问某些资源,例如访问API的权限。当用户登录后,系统会生成一个访问令牌,允许他们访问受保护的资源,同时可以限制其权限。OAuth 2.0 是一种常见的协议,用于生成和管理访问令牌。
- 刷新令牌(Refresh Tokens) :通常与访问令牌一起使用,用于在访问令牌过期后获取新的访问令牌,而无需重新输入用户凭据。刷新令牌通常具有更长的生命周期,用于在不频繁要求用户重新登录的情况下更新访问令牌。
- 令牌授权(Token Authorization) :用于授权对特定资源或操作的访问。例如,OAuth 2.0 的授权码流程会颁发一个授权码,客户端可以使用这个授权码来获取访问令牌。
- 会话令牌(Session Tokens) :在Web应用程序中,会话令牌通常用来跟踪用户会话状态。它可以存储在Cookie中,用于在用户访问多个页面时维护会话状态。
Token简单实现
我们通过项目中的接口来设计一个简单的Token
接口文档如下:
写个简单的表现层代码(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