Docs Vault

在现代软件开发中,应用安全是软件开发时必须要考虑或实现的核心功能点。不安全的应用会带来诸如数据泄露、应用服务中断等严重问题。这些问题,不仅会给企业带来经济损失,还会导致企业声誉和品牌受损,影响企业或产品未来的发展。


在企业应用开发中,可以在语言层、应用层、操作系统层、网络层等方面来保证应用的安全。应用层的安全主要由开发者来实现,通常可以通过以下三种手段来保障应用层的安全:

  1. 认证(Authentication,简称 Authn):认证是指通过一定的手段,确认用户或实体(如服务、设备)的身份。它是应用安全的第一道防线,用于证明“你是谁”。常见的认证方式包括用户名与密码、数字证书、令牌认证、生物识别(如指纹、人脸识别)以及多因素认证(MFA)等;
  2. 授权(Authorization,简称 Authz):授权是在身份认证成功之后进行的,用于确定已认证的用户或应用对某一资源的访问权限以及操作范围。授权回答的是“你可以做什么”的问题,例如是否允许访问特定的文件、执行某些操作或调用相关接口。常见的授权机制包括基于角色的访问控制(RBAC)和基于属性的访问控制(ABAC);
  3. 使用 HTTPS 协议通信:HTTPS 是一种在 HTTP 基础上通过 SSL/TLS 加密实现安全通信的协议,具有数据机密性、完整性和身份认证三大核心特性,能够有效防止数据在传输过程中被窃听、篡改或伪造,同时通过数字证书验证服务器身份,保障通信双方的信任关系。


本节课及下节课会详细介绍下 miniblog 中认证、鉴权和 HTTPS 协议通信的设计和实现。


提示:
本节课最终源码位于 miniblog 项目的 feature/s26 分支。


身份认证是最简单,也是最基本的应用安全保障手段,基本上每一个对外的应用都需要实现身份认证功能。


下面,我们来看下 miniblog 项目具体是如何设计和实现 认证功能的。


常用的身份验证手段


当前业界存在多种身份认证手段,例如基础认证(用户名密码认证)、摘要认证(Digest 认证)、开放授权(OAuth 认证)、令牌认证(Bearer 认证)等。


在前后端分离架构中,最常用的认证方式为基础认证与令牌认证的结合:

  1. 基础认证:通过<用户名+密码>的方式登录系统;
  2. 令牌认证:通过 Token 进行认证,当前最流行的 Token 编码方式是 JWT。


在前后端分离架构中,用户通过控制台登录系统时,需要一种简单、易用的认证方式来完成用户身份的验证。目前最简单的方式是 <用户名+密码> 认证,这里的用户名也可以是手机号或邮箱。

提示


目前还有许多系统使用短信验证进行身份认证,但由于短信验证依赖于第三方接口,并不适合用于教学项目。因此,本实战项目仅采用<用户名+密码>的认证形式。


<用户名+密码> 认证的流程为:后台根据用户名查询出数据库中保存的加密密码串,并对用户传入的明文密码进行加密,然后比较两次加密后的密码是否相等,以此验证用户传入的密码是否正确。


然而,用户登录控制台后需要执行多个操作。如果每次操作都需要传入用户名和密码,后台再从数据库中查询加密密码并进行对比,整个过程不仅体验不友好,还会因为频繁查询数据库而导致接口性能下降。


为了解决上述问题,业界最常用的方案是在用户首次登录后生成一个具有一定有效期的 Token(令牌),并将其存储在浏览器的 Cookie 或 LocalStorage 中。之后的每次请求都会携带该 Token,服务器接收到请求后,通过 Token 对请求进行鉴权。因为 Token 有过期时间,也可以在 Token 过期之前,调用 Token 刷新接口,续签 Token,实现比 Token 过期时间更长的登录状态。Token 有不同的实现方式,业界用的最多的实现方式是 JSON Web Token,简称 JWT。


令牌认证的优点


