Skip to main content

GoLang API

授权

验证访问令牌

安装依赖

go.mod
module Authorization-RS256

go 1.16

require (
github.com/authok/go-jwt-middleware/v2 v2.0.0
github.com/joho/godotenv v1.4.0
)

下载依赖

go mod download

配置应用

创建.env文件用于设置配置.

.env
# The URL of our AuthOK Tenant Domain.
# If you're using a Custom Domain, be sure to set this to that value instead.
AUTHOK_DOMAIN='YOUR_DOMAIN'

# AuthOK API's Identifier.
AUTHOK_AUDIENCE='YOUR_API_IDENTIFIER'

创建中间件用于验证访问令牌

middleware/jwt.go
package middleware

import (
"context"
"log"
"net/http"
"net/url"
"os"
"time"

"github.com/authok/go-jwt-middleware/v2"
"github.com/authok/go-jwt-middleware/v2/jwks"
"github.com/authok/go-jwt-middleware/v2/validator"
)

// CustomClaims contains custom data we want from the token.
type CustomClaims struct {
Scope string `json:"scope"`
}

// Validate does nothing for this example, but we need
// it to satisfy validator.CustomClaims interface.
func (c CustomClaims) Validate(ctx context.Context) error {
return nil
}

// EnsureValidToken is a middleware that will check the validity of our JWT.
func EnsureValidToken() func(next http.Handler) http.Handler {
issuerURL, err := url.Parse("https://" + os.Getenv("AUTHOK_DOMAIN") + "/")
if err != nil {
log.Fatalf("Failed to parse the issuer url: %v", err)
}

provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)

jwtValidator, err := validator.New(
provider.KeyFunc,
validator.RS256,
issuerURL.String(),
[]string{os.Getenv("AUTHOK_AUDIENCE")},
validator.WithCustomClaims(
func() validator.CustomClaims {
return &CustomClaims{}
},
),
validator.WithAllowedClockSkew(time.Minute),
)
if err != nil {
log.Fatalf("Failed to set up the jwt validator")
}

errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Encountered error while validating JWT: %v", err)

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message":"Failed to validate JWT."}`))
}

middleware := jwtmiddleware.New(
jwtValidator.ValidateToken,
jwtmiddleware.WithErrorHandler(errorHandler),
)

return func(next http.Handler) http.Handler {
return middleware.CheckJWT(next)
}
}

保护 API 端点

main.go
package main

import (
"log"
"net/http"

"github.com/joho/godotenv"

"Authorization-RS256/middleware"
)

func main() {
if err := godotenv.Load(); err != nil {
log.Fatalf("Error loading the .env file: %v", err)
}

router := http.NewServeMux()

// 公开路由
router.Handle("/api/public", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"Hello from a public endpoint! You don't need to be authenticated to see this."}`))
}))

// 需要有效的 access_token.
router.Handle("/api/private", middleware.EnsureValidToken()(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// CORS Headers.
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"Hello from a private endpoint! You need to be authenticated to see this."}`))
}),
))

log.Print("Server listening on http://localhost:3010")
if err := http.ListenAndServe("0.0.0.0:3010", router); err != nil {
log.Fatalf("There was an error with the http server: %v", err)
}
}

验证 Scope

func (c CustomClaims) HasScope(expectedScope string) bool {
result := strings.Split(c.Scope, " ")
for i := range result {
if result[i] == expectedScope {
return true
}
}

return false
}

使用 HasScope函数 校验 read:messages:

func main() {
// ...

// This route is only accessible if the user has a
// valid access_token with the read:messages scope.
router.Handle("/api/private-scoped", middleware.EnsureValidToken()(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// CORS Headers.
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")

w.Header().Set("Content-Type", "application/json")

token := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)

claims := token.CustomClaims.(*middleware.CustomClaims)
if !claims.HasScope("read:messages") {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"message":"Insufficient scope."}`))
return
}

w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"Hello from a private endpoint! You need to be authenticated to see this."}`))
}),
))
// ...
}

使用 API

在应用中调用API

curl --request GET \
--url http://localhost:3010/api/private \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'

获取访问令牌

在单页应用 或 移动端/原生应用中, 在授权成功后,你需要获取 访问令牌. 如何获取令牌以及如何调用API将取决于您正在开发的应用程序类型和使用的框架.

更多信息请参考相关应用程序快速入门:

curl --request POST \
--url 'https://YOUR_DOMAIN/oauth/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data 'client_id=YOUR_CLIENT_ID' \
--data client_secret=YOUR_CLIENT_SECRET \
--data audience=YOUR_API_IDENTIFIER

测试API

1. 调用被保护端点

curl --request GET \
--url http://localhost:3010/api/private

以上调用会返回 401 HTTP (Unauthorized) 状态码.

携带 AccessToken 进行调用

curl --request GET \
--url http://localhost:3010/api/private \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'

此时,会返回成功响应.

2. 调用被作用域保护的端点

curl --request GET \
--url http://localhost:3010/api/private-scoped \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'