Bootstrap

GoWeb 项目实战:使用 Iris 框架构建一个安全可靠的 API 服务(附 JWT 认证实现)

1、前言

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.

2、什么是 JWT

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

JWT 规定了数据传输的结构,一串完整的 JWT 由三段落组成,每个段落用英文句号连接 . 连接,他们分别是标头(Header)、有效负荷(Payload)和签名(Signature)三个部分,所以常规的 JWT 内容格式是这样的:aaa.bbb.ccc,即:Header.Payload.Signature

在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web Token。在某些情况下,这可以是无状态授权机制。服务器的受保护路由将在Authorization Header中检查有效的JWT,如果存在,则允许用户访问 受保护的资源。如果JWT包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。需要注意的是,使用签名Token,Token中包含的所有信息都会向用户或其他方公开,即使他们无法更改。这意味着您不应该在Token中放置秘密信息。

3、什么是 UUID

UUID是国际标准化组织(ISO)提出的一个概念。UUID是一个128比特的数值,这个数值可以通过一定的算法计算出来。为了提高效率,常用的UUID可缩短至16位。UUID用来识别属性类型,在所有空间和时间上被视为唯一的标识。一般来说,可以保证这个值是真正唯一的任何地方产生的任意一个UUID都不会有相同的值。使用UUID的一个好处是可以为新的服务创建新的标识符。这样一来,客户端在查找一个服务时,只需要在它的服务查找请求中指出与某类服务(或某个特定服务)有关的UUID,如果服务的提供者能将可用的服务与这个UUID相匹配,就返回一个响应。

UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。UUID可以被任何人独立创建,并按需发布。UUID没有集中管理机构,因为它们是不会被复制的独特标识符。属性协议允许设备使用UUID识别属性类型,从而不需要用读/写请求来识别它们的本地句柄。

4、编写代码

此项目根据 《从零开始实现一个 GoWeb MVC 框架:让你更深入地理解 MVC 架构设计》源代码进阶,如果只想了解 JWT 认证可以忽略

4.1、安装软件包

# 1、下载并安装 jwt
go get -u github.com/golang-jwt/jwt/v4

# 2、下载并安装 uuid
go get -u github.com/gofrs/uuid

4.2、编写代码

工欲善其事,必先利其器,我们从工具包开始

4.2.1、日期时间工具类

在 utils/tool 目录新建文件 datatime_utils.go,完整代码如下:

package tool

import "time"

/**
 * <h1>日期时间工具类</h1>
 * Created by woniu
 * second,秒
 * millisecond,毫秒
 * microsecond,微妙
 * nanosecond,纳秒
 */

/**
 * <h2>获取当前时间</h2>
 */
func DataTimeNow() time.Time {
	return time.Now()
}

/**
 * <h2>获取指定时间 - 秒</h2>
 */
func DataTimeUnix(t time.Time) int64 {
	return t.Unix()
}

/**
 * <h2>获取指定时间 - 秒</h2>
 */
func DataTimeNowUnix() int64 {
	return DataTimeUnix(DataTimeNow())
}

/**
 * <h2>获取指定时间 - 毫秒</h2>
 */
func DataTimeUnixMilli(t time.Time) int64 {
	return t.UnixMilli()
}

/**
 * <h2>获取当前时间 - 毫秒</h2>
 */
func DataTimeNowUnixMilli() int64 {
	return DataTimeUnixMilli(DataTimeNow())
}

/**
 * <h2>指定日期增加几个小时</h2>
 */
func DataTimeAddHour(t time.Time, h time.Duration) time.Time {
	return t.Add(h * time.Hour)
}

/**
 * <h2>格式化时间,yyyy-mm-dd hh:mm:ss</h2>
 */
func DataTimeFormat(t time.Time) string {
	return t.Format("2006-01-02 15:04:05")
}

/**
 * <h2>格式化时间,yyyy-mm-dd</h2>
 */
func DataFormat(t time.Time) string {
	return t.Format("2006-01-02")
}

/**
 * <h2>格式化时间,hh:mm:ss</h2>
 */
func TimeFormat(t time.Time) string {
	return t.Format("15:04:05")
}

4.2.2、字符串工具类

在 utils/tool 目录新建文件 string_utils.go,完整代码如下:

package tool

import (
	"fmt"
	"strconv"
	"time"

	"github.com/gofrs/uuid"
)

/**
 * <h1>字符串工具类</h1>
 * Created by woniu
 */

/***
 * <h2>空校验</h2>
 */
func IsEmpty(str string) bool {
	return len(str) == 0
}

/***
 * <h2>非空校验</h2>
 */
func IsNotEmpty(str string) bool {
	return !IsEmpty(str)
}

/***
 * <h2>字符串在数组内</h2>
 */
func IsStringInArray(value string, array []string) bool {
	for _, v := range array {
		if v == value {
			return true
		}
	}
	return false
}

/***
 * <h2>生成 UnixNano 字符串</h2>
 */
func GetUnixNano() string {
	return strconv.FormatInt(time.Now().UnixNano(), 10)
}