令牌认证是最常用的 API 请求认证方式,其受欢迎的原因在于使用令牌认证可以带来以下好处:

  1. 无状态性(Stateless):令牌认证基于无状态协议(如 HTTP),服务器不需要保存用户会话数据,所有必要的信息都保存在令牌中。这种无状态性减少了服务器资源的占用(如内存和存储),使系统易于扩展,特别是在分布式系统和微服务架构中,无需同步会话数据;
  2. 离线验证:许多令牌类型(如 JWT)是自包含的,不需要通过服务器数据库查询即可验证其合法性和解析内容。这种特性可以降低服务器的负载,并支持高并发场景;
  3. 提高安全性:使用 Token 可以避免直接暴露用户的敏感信息,例如用户名和密码。另外,通过给 Token 设置过期时间,可以减小 Token 泄露后,被滥用的时间窗口;
  4. 支持跨平台和跨服务调用:Token 通常采用标准化的格式(如 JWT),可以在不同语言和平台之间轻松解析和验证。这使得 Token 非常适合分布式系统中多个服务之间的认证和授权;
  5. 灵活性:令牌可以携带额外的信息,比如用户名、用户 ID 等。服务器无需额外查询数据库即可从令牌中读取这些信息,减少了请求延迟。


在实际开发中,使用令牌认证,还有其他很多好处。掌握令牌认证的原理和实现方法,是 Go 语言开发者,必备的核心技能之一。


JWT 核心内容


由于 miniblog 使用 JWT Token 进行身份认证,为了降低学习难度并为后续代码实现奠定基础,本节课将介绍 JWT 的核心内容。


JWT 认证流程


学习 JWT 的最佳方式是通过其认证流程理解其原理。认证流程如图 11-1 所示。

图 11-1 JWT 认证流程


图 11-1 中展示了 JWT 的认证流程,具体流程如下:

  1. 客户端(通常是前端)通过用户名和密码进行登录;
  2. 服务端收到请求后会验证用户名和密码,若与数据库记录不一致,则认证失败,若一致,则认证通过。认证通过后,服务端会签发一个具有有效期的 Token 并返回给客户端;
  3. 客户端接收到 Token 后会将其缓存,例如存储在浏览器的 Cookie 或本地存储中,方便下次调用时使用;
  4. 客户端在之后的每次 API 请求中携带缓存的 Token;
  5. 服务端接收到请求后会验证请求中携带的 Token,验证通过后继续处理业务逻辑并返回数据;
  6. 如果 Token 快过期,前端会调用 Token 刷新接口续期 Token,避免用户再次登录。之后,会使用续期后的 Token 发送 API 请求。


提示:
Go 项目开发中,Token 有效期通常设置为 2 小时。


JWT Token 格式


在 JWT 中,Token 由 Header、Payload、Signature 三部分组成,中间用英文点号(.)隔开,并使用 Base64 编码。JWT Token 示例如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzkwNzgwMDUsImlhdCI6MTczNTQ3ODAwNSwibmJmIjoxNzM1NDc4MDA1LCJ4LXVzZXItaWQiOiJ1c2VyLXc2aXJrZyJ9.GromRG7kK90UfU_Q5iOSHs_xE-zSk0e0HLHqJQUjYMU


(1)Header 介绍


JWT Token 的 Header 中包含两部分信息:Token 的类型和 Token 所使用的加密算法。JWT Header 示例如下:

{
  "typ": "JWT",
  "alg": "HS256"
}

上述示例表明,Token 类型是 JWT,加密算法为 HS256(alg 支持多种加密算法)。


(2)Payload 载荷介绍


Payload 中携带了 Token 的具体内容,其中包含一些标准字段,当然也可以添加额外字段以表达更丰富的信息。这些信息可以用于更复杂的处理场景,例如记录请求的用户 ID、用户名等。标准字段包括:

  1. iss:JWT Token 的签发者;
  2. sub:主题;
  3. exp:JWT Token 的过期时间;
  4. aud:接收 JWT Token 的一方;
  5. iat:JWT Token 的签发时间;
  6. nbf:JWT Token 的生效时间;
  7. jti:JWT Token 的唯一标识(ID)。


