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

33 阅读4分钟

在当今的互联网时代,我们的服务不仅要面向浏览器,还要面向各种客户端,如移动设备、桌面应用、物联网设备等。为了让不同的客户端能够方便地访问我们的服务,我们需要提供一种统一的接口,让客户端能够通过网络请求获取或修改我们的数据。这种接口就是API,即应用程序编程接口。

Restful API

API有很多种风格,如SOAP、RPC、GraphQL等,但最流行的一种是Restful API。Restful API是一种基于HTTP协议的API风格,它遵循REST原则,即表述性状态转移。REST原则的核心思想是,每个资源都有一个唯一的标识符(URI),客户端可以通过HTTP方法(GET、POST、PUT、DELETE等)对资源进行增删改查操作,服务器返回资源的表述(Representation),通常是JSON或XML格式。这样,客户端和服务器之间就形成了一种统一、无状态、可缓存的接口。

API接口设计准则

为了让我们的API更加易用、可维护和可扩展,我们需要遵循一些设计准则,例如:

  • 使用名词而不是动词来表示资源,如/users而不是/getUsers
  • 使用复数而不是单数来表示资源集合,如/users而不是/user
  • 使用HTTP方法来表示对资源的操作,如GET /users表示获取所有用户,POST /users表示创建一个新用户,PUT /users/:id表示更新一个指定用户,DELETE /users/:id表示删除一个指定用户
  • 使用HTTP状态码来表示请求的结果,如200表示成功,400表示客户端错误,500表示服务器错误
  • 使用查询参数来过滤、排序、分页等资源,如GET /users?name=alice&age=20&sort=asc&page=1&size=10
  • 使用路径参数来表示资源之间的关系,如GET /users/:id/posts表示获取一个指定用户的所有文章
  • 使用请求头来传递元数据,如Content-Type表示请求体的格式,Authorization表示用户认证信息
  • 使用响应头来传递元数据,如Content-Type表示响应体的格式,Location表示创建或更新资源后的URI
  • 使用响应体来传递资源的表述,如JSON或XML格式

用户认证简介

当我们提供API给客户端时,我们可能需要限制某些资源或操作只能由特定的用户访问。例如,我们可能只允许用户查看或修改自己的个人信息,或者只允许管理员执行某些敏感操作。为了实现这种功能,我们需要对用户进行认证(Authentication)和授权(Authorization)。认证是指验证用户的身份,授权是指验证用户是否有权限访问某个资源或执行某个操作。

用户认证有很多种策略,如基本认证(Basic Authentication)、摘要认证(Digest Authentication)、令牌认证(Token Authentication)、OAuth、OpenID Connect等。这里我们只介绍一种比较简单而又常用的策略,即令牌认证。

令牌认证的原理是,用户在登录时,服务器验证用户的用户名和密码,如果正确,就生成一个令牌(Token),并返回给客户端。客户端在后续的请求中,将令牌放在请求头的Authorization字段中,服务器根据令牌来验证用户的身份和权限。令牌通常有一定的有效期,过期后需要重新登录或刷新。令牌可以是任意的字符串,但通常使用一种叫做JWT(JSON Web Token)的格式,它包含了用户的信息和签名,可以防止被篡改。

代码示例

然后,我们需要定义一个User结构体来表示用户:

type User struct {  
    ID int `json:"id"`  
    Username string `json:"username"`  
    Password string `json:"password"`  
}  

接着,我们需要定义一个全局的用户列表来模拟数据库:

var users = []User{  
    {ID: 1, Username: "alice", Password: "123456"},  
    {ID: 2, Username: "bob", Password: "654321"},  
}  

然后,我们需要定义一个函数来生成JWT令牌:

import (  
    "github.com/dgrijalva/jwt-go"  
)  
  
    var secretKey = []byte("secret")  
  
func generateToken(user User) (string, error) {  
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{  
    "id": user.ID,  
    "username": user.Username,  
})  
    return token.SignedString(secretKey)  
}  

接着,我们需要定义一个函数来解析JWT令牌:

func parseToken(tokenString string) (*User, error) {  
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {  
    return secretKey, nil  
    })  
if err != nil {  
    return nil, err  
}  
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {  
    id := int(claims["id"].(float64))  
    username := claims["username"].(string)  
    return &User{ID: id, Username: username}, nil  
}  
    return nil, err  
}  

然后,我们需要定义一个中间件来验证用户的令牌:

func authMiddleware(c *gin.Context) {  
    authHeader := c.GetHeader("Authorization")  
    if authHeader == "" {  
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "no authorization header"})  
        return  
    }  
    user, err := parseToken(authHeader)  
    if err != nil {  
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()})  
        return  
    }  
    c.Set("user", user)  
    c.Next()  
}  

处理用户登录:

func loginHandler(c *gin.Context) {  
    var user User  
    if err := c.ShouldBindJSON(&user); err != nil {  
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})  
        return  
    }  
    for _, u := range users {  
        if u.Username == user.Username && u.Password == user.Password {  
            token, err := generateToken(u)  
            if err != nil {  
                c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})  
                return  
            }  
            c.JSON(http.StatusOK, gin.H{"token": token})  
            return  
        }  
    }  
    c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})  
}