构建 API 接口和用户认证的实践指南|青训营

129 阅读6分钟

构建 API 接口

选择适合的框架

Go语言中,有许多适合构建API服务的框架,比如gin、Fiber、beego、buffalo、echo等。

这些框架提供了路由、中间件(如身份验证、授权、日志记录、跨域处理等)、参数解析、错误处理、数据绑定与序列化、缓存和性能优化验证与校验等常见的API服务所需的功能。能够显著提升API服务的开发效率、性能、扩展性、安全性和可维护性,使开发人员能够更好地专注于业务逻辑,而不必过多关注底层细节。

RESTful规范

RESTful设计风格是目前API设计的主流规范之一,能使使API更易于理解和使用。API接口有一些规范需要遵循,例如使用HTTP协议或HTTPS协议、使用URL标识资源、使用HTTP请求方法(GET、POST、PUT、DELETE等)完成操作、使用JSON或XML作为数据交换格式等等。gin框架和beego框架都提供了良好的支持。

HTTP请求方法

//GET
GET /api/users  ( 表示读取用户列表)
GET /api/users/123 (表示获取单个用户的信息)
GET /api/teams/123/members/456 表示获取 id 为123的小组下,id 为456的成员信息

//POST
//POST是一个非幂等的方法,例如多次请求下列语句就会创建多个这样的用户。
POST /api/users  (在服务器上创建一个 name 属性为 'John Snow' 的用户)
{
  "name": "John Snow"
}

//PUT & PATCH
//PUT用于更新资源的全部信息
PUT /api/users/123 ({"id": 123, "name": "Original", "age": 20}更新为{"id": 123, "name": "PUT test"})
{
 "name": "PUT test"
}
//PATCH用于局部更新
PATCH /api/users/123  ({"id": 123, "name": "Original", "age": 20}更新为{"id": 123, "name": "PATCH test", "age": 20})
{
 "name": "PATCH test"
}

//DELETE
//DELETE是一个幂等的方法
DELETE /api/users/123 ( 表示删除用户123

版本控制

明确API版本控制策略。版本控制可以确保旧版本的API仍然可用,同时允许新功能的添加。 具体方法见API 版本控制的几种方式

实践

选择gin框架,使用go get -u github.com/gin-gonic/gin安装Gin相关的库

package main

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

type User struct {
	ID   uint64
	Name string
}

func main() {
	users := []User{{ID: 123, Name: "tom"}, {ID: 456, Name: "peter"}}
	r := gin.Default()
	r.GET("/users", func(c *gin.Context) {
		c.JSON(200, users)
	})
	r.Run(":8080")
}

浏览器打开http://localhost:8080/users ,能得到信息[{"ID":123,"Name":"tom"},{"ID":456,"Name":"peter"}]

用户认证

为了保证API网关的安全性和可扩展性,我们还需要添加认证授权的机制。

选择适合的认证方法

  1. 基本认证(Basic Authentication) : 这是一种简单的认证方式,客户端在请求中发送用户名和密码的组合作为Authorization头的一部分。虽然简单,但不够安全,因为凭证在每个请求中都会以明文形式传输,容易被拦截。建议在非安全性要求较低的内部系统中使用。
  2. 令牌认证(Token Authentication) : 这是一种更安全的认证方式,客户端在登录时获得一个令牌(Token),然后在每次请求中将令牌放在Authorization头中。令牌可以是短暂的,需要定期刷新,从而增加安全性。常见的包括JWT(JSON Web Token)和OAuth 2.0令牌。
  3. OAuth 2.0: OAuth 2.0是一种授权框架,适用于授权第三方应用访问资源所有者的资源。它支持多种授权流程,如授权码流、密码流、客户端凭证流等。OAuth 2.0广泛用于允许用户使用第三方应用登录、访问受保护资源等场景。
  4. API密钥认证(API Key Authentication) : 每个用户或应用都被分配一个唯一的API密钥,用于识别身份。这种方法适用于公开的API,但相对较弱,因为密钥容易泄漏。
  5. 双因素认证(Two-Factor Authentication) : 除了用户名和密码外,还需要提供第二种验证因素,如手机验证码、指纹识别等。这大大增加了认证的安全性。
  6. 多因素认证(Multi-Factor Authentication) : 类似于双因素认证,但可能包含多个验证因素,如生物识别、硬件令牌等。
  7. OpenID Connect: 这是建立在OAuth 2.0之上的身份验证协议,专门用于认证。它提供了标准化的方式来获取用户身份信息。
  8. SAML(Security Assertion Markup Language) : SAML是一种用于基于浏览器的单点登录(SSO)的标准,允许用户在不同应用之间进行单一的身份验证和授权。

存储密码的安全性

存储用户密码时,应采取适当的安全措施,如使用哈希函数将密码转换为不可逆的哈希值,以防止密码泄漏时暴露真实密码。Go语言的crypto包提供了常见的哈希函数,如bcrypt、scrypt等,用于进行密码哈希。

多因素认证

考虑实现多因素认证(MFA)以提高安全性,例如通过手机验证码、硬件令牌等。MFA要求用户提供两个或多个身份验证因素,如密码和手机验证码,以确保只有合法用户能够访问API。

用户会话管理

用户会话管理涉及到如何在服务器和客户端之间跟踪和管理用户的认证状态以及相关信息。虽然API通常是无状态的,但在某些情况下,会话管理可以提供更好的用户体验和安全性。这可以通过HTTP cookie或令牌来实现。

权限限制

在API层面实现权限控制,确保不同用户只能访问其有权访问的资源。定义合适的角色和权限模型。

访问限制

限制API的访问频率,防止滥用。如实现对登录尝试次数的限制和延迟,以防止恶意用户进行暴力破解。

实践

实践一个简单jwt token 使用go get -u github.com/dgrijalva/jwt-go安装jwt库

package main

import (
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/dgrijalva/jwt-go"
)

// 生成token
func CreateJwtToken(userId string) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"id":  userId,
		"exp": time.Now().Add(time.Hour * 24).Unix(), //设置JWT Token过期时间为1天
	})
	return token.SignedString([]byte("my-secret-key"))
}

// 解析token
func ParseJwtToken(tokenString string) (string, error) {
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		return []byte("my-secret-key"), nil
	})
	if err != nil {
		return "", err
	}
	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		userId := claims["id"].(string)
		return userId, nil
	} else {
		return "", errors.New("invalid token")
	}
}

func main() {
	userid := "10001"
	fmt.Println(userid)
	token, err := CreateJwtToken(userid)
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("加密后token字符串", token)
	myuserid, err := ParseJwtToken(token)
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("解密我的userid", myuserid)
}

加密也可用结构体,解密也可用jwt.ParseWithClaims

package main

import (
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/dgrijalva/jwt-go"
)

type User struct {
	ID string `json:"id"`
	jwt.StandardClaims
}

// 生成token
func CreateJwtToken(userId string) (string, error) {
	myclaim := User{
		ID: userId,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, myclaim)
	return token.SignedString([]byte("my-secret-key"))
}

// 解析token
func ParseJwtToken(tokenString string) (string, error) {
	token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte("my-secret-key"), nil
	})
	if err != nil {
		return "", err
	}
	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		userId := claims["id"].(string)
		return userId, nil
	} else {
		return "", errors.New("invalid token")
	}
}

func main() {
	userid := "10001"
	fmt.Println(userid)
	token, err := CreateJwtToken(userid)
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("加密后token字符串", token)
	myuserid, err := ParseJwtToken(token)
	if err != nil {
		log.Println(err.Error())
		return
	}
	fmt.Println("解密我的userid", myuserid)
}