Payload 示例如下所示:

{
  "id": 2,
  "userID": "user-p7q78j",
  "nbf": 1527931805,
  "iat": 1527931805
}


(3)Signature 签名介绍


Signature 是 Token 的签名部分,其生成方式如下:

  1. 使用 Base64 对 header.payload 进行编码;
  2. 使用密钥(Secret)对编码后的内容进行加密,加密后的内容即为 Signature。


密钥相当于一个密码,存储在服务端,通常通过配置文件设置密钥的值。miniblog 项目中,密钥由$HOME/.miniblog/mb-apiserver.yaml 配置文件中的 jwt-key 配置项来配置。


最终生成的 Token 如下所示:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzkwNzgwMDUsImlhdCI6MTczNTQ3ODAwNSwibmJmIjoxNzM1NDc4MDA1LCJ4LXVzZXItaWQiOiJ1c2VyLXc2aXJrZyJ9.GromRG7kK90UfU_Q5iOSHs_xE-zSk0e0HLHqJQUjYMU


签名后,服务端会返回生成的 Token。客户端在下次请求时会携带该 Token,服务端收到 Token 后会解析出 header.payload,然后使用相同的加密算法和密码对 header.payload 再次加密,并将加密后的 Token 与收到的 Token 进行比对。如果二者相同,则验证通过;如果不相同,则返回 HTTP 401 Unauthorized 错误。


JWT Token 生成示例


代码清单 11-1(位于 scripts/gen_token.sh 文件中)展示了具体如何生成一个 JWT Token,通过代码可以详细的了解 Token 生成的方式:

代码清单 11-1 JWT Token 生成示例

#!/bin/bash

# 定义Header
HEADER='{"alg":"HS256","typ":"JWT"}'

# 定义Payload
PAYLOAD='{"exp":1739078005,"iat":1735478005,"nbf":1735478005,"x-user-id":"user-w6irkg"}'

# 定义Secret(用于签名)
SECRET="Rtg8BPKNEf2mB4mgvKONGPZZQSaJWNLijxR42qRgq0iBb5"

