跳转至

在后端验证 JWT

本节将介绍如何解码 JWT 令牌以获取当前登录的用户。

说明

使用 NestJS 开发?@authgear/nestjs SDK 提供了现成的模块、守卫(guard)和装饰器,您无需手动搭建 JWT 校验逻辑。

开始之前,请确保在管理台的应用设置中启用了将 JWT 作为访问令牌签发选项。

在管理台的应用设置中启用此选项

在应用中开启将 JWT 作为访问令牌签发选项后,Authany 会将 JWT 作为访问令牌签发。传入的 HTTP 请求应在 Authorization 请求头中携带该访问令牌。无需设置反向代理,您的后端服务器就可以使用 Authany 的 JWKS 来验证请求,并从 JWT 访问令牌中解码用户信息。

JWT 访问令牌的载荷

访问令牌中的声明请参见此参考文档:JWT 访问令牌。如需了解如何往 JWT 中添加声明,请参阅为 JWT 访问令牌或 ID 令牌添加自定义字段。

查找 JSON Web Key Sets(JWKS)端点

这个发现端点(Discovery endpoint)是一个 JSON 文档,包含您应用的 OpenID Connect 配置,其中包括授权端点、令牌端点和 JWKS 端点。

https://<YOUR_AUTHGEAR_ENDPOINT>/.well-known/openid-configuration

JSON Web Key Sets(JWKS)端点可以在配置中的 jwks_uri 字段中找到。

OpenID Connect 配置 JSON 示例

以下是具体样式的示例。

{
    "issuer": "https://project-id.authanyid.com",
    "authorization_endpoint": "https://project-id.authanyid.com/oauth2/authorize",
    "jwks_uri": "https://project-id.authanyid.com/oauth2/jwks", // the JWKS endpoint
    ...
}

从访问令牌中解码用户信息

请按照以下步骤示例来验证并解码 JWT 令牌。

第 1 步:安装依赖包

pip install cryptography
pip install PyJWT

第 2 步:查找 JSON Web Key Sets(JWKS)端点

定义一个函数,从 OpenID Connect 配置中查找 JWKS 端点。将您的 Authany 端点用作 base_address

import json
from contextlib import closing
from urllib.request import urlopen

base_address = "https://<your_app_endpoint>"

def fetch_jwks_uri(base_address):
    doc_url = base_address + "/.well-known/openid-configuration"
    with closing(urlopen(doc_url)) as f:
        doc = json.load(f)
    jwks_uri = doc["jwks_uri"]
    if not jwks_uri:
        raise Exception('Failed to fetch jwks uri.')
    return jwks_uri

第 3 步:从 Authorization 请求头中获取 JWT 令牌

定义一个函数,从传入请求的 Authorization 请求头中提取访问令牌。它应该形如 Authorization: Bearer <access_token>。

def parse_header(authz_header):
    parts = authz_header.split(" ")
    if len(parts) != 2:
        return

    scheme = parts[0]
    if scheme.lower() != "bearer":
        return

    return parts[1]

第 4 步:验证并解码 JWT 令牌

这里我们展示一个使用 Flask 框架保护路径的示例。您可能需要根据自己的技术栈调整部分代码。

from flask import request
import jwt
from jwt import PyJWKClient

@app.route("/hello")
def hello():
    authz_header = request.headers.get("Authorization")
    if not authz_header:
        return {
            "message": "authz header not found"
        }

    # get jwt token from Authorization header
    token = parse_header(authz_header)
    if token:
        try:
            # fetch jwks_uri from the Authgear Discovery Endpoint
            jwks_uri = fetch_jwks_uri(base_address)
            # Reuse PyJWKClient for better performance
            jwks_client = PyJWKClient(jwks_uri)
            signing_key = jwks_client.get_signing_key_from_jwt(token)
            user_data = jwt.decode(
                token,
                signing_key.key,
                algorithms=["RS256"],
                audience=base_address,
                options={"verify_exp": True},
            )
            return {
                "message": "Hello!",
                "user_data": user_data
            }
        except:
            return {
                "message": "JWT decode failed"
            }
    else:
        return {
            "message": "no token"
        }

第 1 步:安装依赖

npm install --save axios jwks-rsa jsonwebtoken

第 2 步:查找 JWKS 端点

使用以下方法获取 JWKS URI(您需要用这个 URI 从 JWT 中提取公钥)。

const appUrl = ""; //place your authgear app endpoint here
const getJwksUri = async (appUrl) => {
    const config_endpoint = appUrl + "/.well-known/openid-configuration";
    const data = await axios.get(config_endpoint);
    return data.data.jwks_uri;
}

第 3 步:从请求头中提取 JWT

在 Express 应用中使用以下代码,从 Bearer [token] 格式的 Authorization 请求头中仅提取令牌部分:

const express = require("express");
const axios = require("axios");
const node_jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const app = express();
const port = 3002;
app.get('/', async (req, res) => {

    const requestHeader = req.headers;
    if (requestHeader.authorization == undefined) {
        res.send("Invalid header");
        return;
    }
    const authorizationHeader = requestHeader.authorization.split(" ");
    const access_token = authorizationHeader[1];

}

第 4 步:解码访问令牌

接下来,解码访问令牌,以便从结果中提取 JWT 的 kid。您需要用这个 kid 来获取公钥。使用以下代码解码 JWT:

const decoded_access_token = node_jwt.decode(access_token, {complete: true});

第 5 步:获取 JWT 签名密钥并验证 JWT

使用以下代码提取 JWT 公钥,然后用这些密钥验证 JWT:

const jwks_uri = await getJwksUri(appUrl);
    const client = jwksClient({
        strictSsl: true,
        jwksUri: jwks_uri
    });
    const signing_key = await client.getSigningKey(decoded_access_token.header.kid);

    try {
        const verify = node_jwt.verify(access_token, signing_key.publicKey, { algorithms: ['RS256'] });
        res.send(JSON.stringify(verify))
    }
    catch(error) {
        res.send(error);  
    }

完成以上所有步骤后,您的 Express 应用大致如下:

const express = require("express");
const axios = require("axios");
const node_jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const app = express();
const port = 3002;

const appUrl = "https://demo-1-ea.authanyid.com";
const getJwksUri = async (appUrl) => {
    const config_endpoint = appUrl + "/.well-known/openid-configuration";
    const data = await axios.get(config_endpoint);
    return data.data.jwks_uri;
}

app.get('/', async (req, res) => {

    const requestHeader = req.headers;
    if (requestHeader.authorization == undefined) {
        res.send("Invalid header");
        return;
    }
    const authorizationHeader = requestHeader.authorization.split(" ");
    const access_token = authorizationHeader[1];
    const decoded_access_token = node_jwt.decode(access_token, {complete: true});
    const jwks_uri = await getJwksUri(appUrl);
    const client = jwksClient({
        strictSsl: true,
        jwksUri: jwks_uri
    });
    const signing_key = await client.getSigningKey(decoded_access_token.header.kid);

    try {
        const verify = node_jwt.verify(access_token, signing_key.publicKey, { algorithms: ['RS256'] });
        res.send(JSON.stringify(verify))
    }
    catch(error) {
        res.send(error);  
    }
});

app.listen(port, () => {
    console.log(`server started on port ${port}`);
});

将您的 Authany 端点用作 base_address

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"

    "github.com/lestrrat-go/jwx/v3/jwk"
    "github.com/lestrrat-go/jwx/v3/jwt"
)

var (
    baseAddress = "https://<your_app_endpoint>"
)

type OIDCDiscoveryDocument struct {
    JWKSURI string `json:"jwks_uri"`
}

func FetchOIDCDiscoveryDocument(endpoint string) (*OIDCDiscoveryDocument, error) {
    resp, err := http.DefaultClient.Get(endpoint)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf(
            "failed to fetch discovery document: unexpected status code: %d",
            resp.StatusCode,
        )
    }

    var document OIDCDiscoveryDocument
    err = json.NewDecoder(resp.Body).Decode(&document)
    if err != nil {
        return nil, err
    }
    return &document, nil
}

func FetchJWK(baseAddress string) (jwk.Set, error) {
    doc, err := FetchOIDCDiscoveryDocument(
        baseAddress + "/.well-known/openid-configuration",
    )
    if err != nil {
        return nil, err
    }

    set, err := jwk.Fetch(context.Background(), doc.JWKSURI)
    return set, err
}

// DecodeUser parse request Authorization header and obtain user id and claims
func DecodeUser(r *http.Request) (string, bool, bool, error) {
    // fetch jwks_uri from Authgear
    // you can cache the value of jwks using jwk.Cache to have better performance
    set, err := FetchJWK(baseAddress)
    if err != nil {
        return "", false, false, fmt.Errorf("failed to fetch JWK: %s", err)
    }

    // parse jwt token
    token, err := jwt.ParseRequest(
        r, 
        // This may not work out of the box depending on the jwk.Set.
        // Please read about requirements for "kid" and "alg" (and possibly
        // "WithDefaultKey") when using jwk.Set in the jwt.WithKeySet documentation.
        jwt.WithKeySet(set),
    )
    if err != nil {
        return "", false, false, fmt.Errorf("invalid token: %s", err)
    }

    // validate jwt token
    err = jwt.Validate(token,
        jwt.WithClock(jwt.ClockFunc(
            func() time.Time { return time.Now().UTC() },
        )),
        jwt.WithAudience(baseAddress),
    )
    if err != nil {
        return "", false, false, fmt.Errorf("invalid token: %s", err)
    }

    var verified bool
    var anonymous bool
    // ignore errors -- if the claim does not exist, it doesn't matter
    _ = token.Get("https://authgear.com/claims/user/is_verified", &verified)
    _ = token.Get("https://authgear.com/claims/user/is_anonymous", &anonymous)

    // In jwx v3, Subject() returns (value, ok).
    sub, _ := token.Subject()
    return sub, verified, anonymous, nil
}

