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

123 阅读8分钟

构建 API 接口

API 接口

API(Application Programming Interface,应用程序编程接口)是一组定义软件组件之间交互的规则。API定义了不同应用程序之间进行调用(call)和请求(request)的种类、调用方法、参数类型以及返回类型。API可以帮助开发人员更好地组织和管理软件的代码,提高软件的可维护性和可扩展性。

API 接口的设计原则

良好的API设计应该遵循以下原则:

  • 学习简单,使用简单,对接的开发者可以快速上手,即使在缺少文档的情况下也能够使用。
  • 不易错误使用,对接的开发者不容易犯错,即使犯错也能够快速发现。
  • 可读性好,对接的开发者可以快速理解API的设计意图。
  • 易于扩展。
  • 足够满足需求。一个API只实现一个功能。

API 接口架构风格

API接口的架构风格包括REST、RPC、GraphQL等。

RPC

RPC(Remote Procedure Call,远程过程调用)是一种通信协议,它允许应用程序请求服务而不需要了解底层网络技术。RPC允许开发人员使用类似于本地方法调用的方式来调用远程服务,使得开发人员可以更加方便地开发分布式应用程序。

RPC的调用较为简单直接,主要包括获取数据和执行其他调用,类似于HTTP的GET和POST请求。目前,专门的RPC框架有gRPC、Thrift、Dubbo等。使用RPC框架可以高性能地调用远程服务,因而广泛应用于分布式系统、微服务等场景。

REST

REST(Representational State Transfer,表现层状态转移)是一种架构风格,它是一种设计风格而不是标准。REST的核心思想是将资源抽象为一种状态,通过URL来表示资源的状态,通过HTTP的GET、POST、PUT、DELETE等方法来操作资源的状态。REST的核心思想是资源,而不是方法。

使用REST风格可以更好地解耦客户端和服务端,提高系统的可扩展性和可维护性。目前,REST风格的API接口已经成为互联网应用程序的主流设计风格。

RPC vs REST

几乎所有B/S架构和大多数C/S架构都选择使用HTTP作为通信协议,通常服务端会构建一套WEB API接口,供客户端调用。实际上,目前不少WEB API接口设计是偏向于RPC风格的,而不是REST风格的,激进的说法是这些接口实际上是「HTTP RPC」。

例如,对于用户注册、登录、用户信息、删除的WEB API接口,不同的设计风格类似这样:

接口RPC风格REST风格
注册POST /user/signupPOST /user
登录POST /user/loginPOST /user/session
登出POST /user/logoutDELETE /user/session
获取用户信息GET /user/infoGET /user/:id
更新用户信息POST /user/updatePUT /user/:id
删除POST /user/deleteDELETE /user/:id

从上表可以看出,RPC风格的WEB API接口设计更加直观,但是不符合REST风格的设计原则。REST风格侧重于资源,而不是操作。实际上,设计一套完全符合REST风格的WEB API接口是比较困难的,需要开发者综合考虑对资源的建模。

RESTful API 的设计与实践

设计原则

RESTful API的设计原则包括:

  • 使用名词而不是动词来表示资源。
  • 客户端-服务端结构,将二者的关注点分离。分离客户端注重用户界面的逻辑和服务端注重数据操作的逻辑,提高客户端可移植性和服务端可扩展性。
  • 无状态,客户端的每次请求都包含所有的信息,服务端不保存客户端的状态。
  • 缓存,GET请求可以被缓存,提高性能。
  • 统一接口,这是RESTful API的根本出发点。
    • 资源标识,使用URL来标识资源。
    • 资源操作,使用HTTP的GET、POST、PUT、DELETE等方法来操作资源。
    • 自描述消息,资源包含足够的信息来描述如何处理自身。
    • HATEOAS, 超媒体作为应用状态的引擎,客户端通过资源的状态来决定下一步的操作。例如,客户端通过GET /user/:id获取用户信息,然后通过用户信息中的URL来获取用户的订单信息。这样的好处是客户端不需要硬编码所有服务的URL,而是通过资源的状态来决定下一步的操作。

严格遵守REST设计原则是比较困难的,很少有API接口集群能够完全应用HATEOAS原则,它意味着API架构设计的完全自动化和智能化。一些接口更偏好于被设计成RPC风格的,例如,更多的开发者选择POST user/logout而不是DELETE user/session进行用户登出操作。

请求方法

HTTP的请求方法包括GET、POST、PUT、DELETE、HEAD、OPTIONS、PATCH等,其中GET、POST、PUT、DELETE是最常用的请求方法。相比于「HTTP RPC」式的接口只使用GET和POST方法,RESTful API的设计充分利用HTTP的请求方法的语义。