# 1. Base64编码Header
HEADER_BASE64=$(echo -n "${HEADER}" | openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')

# 2. Base64编码Payload
PAYLOAD_BASE64=$(echo -n "${PAYLOAD}" | openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')

# 3. 拼接Header和Payload为签名数据
SIGNING_INPUT="${HEADER_BASE64}.${PAYLOAD_BASE64}"

# 4. 使用HMAC SHA256算法生成签名
SIGNATURE=$(echo -n "${SIGNING_INPUT}" | openssl dgst -sha256 -hmac "${SECRET}" -binary | openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')

# 5. 拼接最终的JWT Token
JWT="${SIGNING_INPUT}.${SIGNATURE}"

# 输出JWT Token
echo "Generated JWT Token:"
echo "${JWT}"


Token 认证实现思路


在 Token 认证流程中,需要对密码进行加密和验证,以及对 Token 进行签发和解析。这两类功能是许多 Go 项目中常见的基础功能,因此可以考虑在 pkg 目录下分别实现 auth 包和 token 包,用于实现上述功能。此外,由于绝大部分请求都需要进行身份认证,最优的实现方式是通过 gRPC 拦截器或 Gin 中间件完成认证功能。


miniblog 身份认证功能实现


根据功能需求、实现思路以及依赖关系,总结出以下认证功能实现步骤:

  1. 开发 token 包;
  2. 签发 Token;
  3. 实现认证中间件;
  4. 加载认证中间件。


(1)开发 token 包


签发 Token 需要使用密钥。为了能够通过 token.Sign() 这种简洁的方式直接签发,而不是采用 t := token.New(); t.Sign() 这种相对繁琐的调用方式,需要实现一个 Init 方法用于将密钥保存在全局变量中,供 token 包后续操作使用。在实现 Init 函数后,为防止同一服务进程中多次初始化密钥,可以通过 sync.Once 确保 token 包仅被初始化一次。这种实现方式借鉴了许多高质量代码,是一种优雅且安全的设计。


Token 中可以携带一些额外的信息,自然会想到,将用户 ID 或用户名保存在 Token 中,在请求时通过解析 Token 便可以拿到用户 ID,根据用户 ID 从数据库查询用户信息,用于后续的请求处理。在 JWT Token 中,类似用户 ID 这种唯一的身份标识,一般会用 identityKey 来指代,identityKey 可以保存在 Token 的 Claims 中。因此,在签发 Token 时,需要将用户 ID 存入 Token 中,在解析过程中,则需要从 Claims 中提取用户 ID。


在需要实现一个功能时,优先考虑的方法是去 GitHub 上查找社区有无现成的优秀实现(Go 包、代码段或完整项目)。经过在 GitHub 上的搜索,发现 golang-jwt/jwt 星星最多,而且其功能可以满足项目开发需求。所以,接下来就可以使用 golang-jwt/jwt 包完成 Token 的签发和解析。


因为 miniblog 项目同时实现了 gRPC 服务器和 HTTP 服务器,所以 token 包提供的 Token 解析函数,需要能够支持同时从 HTTP 请求头和 gRPC Header 元数据中获取 Token 信息。


最终开发完成后的 token 包代码位于 feature/s24 分支的 pkg/token/token.go 文件中。


(2)签发 Token


在 miniblog 项目中,签发 Token 的逻辑位于用户登录和密钥刷新两个 API 接口中。Login 接口 Biz 层签发 Token 的代码如代码清单 11-2 所示。

代码清单 11-2 Login 接口签发 Token

// Login 实现 UserBiz 接口中的 Login 方法.
func (b *userBiz) Login(ctx context.Context, rq *apiv1.LoginRequest) (*apiv1.LoginResponse, error) {
    // 获取登录用户的所有信息
    whr := where.F("username", rq.GetUsername())
    userM, err := b.store.User().Get(ctx, whr)
    if err != nil {
        return nil, errno.ErrUserNotFound
    }

    // 对比传入的明文密码和数据库中已加密过的密码是否匹配
    if err := auth.Compare(userM.Password, rq.GetPassword()); err != nil {
        return nil, errno.ErrPasswordInvalid
    }

    // 如果匹配成功,说明登录成功,签发 token 并返回
    tokenStr, expireAt, err := token.Sign(userM.UserID)
    if err != nil {
        return nil, errno.ErrSignToken
    }

    return &apiv1.LoginResponse{Token: tokenStr, ExpireAt: timestamppb.New(expireAt)}, nil
}


在代码清单 11-2 中,首先通过对比密码是否跟数据库中保存的密码是否一致来判断是否允许登录。如果一致,则 Login 方法会调用 token.Sign(userM.UserID) 签发 Token 并返回,在签发 Token 时,会在 Token 的 Payload 中保存 UserID。


Login 接口返回了签发的 Token 字符串以及 Token 过期时间。返回 Token 过期时间可以帮助客户端判断 Token 的有效性,从而在 Token 过期之前续期 Token。Login 接口返回示例如下:

{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzkyMjc4MTEsImlhdCI6MTczNTYyNzgxMSwibmJmIjoxNzM1NjI3ODExLCJ4LXVzZXItaWQiOiJ1c2VyLTl0dXIwMiJ9.YTzAbSE-RZwh4DVJNIobADXXTA0Mn9X1gcTvbKL9QxA",
    "expireAt": {
        "seconds": 1739227811,
        "nanos": 671029750
    }
}


token.Sign 方法实现位于 pkg/token/token.go 文件中,代码如代码清单 11-3 所示。

代码清单 11-3 签发 Token 函数实现

// Sign 使用 jwtSecret 签发 token,token 的 claims 中会存放传入的 subject.
func Sign(identityKey string) (string, time.Time, error) {
    // 计算过期时间
    expireAt := time.Now().Add(config.expiration)
    
    // Token 的内容
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        config.identityKey: identityKey,       // 存放用户身份
        "nbf":              time.Now().Unix(), // token 生效时间
        "iat":              time.Now().Unix(), // token 签发时间
        "exp":              expireAt.Unix(),   // token 过期时间
    })
    
    // 签发 token 
    tokenString, err := token.SignedString([]byte(config.key))
    if err != nil {
        return "", time.Time{}, err
    }

    return tokenString, expireAt, nil // 返回 token 字符串、过期时间和错误
}


代码清单 11-3 定义了一个 Sign 函数,用于使用 config.key 签发 JWT Token。Sign 函数接受一个 identityKey 参数(通常表示用户身份标识,miniblog 项目是 UserID),并在 JWT 的 claims 中存储用户身份信息、签发时间(iat)、生效时间(nbf),以及过期时间(exp)(由当前时间加上配置的过期时长计算)。如果签名成功,函数返回生成的令牌字符串、过期时间,以及错误信息(如果有)。


miniblog 项目的 PUT /refresh-token 接口用来续签 Token,在签发 Token 之前,需要先确保认证通过,Token 签发实现跟 Login 接口保持一致,不再介绍。


(3)实现认证中间件


企业应用开发中,认证能力通常以 Web 中间件的方式来实现。为此,还需要开发 gRPC 拦截器和 Gin 中间件。这里以 gRPC 认证拦截器的实现为例,来介绍下认证中间件如何编写。Gin 认证中间件实现跟 gRPC 认证拦截器实现类似,不再介绍。


gRPC 认证拦截器的实现代码位于 internal/pkg/middleware/grpc/authn.go 文件中,代码如代码清单 11-4 所示。

代码清单 11-4 gRPC 认证拦截器实现

// UserRetriever 用于根据用户名获取用户信息的接口.
type UserRetriever interface {
    // GetUser 根据用户 ID 获取用户信息
    GetUser(ctx context.Context, userID string) (*model.UserM, error)
}

// AuthnInterceptor 是一个 gRPC 拦截器,用于进行认证.
func AuthnInterceptor(retriever UserRetriever) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
        // 解析 JWT Token
        userID, err := token.ParseRequest(ctx)
        if err != nil {
            log.Errorw("Failed to parse request", "err", err)
            return nil, errno.ErrTokenInvalid.WithMessage(err.Error())
        }

        log.Debugw("Token parsing successful", "userID", userID)

        user, err := retriever.GetUser(ctx, userID)
        if err != nil {
            return nil, errno.ErrUnauthenticated.WithMessage(err.Error())
        }

        // 将用户信息存入上下文
        //nolint: staticcheck
        ctx = context.WithValue(ctx, known.XUsername, user.Username)
        //nolint: staticcheck
        ctx = context.WithValue(ctx, known.XUserID, userID)

        // 供 log 和 contextx 使用
        ctx = contextx.WithUserID(ctx, user.UserID)
        ctx = contextx.WithUsername(ctx, user.Username)

        // 继续处理请求
        return handler(ctx, req)
    }
}