/***
 * <h2>生成UUID</h2>
 */
func GetUUID() string {
	return GetUUIDV4()
}

/***
 * <h2>生成UUID</h2>
 * 基于mac地址、时间戳
 */
func GetUUIDV1() string {
	// Version 1: 时间 + Mac地址
	id, err := uuid.NewV1()
	if err != nil {
		fmt.Printf("uuid NewUUID err:%+v", err)
	}
	return id.String()
}

/***
 * <h2>生成UUID</h2>
 * 纯随机数
 */
func GetUUIDV4() string {
	id, err := uuid.NewV4()
	if err != nil {
		fmt.Printf("uuid NewUUID err:%+v", err)
	}
	return id.String()
}

4.2.3、JWT 工具类

注意:此工具类是本次项目进阶核心代码

在 utils/tool 目录新建文件 jwt_utils.go,完整代码如下:

package tool

import (
	"fmt"
	"go-iris/app/vo"

	"github.com/golang-jwt/jwt/v4"
)

/**
 * <h1>JWT 工具类</h1>
 * Created by woniu
 */

// JWT 秘钥,自己自定义
var jwtSecret = []byte("csdn_vip")

// Claim 是一些实体(通常指的用户)的状态和额外的元数据
type DClaims struct {
	UserId   int    `json:"userId"`   // 用户ID
	UserName string `json:"userName"` // 用户姓名
	Account  string `json:"account"`  // 用户账号
	jwt.StandardClaims
}

/*
* <h2>打印当前结构的信息</h2>
**/
func (to *DClaims) ToString() {
	fmt.Println("用户 ID:", to.UserId)
	fmt.Println("用户姓名:", to.UserName)
	fmt.Println("用户账号:", to.Account)
	fmt.Println("唯一ID:", to.StandardClaims.Id)
	fmt.Println("主题:", to.StandardClaims.Subject)
	fmt.Println("发行人:", to.StandardClaims.Issuer)
	fmt.Println("发布时间:", to.StandardClaims.IssuedAt)
	fmt.Println("生效时间:", to.StandardClaims.NotBefore)
	fmt.Println("过期时间:", to.StandardClaims.ExpiresAt)
}

/**
 * <h2>生成 JWT 的 Token</h2>
 */
func SignedJwtTokenBySysUser(sysUser *vo.SysUser) string {

	// 获取系统当前时间
	dataTime := DataTimeNow()
	// 过期时间,2 个小时
	expTime := DataTimeAddHour(dataTime, 2)

	claims := DClaims{
		UserId:   1,
		UserName: sysUser.Name,
		Account:  sysUser.Account,
		StandardClaims: jwt.StandardClaims{
			Id:        GetUUID(),              // 唯一 ID
			Subject:   "主题",                   // 主题
			Issuer:    "发行人",                  // 发行人
			IssuedAt:  DataTimeUnix(dataTime), // 发布时间,单位:秒
			NotBefore: DataTimeUnix(dataTime), // 生效时间,单位:秒
			ExpiresAt: DataTimeUnix(expTime),  // 过期时间,单位:秒

		},
	}

	tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 该方法内部生成签名字符串,再用于获取完整、已签名的token
	tokenString, _ := tokenClaims.SignedString(jwtSecret)

	return tokenString
}

/**
 * <h2>解析 JWT 的 Token</h2>
 */
func ParseWithClaims(tokenString string) (*DClaims, error) {

	// 用于解析鉴权的声明,方法内部主要是具体的解码和校验的过程,最终返回*Token
	tokenClaims, err := jwt.ParseWithClaims(tokenString, &DClaims{}, func(token *jwt.Token) (interface{}, error) {
		return jwtSecret, nil
	})

	if err != nil {
		return nil, err
	}

	if tokenClaims != nil {
		// 从 Token 中获取到 Claims 对象,并使用断言,将该对象转换为我们自己定义的 DClaims
		// 要传入指针,项目中结构体都是用指针传递,节省空间。
		if claims, ok := tokenClaims.Claims.(*DClaims); ok && tokenClaims.Valid {
			return claims, nil
		}
	}
	return nil, err
}

4.2.4、用户登录功能

4.2.4.1、系统用户响应结构

在 app/vo 目录修改文件 sys_user_vo.go,完整代码如下:

package vo

// 系统用户信息
type SysUser struct {
	Account string `json:"account"` // 用户账号
	Name    string `json:"name"`    // 用户姓名
	Age     int    `json:"age"`     // 用户年龄
	Token   string `json:"token"`   // token
}
4.2.4.2、系统用户业务处理

在 app/controller 目录修改文件 sys_user_controller.go,完整代码如下:

package controller

import (
	"go-iris/app/dto"
	"go-iris/app/service"
	"go-iris/utils/common"
	"go-iris/utils/tool"

	"github.com/kataras/iris/v12"
)

/**
 * <h1>系统用户控制器</h1>
 * Created by woniu
 */
var SysUser = new(SysUserController)

type SysUserController struct{}

/**
 * <h2>用户名密码登录</h2>
 */
