Skip to main content

Golang

配置Authok

获取应用密钥

你需要如下信息

  • Domain
  • Client ID
  • Client Secret

配置回调URL

配置 Logout URL

在 Golang 中集成 Authok

下载依赖

go.mod
module 01-Login

go 1.16

require (
github.com/coreos/go-oidc/v3 v3.1.0
github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.7.4
github.com/joho/godotenv v1.4.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
)

运行如下命令下载依赖:

go mod download

配置应用

在项目根目录创建一个 .env文件用于存储应用配置, 加入如下环境变量:

# Authok 租户的域名URL, 也可以是自定义域名.
AUTHOK_DOMAIN='YOUR_DOMAIN'

# Authok 应用的 Client ID.
AUTHOK_CLIENT_ID='YOUR_CLIENT_ID'

# Authok 应用的 Client Secret.
AUTHOk_CLIENT_SECRET='YOUR_CLIENT_SECRET'

# 应用的回调URL.
AUTHOK_CALLBACK_URL='http://localhost:3000/callback'

配置 OAuth2 和 OpenID Connect

platform/authenticator/auth.go

package authenticator

import (
"context"
"errors"
"os"

"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)

// Authenticator is used to authenticate our users.
type Authenticator struct {
*oidc.Provider
oauth2.Config
}

// New instantiates the *Authenticator.
func New() (*Authenticator, error) {
provider, err := oidc.NewProvider(
context.Background(),
"https://"+os.Getenv("AUTHOK_DOMAIN")+"/",
)
if err != nil {
return nil, err
}

conf := oauth2.Config{
ClientID: os.Getenv("AUTHOK_CLIENT_ID"),
ClientSecret: os.Getenv("AUTHOK_CLIENT_SECRET"),
RedirectURL: os.Getenv("AUTHOK_CALLBACK_URL"),
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile"},
}

return &Authenticator{
Provider: provider,
Config: conf,
}, nil
}

// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken.
func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.New("no id_token field in oauth2 token")
}

oidcConfig := &oidc.Config{
ClientID: a.ClientID,
}

return a.Verifier(oidcConfig).Verify(ctx, rawIDToken)
}

创建应用路由

platform/router/router.go

package router

import (
"encoding/gob"
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"

"01-Login/platform/authenticator"
"01-Login/platform/middleware"
"01-Login/web/app/callback"
"01-Login/web/app/login"
"01-Login/web/app/logout"
"01-Login/web/app/user"
)

// New registers the routes and returns the router.
func New(auth *authenticator.Authenticator) *gin.Engine {
router := gin.Default()

// To store custom types in our cookies,
// we must first register them using gob.Register
gob.Register(map[string]interface{}{})

store := cookie.NewStore([]byte("secret"))
router.Use(sessions.Sessions("auth-session", store))

router.Static("/public", "web/static")
router.LoadHTMLGlob("web/template/*")

router.GET("/", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "home.html", nil)
})
router.GET("/login", login.Handler(auth))
router.GET("/callback", callback.Handler(auth))
router.GET("/user", user.Handler)
router.GET("/logout", logout.Handler)

return router
}

启动应用

main.go
package main

import (
"log"
"net/http"

"github.com/joho/godotenv"

"01-Login/platform/authenticator"
"01-Login/platform/router"
)

func main() {
if err := godotenv.Load(); err != nil {
log.Fatalf("Failed to load the env vars: %v", err)
}

auth, err := authenticator.New()
if err != nil {
log.Fatalf("Failed to initialize the authenticator: %v", err)
}

rtr := router.New(auth)

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

登录

web/app/login/login.go

package login

import (
"crypto/rand"
"encoding/base64"
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"

"01-Login/platform/authenticator"
)

// Handler for our login.
func Handler(auth *authenticator.Authenticator) gin.HandlerFunc {
return func(ctx *gin.Context) {
state, err := generateRandomState()
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// Save the state inside the session.
session := sessions.Default(ctx)
session.Set("state", state)
if err := session.Save(); err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

ctx.Redirect(http.StatusTemporaryRedirect, auth.AuthCodeURL(state))
}
}

func generateRandomState() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}

state := base64.StdEncoding.EncodeToString(b)

return state, nil
}

home.html模版中添加 /login路由:

web/template/home.html
<div>
<h3>Authok 示例</h3>
<a href="/login">登录</a>
</div>

处理认证回调

web/app/callback/callback.go

package callback

import (
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"

"login/platform/authenticator"
)

// Handler for our callback.
func Handler(auth *authenticator.Authenticator) gin.HandlerFunc {
return func(ctx *gin.Context) {
session := sessions.Default(ctx)
if ctx.Query("state") != session.Get("state") {
ctx.String(http.StatusBadRequest, "Invalid state parameter.")
return
}

// Exchange an authorization code for a token.
token, err := auth.Exchange(ctx.Request.Context(), ctx.Query("code"))
if err != nil {
ctx.String(http.StatusUnauthorized, "Failed to exchange an authorization code for a token.")
return
}

idToken, err := auth.VerifyIDToken(ctx.Request.Context(), token)
if err != nil {
ctx.String(http.StatusInternalServerError, "Failed to verify ID Token.")
return
}

var profile map[string]interface{}
if err := idToken.Claims(&profile); err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

session.Set("access_token", token.AccessToken)
session.Set("profile", profile)
if err := session.Save(); err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// Redirect to logged in page.
ctx.Redirect(http.StatusTemporaryRedirect, "/user")
}
}

展示用户信息

web/app/user/user.go

package user

import (
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)

// Handler for our logged-in user page.
func Handler(ctx *gin.Context) {
session := sessions.Default(ctx)
profile := session.Get("profile")

ctx.HTML(http.StatusOK, "user.html", profile)
}
web/template/user.html
<div>
<img class="avatar" src="{{ .picture }}"/>
<h2>欢迎 {{.nickname}}</h2>
</div>

注销

web/app/logout/logout.go
package logout

import (
"net/http"
"net/url"
"os"

"github.com/gin-gonic/gin"
)

// Handler for our logout.
func Handler(ctx *gin.Context) {
logoutUrl, err := url.Parse("https://" + os.Getenv("AUTHOK_DOMAIN") + "/v1/logout")
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

scheme := "http"
if ctx.Request.TLS != nil {
scheme = "https"
}

returnTo, err := url.Parse(scheme + "://" + ctx.Request.Host)
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

parameters := url.Values{}
parameters.Add("returnTo", returnTo.String())
parameters.Add("client_id", os.Getenv("AUTHOK_CLIENT_ID"))
logoutUrl.RawQuery = parameters.Encode()

ctx.Redirect(http.StatusTemporaryRedirect, logoutUrl.String())
}

web/static/js中创建文件 user.js, 用于删除cookie

$(document).ready(function () {
$('.btn-logout').click(function (e) {
Cookies.remove('auth-session');
});
});

可选步骤

检查用户是否认证

platform/middleware/isAuthenticated.go
package middleware

import (
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)

// IsAuthenticated is a middleware that checks if
// the user has already been authenticated previously.
func IsAuthenticated(ctx *gin.Context) {
if sessions.Default(ctx).Get("profile") == nil {
ctx.Redirect(http.StatusSeeOther, "/")
} else {
ctx.Next()
}
}

给需要认证的路由添加中间件.

platform/router/router.go
router.GET("/user", middleware.IsAuthenticated, user.Handler)