代码清单 11-4 实现了一个 gRPC 认证拦截器(AuthnInterceptor),用于对 gRPC 请求进行身份认证。它依赖 UserRetriever 接口,来根据用户 ID 获取用户信息。拦截器首先通过 token.ParseRequest 从请求中解析出用户 ID,如果解析失败,则直接返回认证失败的错误。接着,它通过 retriever.GetUser 根据用户 ID 查询用户信息,成功获取用户信息后,拦截器将用户数据添加到请求的上下文及自定义上下文中,以便在后续处理逻辑中使用。


代码清单 11-4 通过引入 UserRetriever 接口,对用户信息获取逻辑进行了抽象,从而实现了解耦上层依赖的目的。AuthnInterceptor 中间件仅依赖 UserRetriever 接口对用户信息进行处理,而无需直接与具体实现(如数据库或缓存)耦合。这使中间件层保持了独立性和纯粹性,专注于 gRPC 请求的认证流程,不受底层用户数据获取逻辑的影响。这样的设计不仅提升了代码的可维护性和扩展性,还遵循了依赖倒置原则(DIP),从而使系统能够灵活切换不同的用户信息获取实现(如数据库、远程服务或缓存)而无需任何修改中间件逻辑。


(4)加载认证中间件


在实现了认证中间件之后,还需要在服务器启动时加载认证拦截器。在 internal/apiserver/grpcserver.go 文件中添加以下代码,来给 gRPC 服务器加载认证拦截器:

...
import (
    ...
    "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors"
    "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/selector"
    ...
)
func (c *ServerConfig) NewGRPCServerOr() (server.Server, error) {
    // 配置 gRPC 服务器选项,包括拦截器链
    serverOptions := []grpc.ServerOption{
        // 注意拦截器顺序!
        grpc.ChainUnaryInterceptor(
            ...
            // 认证拦截器
            selector.UnaryServerInterceptor(mw.AuthnInterceptor(c.retriever), NewAuthnWhiteListMatcher()),
            ...
        ),
    }
    ...
}

// NewAuthnWhiteListMatcher 创建认证白名单匹配器.
func NewAuthnWhiteListMatcher() selector.Matcher {
    whitelist := map[string]struct{}{
        apiv1.MiniBlog_Healthz_FullMethodName:    {},
        apiv1.MiniBlog_CreateUser_FullMethodName: {},
        apiv1.MiniBlog_Login_FullMethodName:      {},
    }
    return selector.MatchFunc(func(ctx context.Context, call interceptors.CallMeta) bool {
        _, ok := whitelist[call.FullMethod()]
        return !ok
    })
}

上述代码给 gRPC 服务器添加了认证拦截器和白名单功能。NewGRPCServerOr 方法通过配置 grpc.ChainUnaryInterceptor 将多种拦截器组成一个拦截器调用链,其中 AuthnInterceptor 用于认证逻辑。


为了在应用认证时排除白名单中的方法,代码通过 selector.UnaryServerInterceptor 包装 AuthnInterceptor,并结合 NewAuthnWhiteListMatcher 创建了方法匹配器。白名单匹配器使用 MatchFunc 定义了一组无需认证的方法(如健康检查、用户创建和登录方法),通过检查调用方法是否在白名单中决定是否跳过认证。


在加载认证中间件时,需要传入 UserRetriever 接口类型的参数。UserRetriever 接口类型的实现及创建代码位于 internal/apiserver/server.go 文件中,代码如下:

// UserRetriever 定义一个用户数据获取器. 用来获取用户信息.
type UserRetriever struct {
    store store.IStore
}

// GetUser 根据用户 ID 获取用户信息.
func (r *UserRetriever) GetUser(ctx context.Context, userID string) (*model.UserM, error) {
    return r.store.User().Get(ctx, where.F("userID", userID))
}   


上述代码实现了 UserRetriever 接口,用来从数据库中根据 UserID 获取用户信息。


修改 internal/apiserver/httpserver.go 文件,变更代码如下:

...
// 注册 API 路由。路由的路径和 HTTP 方法,严格遵循 REST 规范.
func (c *ServerConfig) InstallRESTAPI(engine *gin.Engine) {
    engine.POST("/login", handler.Login)
    // 注意:认证中间件要在 handler.RefreshToken 之前加载
    engine.PUT("/refresh-token", mw.AuthnMiddleware(c.retriever), handler.RefreshToken)

    authMiddlewares := []gin.HandlerFunc{mw.AuthnMiddleware(c.retriever)}

    // 注册 v1 版本 API 路由分组
    v1 := engine.Group("/v1")
    {
        // 用户相关路由
        userv1 := v1.Group("/users")
        {   
            // 创建用户。这里要注意:创建用户是不用进行认证和授权的
            userv1.POST("", handler.CreateUser) 
            userv1.Use(authMiddlewares...)
            ...
        }

        // 博客相关路由
        postv1 := v1.Group("/posts", authMiddlewares...)
        {   
            ...
        }
    }
}