func (sc *SysUserController) PasswordLogin(ctx iris.Context) {

	// 登录参数
	var requestModel dto.LoginDto

	// 参数绑定
	if err := ctx.ReadForm(&requestModel); err != nil {
		ctx.JSON(common.ResponseError(-1, "传参异常"))
		return
	}

	// 用户登录
	sysUser, err := service.SysUser.PasswordLogin(requestModel)

	if err != nil {
		// 响应失败
		ctx.JSON(common.ResponseErrorMessage(err.Error()))
		return
	}

	// 生成 JWT 的 Token
	sysUser.Token = tool.SignedJwtTokenBySysUser(sysUser)

	// 响应成功
	ctx.JSON(common.ResponseSuccess(sysUser))
}

4.2.5、业务异常自定义

在 utils/constant 目录修改文件 errors.go,完整代码如下:

package constant

import "errors"

/**
 * <h1>业务异常自定义</h1>
 * Created by woniu
 */

var (
	ResErrAuthorizationIsNilErr = errors.New("1001_token 为空")
	ResErrSysUserPasswordErr    = errors.New("1003_密码错误")
	ResErrSysUserIsNil          = errors.New("1004_用户不存在")
)

4.2.6、请求路由

在 router 目录修改文件 router.go,完整代码如下:

package router

import (

	// 自己业务 controller 路径
	"fmt"
	"go-iris/app/controller"
	"go-iris/utils/common"
	"go-iris/utils/constant"
	"go-iris/utils/tool"

	"github.com/kataras/iris/v12"
)

/**
 * <h2>注册路由</h2>
 */
func RegisterRouter(app *iris.Application) {

	// 跨域解决方案
	app.Use(Cors)
	// 登录验证中间件
	app.Use(CheckAuthorization)

	// 系统用户
	login := app.Party("/sysUser")
	{
		// 用户名 + 密码登录
		login.Post("/passwordLogin", controller.SysUser.PasswordLogin)
	}

	// 通用异常
	err := app.Party("/error")
	{
		err.Get("/responseError", controller.Error.ResponseError)
		err.Get("/responseErrorCode", controller.Error.ResponseErrorCode)
		err.Get("/responseErrorMessage", controller.Error.ResponseErrorMessage)
	}
}

/**
 * <h2>跨域解决方案</h2>
 */
func Cors(ctx iris.Context) {
	ctx.Header("Access-Control-Allow-Origin", "*")
	if ctx.Request().Method == "OPTIONS" {
		ctx.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH,OPTIONS")
		ctx.Header("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization")
		ctx.StatusCode(204)
		return
	}
	ctx.Next()
}

/**
 * <h2>登录验证中间件</h2>
 */
func CheckAuthorization(ctx iris.Context) {
	fmt.Println("登录验证中间件", ctx.Path())
	// 放行设置
	urlItem := []string{"/sysUser/passwordLogin", "/sysUser/login"}
	if !tool.IsStringInArray(ctx.Path(), urlItem) {
		// 从请求头中获取Token
		token := ctx.GetHeader("Authorization")

		// 请求头 Authorization 为空
		if tool.IsEmpty(token) {
			ctx.JSON(common.ResponseErrorMessage(constant.ResErrAuthorizationIsNilErr.Error()))
			return
		}

		claims, err := tool.ParseWithClaims(token)
		if err != nil {
			fmt.Println("token 解析异常信息:", err)
			ctx.JSON(common.ResponseErrorMessage(err.Error()))
			return
		}

		// 打印消息
		claims.ToString()
	}
	// 前置中间件
	ctx.Application().Logger().Infof("Runs before %s", ctx.Path())
	ctx.Next()
}


5、启动并测试

5.1、启动项目

在 VS Code 终端输入以下命令并执行

# 启动项目
go run main.go

有以下信息代表启动成功

Iris Version: 12.2.0-beta6

Now listening on: http://localhost:8080
Application started. Press CTRL+C to shut down.

5.2、测试接口

5.2.1、用户登录

account = admin 并且 password = 123456,响应成功

在这里插入图片描述

响应结果增加 JWT 格式 token 字段

JSON Web Tokens (JWT) 在线解密

在这里插入图片描述

5.2.2、通用响应异常测试

请求头不添加 Authorization,响应失败

在这里插入图片描述

请求头里加入Authorization,响应成功

在这里插入图片描述
通过上述测试验证,项目达到预期目标,小伙伴自己赶紧动手试试吧。

6、每日一记

UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。通常平台会提供生成的API。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和随机数。

6.1、UUID 五种版本的区别

  • V1,基于mac地址、时间戳。

  • V2,based on timestamp,MAC address and POSIX UID/GID (DCE 1.1)

  • V3,Hash获取入参并对结果进行MD5。

  • V4,纯随机数。

  • V5,based on SHA-1 hashing of a named value。

6.2、UUID 两种包:

# 仅支持 V1 和 V4 版本
github.com/google/uuid

# 支持全部五个版本
github.com/gofrs/uuid

本文教程到此结束,有问题欢迎大家讨论。

实践是检验真理的唯一标准,一键送三连关注不迷路。

;