例如,在RESTful API的设计中,商品资源的相关操作可以使用如下请求方法:

资源GETPOSTPUTDELETE
/goods获取商品列表在商品列表中追加替换商品列表删除商品列表
/goods/:id获取商品添加商品信息替换商品信息删除商品

GET、PUT、DELETE方法的语义是幂等的,即多次调用不会产生副作用。例如,多次调用DELETE /goods/:id方法不会产生副作用,因为商品已经被删除了。而POST方法的语义是非幂等的,多次调用会产生副作用。例如,多次调用POST /goods方法会在商品列表中追加多个商品。

用户认证

用户认证和授权

用户认证和授权是互联网应用程序的基本功能,它们是保护用户隐私和数据安全的重要手段。用户认证和授权是验证用户身份、验证用户是否有权限访问某个资源的过程。

常用的用户认证方式

常用的用户认证方式包括:

Session / Cookie

Session / Cookie是最常用的用户认证方式。用户登录后,服务端会生成一个Session ID,然后将Session ID存储在Cookie中,发送给客户端。客户端在后续的请求中会携带Cookie,服务端通过解析Cookie中的Session ID来验证用户身份。

JWT / Token

Token 是一种用户认证方式,它是服务端生成的一串字符串,包含用户的身份信息、权限信息等。Token通常存储在客户端,客户端在后续的请求中会携带Token,服务端通过解析Token来验证用户身份和权限。

JWT(JSON Web Token)是一种开放标准,它定义了一种紧凑的、自包含的方式来表示各方之间的信息。JWT通常被用来在用户和服务端之间传递安全的信息。JWT通常包含用户的身份信息、权限信息等,服务端可以通过解析JWT来验证用户身份和权限。

OAuth2

OAuth2是一种用户认证和授权的开放标准,它定义了一种授权方式,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2通常用于用户授权,而不是用户认证。

使用JWT进行用户认证

go库jwt-go提供了JWT的实现,可以使用它来生成和解析JWT。在接下来的示例中,我们将使用jwt-go来实现用户认证。

安装jwt-go库:

go get github.com/dgrijalva/jwt-go

生成JWT

需要注意的是,JWT中包含的信息是公开的,解密JWT时不需要密钥。因此,JWT中不应该包含敏感信息,例如用户的密码等。

在以下示例中,JWT中包含用户的ID、用户名、过期时间等信息。JWT的过期时间是可选的,如果不设置过期时间,那么JWT将永远有效。

func GenerateToken(user *models.User) (string, error) {
	currentTime := time.Now().Unix()
	expireTime := currentTime + config.ExpireTime
	claims := jwt.StandardClaims{
		Audience:  user.Name,
		ExpiresAt: expireTime,
		Id:        strconv.FormatInt(user.Id, 10),
		IssuedAt:  currentTime,
		Issuer:    "service",
		NotBefore: currentTime,
		Subject:   "login",
	}

	jwtSecret := []byte(config.JWTSecret)
	tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	token, err := tokenClaims.SignedString(jwtSecret)
	if err != nil {
		return "", err
	}
	return token, nil
}

解析JWT

func ParseToken(token string) (*jwt.StandardClaims, error) {
	jwtSecret := []byte(config.JWTSecret)
	tokenClaims, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
		return jwtSecret, nil
	})
	if err != nil {
		// if err is start with "token is expired by", return ErrTokenExpired
		if strings.HasPrefix(err.Error(), "token is expired by") {
			return nil, ErrTokenExpired{}
		}
		return nil, ErrInvalidToken{}
	}
	claims, ok := tokenClaims.Claims.(*jwt.StandardClaims)
	if !ok || !tokenClaims.Valid {
		return nil, ErrInvalidToken{}
	}
	return claims, nil
}

这个函数会解析JWT,如果JWT过期或者无效,会返回相应的错误。解析后,我们得到了JWT中包含的payload。用户id可以从claims.id中获取。可以根据得到的用户信息来进行后续的操作。

总结

本文主要介绍了构建 API 接口和用户认证的相关知识。在构建 API 接口方面,介绍了 API 接口的设计原则和架构风格,重点介绍了 RESTful API 的设计与实践。在用户认证方面,本文介绍了用户认证和授权的概念,以及常用的用户认证方式,如 Session/Cookie、JWT/Token 和 OAuth2。其中,使用 JWT 进行用户认证的部分详细介绍了如何生成和解析 JWT。在学习过程中,我理解了 API 接口和用户认证的重要性和实践原则。在今后的开发实践中,我们应该注重 API 接口和用户认证的设计和实现,提高系统的安全性和可靠性。