在上述代码中,将 AuthnBypasswMiddleware 替换成了 AuthnMiddleware。这里要注意认证中间件的添加位置,确保只在需要的路由中执行认证中间件。


至此,miniblog 认证能力开发完成,完整代码见 feature/s24 分支。


认证功能测试



执行以下命令编译并启动 mb-apiserver:

$ git checkout feature/s24 # 切换到对应的分支,或者master分支
$ make build # 编译源码
$ _output/platforms/linux/amd64/mb-apiserver # 启动服务


打开一个新的 Linux 终端,分别执行以下命令进行测试。


(1)创建新的用户


创建用户命令如下:

$ curl -XPOST -H"Content-Type: application/json" http://127.0.0.1:5555/v1/users  -d'{"username":"authntest","password":"miniblog1234","nickname":"colin404","email":"[email protected]","phone":"18130000000"}'
{"userID":"user-die7iy"}


上述命令会新建一个名为 authntest 的测试用户,接口返回用户的 ID:user-die7iy。


(2)使用用户名和密码登录


登录命令如下:

$ curl -XPOST -H"Content-Type: application/json" http://127.0.0.1:5555/login -d'{"username":"authntest","password":"miniblog1234"}'
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDIwMDA2MzcsImlhdCI6MTczODQwMDYzNywibmJmIjoxNzM4NDAwNjM3LCJ4LXVzZXItaWQiOiJ1c2VyLWRpZTdpeSJ9.bRvWwAtgE2_tkIhccx69URJdBvbu9macDErWnrulW88", "expireAt":"2025-02-01T01:03:57.620410022Z"}

上述命令请求 /login 接口,该接口会返回 Token 及 Token 的过期时间。


(3)使用 Token 创建博客


使用以下命令创建博客:

$ curl -XPOST -H"Content-Type: application/json" -H"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDIwMDA2MzcsImlhdCI6MTczODQwMDYzNywibmJmIjoxNzM4NDAwNjM3LCJ4LXVzZXItaWQiOiJ1c2VyLWRpZTdpeSJ9.bRvWwAtgE2_tkIhccx69URJdBvbu9macDErWnrulW88" http://127.0.0.1:5555/v1/posts -d'{"title":"installation","content":"installation."}'
{"postID":"post-w6irkg"}

上述命令调用了 /v1/posts 接口来创建一个博客,调用接口时,传入了认证头:-H"Authorization: Bearer <Token>",<Token> 是步骤 2 中 /login 接口返回的 Token。博客创建成功后,会返回博客 ID:post-w6irkg。


小结(AI 自动生成并人工审核)


本文详细介绍了在现代软件开发中如何通过认证机制保障应用层安全,并以 miniblog 项目为例,阐述了基于 JWT 的身份认证功能的设计与实现。


文章首先分析了常见的认证方式,并重点讲解了基础认证与令牌认证结合的优势,尤其是 JWT 在无状态性、离线验证和跨平台支持等方面的特点。随后,通过对 JWT 的结构和认证流程的解析,深入剖析了其核心原理,并结合实际代码,展示了如何在 miniblog 中开发 token 包、签发 Token,以及实现认证中间件。


通过 gRPC 拦截器和 HTTP 中间件,miniblog 实现了对 API 请求的统一认证逻辑,同时支持白名单机制以排除无需认证的接口。


最后,文章通过具体的测试步骤验证了认证功能的有效性,展示了如何通过 Token 认证确保用户身份的安全性和接口访问的可靠性,为企业级应用提供了实用的安全保障方案。