func handler(w http.ResponseWriter, r *http.Request) {
    // decode user example
    userid, isUserVerified, isAnonymousUser, err := DecodeUser(r)

    // ... your handler logic
}

以下示例使用 Spring Boot。

第 1 步:安装依赖

在您的 build.gradle 文件中添加以下依赖:

dependencies {
    implementation("com.nimbusds:nimbus-jose-jwt:10.2")
    implementation("org.json:json:20250107")
}

然后在控制器文件顶部添加以下导入:

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;

第 2 步:获取 JWKS 端点

实现以下方法来获取 JWKS URI:

private static String fetchJwksUri(String baseAddress) throws Exception {
    String docUrl = baseAddress + "/.well-known/openid-configuration";
    HttpURLConnection conn = (HttpURLConnection) new URL(docUrl).openConnection();
    conn.setRequestMethod("GET");

    try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            response.append(line);
        }

        String jwksUri = new org.json.JSONObject(response.toString()).getString("jwks_uri");
        if (jwksUri == null || jwksUri.isEmpty()) {
            throw new Exception("Failed to fetch JWKS URI.");
        }
        return jwksUri;
    }
}

第 3 步:获取签名密钥

使用以下方法从 JWK 中获取签名密钥:

private static RSAKey getSigningKeyFromJwks(String jwksUri, String token) throws Exception {
    JWKSet jwkSet = JWKSet.load(new URL(jwksUri));
    List<com.nimbusds.jose.jwk.JWK> keys = jwkSet.getKeys();

    SignedJWT signedJWT = SignedJWT.parse(token);
    String keyId = signedJWT.getHeader().getKeyID();

    return keys.stream()
            .filter(jwk -> jwk.getKeyID().equals(keyId))
            .findFirst()
            .map(jwk -> (RSAKey) jwk)
            .orElse(null);
}

第 4 步:验证 JWT

为了演示如何验证 JWT,我们将在 Spring Boot 应用中实现一个 validateJWT 端点。该端点会从 Bearer Authorization 请求头中读取访问令牌。

它会调用第 1 步和第 2 步中的 fetchJwksUri() 和 getSigningKeyFromJwks(),获取解析 JWT 所需的 JWK URI 和签名密钥。

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

@SpringBootApplication
@RestController
public class DemoApplication {

    //paste implementation for fetchJwksUri() method below this line.

    //paste implemetation of getSigningKeyFromJwks() method below this line.

    private static final String BASE_ADDRESS = ""; //place your authgear app endpoint here

    @GetMapping("/validateJwt")
    public Object validateJwt(@RequestHeader("Authorization") String authorizationHeader) {
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            return new ResponseMessage("authorization header not found");
        }

        String token = authorizationHeader.substring(7); // Extract token

        try {
            // Fetch JWKS URI dynamically
            String jwksUri = fetchJwksUri(BASE_ADDRESS);

            // Get signing key from JWKS
            RSAKey signingKey = getSigningKeyFromJwks(jwksUri, token);
            if (signingKey == null) {
                return new ResponseMessage("JWT decode failed: Signing key not found");
            }

            // Validate and decode JWT
            SignedJWT signedJWT = SignedJWT.parse(token);
            JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();

            return new ResponseMessage("Hello!", claimsSet.toJSONObject());

        } catch (Exception e) {
            return new ResponseMessage("JWT decode failed: " + e.getMessage());
        }
    }

    private static String fetchJwksUri(String baseAddress) throws Exception {
        String docUrl = baseAddress + "/.well-known/openid-configuration";
        HttpURLConnection conn = (HttpURLConnection) new URL(docUrl).openConnection();
        conn.setRequestMethod("GET");

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }

            String jwksUri = new org.json.JSONObject(response.toString()).getString("jwks_uri");
            if (jwksUri == null || jwksUri.isEmpty()) {
                throw new Exception("Failed to fetch JWKS URI.");
            }
            return jwksUri;
        }
    }

    private static RSAKey getSigningKeyFromJwks(String jwksUri, String token) throws Exception {
        JWKSet jwkSet = JWKSet.load(new URL(jwksUri));
        List<com.nimbusds.jose.jwk.JWK> keys = jwkSet.getKeys();

        SignedJWT signedJWT = SignedJWT.parse(token);
        String keyId = signedJWT.getHeader().getKeyID();

        return keys.stream()
                .filter(jwk -> jwk.getKeyID().equals(keyId))
                .findFirst()
                .map(jwk -> (RSAKey) jwk)
                .orElse(null);
    }

    static class ResponseMessage {
        public String message;
        public Object user_data;

        public ResponseMessage(String message) {
            this.message = message;
        }

        public ResponseMessage(String message, Object user_data) {
            this.message = message;
            this.user_data = user_data;
        }
    }
}

第 1 步:安装依赖包

首先,运行以下命令安装所需的依赖:

composer require firebase/php-jwt
composer require guzzlehttp/guzzle

第 2 步:查找 JWKS 端点

使用以下代码创建一个函数,从您的 Authany 应用端点中查找 JWKS 端点:

<?php
require 'vendor/autoload.php';
use Firebase\JWT\JWT;

use Firebase\JWT\JWK;
use Firebase\JWT\Key;
use GuzzleHttp\Client;

$appUrl = ""; //place your authgear app endpoint here

function getJwksUri($appUrl) {
    $configEndpoint = $appUrl . "/.well-known/openid-configuration";
    $httpClient = new Client();
    $response = $httpClient->request('GET', $configEndpoint);
    $responseObject = json_decode($response->getBody());
    return $responseObject->jwks_uri;
}

第 3 步:获取签名密钥

在您的应用中添加以下代码来获取 JWT 签名密钥:

$jwksUri = getJwksUri($appUrl);
$httpClient = new Client();
$jwksUriResponse = $httpClient->request('GET', $jwksUri);
$keysObject = json_decode($jwksUriResponse->getBody());

$jwks = (array) ($keysObject->keys)[0];

$parsedKey = JWK::parseKey($jwks, "RS256");
$signingKey = $parsedKey->getKeyMaterial();

第 4 步:从请求头中提取 JWT

使用以下代码从 HTTP 请求中提取访问令牌:

if (!isset($_SERVER['HTTP_AUTHORIZATION']))
    throw new Exception("Invalid authorization header");
$authorizationHeader = $_SERVER['HTTP_AUTHORIZATION'];
$jwt = (explode(" ", $authorizationHeader))[1];

第 5 步:验证并解码 JWT

最后,用签名密钥解码 JWT。

$decoded = JWT::decode($jwt, new Key($signingKey, 'RS256'));
echo json_encode($decoded);

第 1 步:安装 NuGet 包

dotnet add package NSwag.AspNetCore

然后在 program.cs 文件顶部添加以下导入:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

第 2 步:配置 JWT 身份验证

这会让 ASP.NET Core 使用 JWT Bearer 令牌进行身份验证

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

builder.Services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;

}).AddJwtBearer(x => // here we configure what to validate in JWT tokens  
{
    // .NET will automatically fetch JWKS keys from {authority}/.well-known/openid-configuration
    x.Authority = ""; // place your authgear app endpoint here,
    x.RequireHttpsMetadata = false; // Allow HTTP for development
    x.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = x.Authority,
        ValidateAudience = false, // set to true if you validate audience
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
    };
});

第 3 步:添加授权

builder.Services.AddAuthorization();

第 4 步:配置中间件管道

顺序很重要!身份验证必须在授权之前

app.UseAuthentication();
app.UseAuthorization();

第 5 步:创建受保护的端点

以下示例使用 Minimal API 模型

app.MapGet("/", (HttpContext context) => 
{
    var user = context.User;
    Console.WriteLine($"Authorization header: {context.Request.Headers.Authorization}");
    Console.WriteLine($"User authenticated: {user.Identity?.IsAuthenticated}");
    if (user.Identity?.IsAuthenticated == true)
    {
        var claims = user.Claims.ToDictionary(c => c.Type, c => c.Value);
        return Results.Ok(claims);
    }
    return Results.Json(new { error = "Unauthorized" }, statusCode: 401);
}).RequireAuthorization(); // require auth from this endpoint

对于基于 Controller 的 API,只需在控制器类或各个 action 方法上添加 [Authorize] 即可对其进行保护

检查 JWT 的有效性

OIDC ID 令牌中的 auth_time 声明表示用户完成身份验证的时间。请从令牌中提取 auth_time 声明,它应表示原始身份验证发生的时间(以秒为单位)。如果当前时间与 auth_time 之差超过您设定的阈值(例如 5 分钟),则发起重新验证身份流程。

关于如何验证 ID 令牌的签名,以及验证其中的 auth_time 声明,请参见这里的示例。

从 Cookie 中解码用户信息

在应用服务器中验证 JWT _目前_仅适用于基于令牌的身份验证。

说明

对于基于 Cookie 的身份验证,目前还不支持在 Cookie 中使用 JWT。您可以在此追踪相关议题。