gin框架39--重构 BasicAuth 中间件
- 介绍
- gin BasicAuth 解析
- 自定义newAuth实现基础认证
- 注意事项
- 说明
介绍
每当我们打开一个网址的时候,会自动弹出一个认证界面,要求我们输入用户名和密码,这种BasicAuth是最基础、最常见的认证方式,gin框架中提供了一种内置的方式,但它只能用内置的用户和密码,无法使用外部db中的用户和密码,这种方式很多时候是不友好的。
为此,本文根据gin.BasicAuth的原理对其就行重构,实现一个简单的newAuth中间件,该中间件可以代替默认的BasicAuth,并且可以按需更改为自定义查询函数,实现从外部db或者用户管理系统查询信息实现登录认证的功能。
gin BasicAuth 解析
博文 gin框架14–使用 BasicAuth 中间件 介绍了BasicAuth 中间件的基础使用方法,直接使用 gin.BasicAuth(gin.Accounts{“foo”: “bar”, “austin”: “1234”, “lena”: “hello2”, “manu”: “4321”, }) 即可,非常简单实用。
实际上当我们访问url的时候,它会从请求的 Authorization 中获取用户信息,并和gin.Accounts中内置用户对比,如果用户存在就将用户名称存放在Context的 Keys map结构中,方便后续查找或者获取用户信息;如果不存在就设置c.Header(“WWW-Authenticate”, realm), 并返回c.AbortWithStatus(http.StatusUnauthorized),浏览器上的表现就是重新弹出输入用户名和密码的窗口 。
核心逻辑在 BasicAuthForRealm 方法中,如下所示:
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
if realm == "" {
realm = "Authorization Required"
}
realm = "Basic realm=" + strconv.Quote(realm)
pairs := processAccounts(accounts)
return func(c *Context) {
// Search user in the slice of allowed credentials
user, found := pairs.searchCredential(c.requestHeader("Authorization"))
if !found {
// Credentials doesn't match, we return 401 and abort handlers chain.
c.Header("WWW-Authenticate", realm)
c.AbortWithStatus(http.StatusUnauthorized)
return
}
// The user credentials was found, set user's id to key AuthUserKey in this context, the user's id can be read later using
// c.MustGet(gin.AuthUserKey).
c.Set(AuthUserKey, user)
}
}
自定义newAuth实现基础认证
gin.BasicAuth 只能提供默认的认证功能,且需要内置指定的用户|密码,但实际在代码中hardcode大量用户信息是不科学的,因此我们需要自己重构一个BasicAuth来实验基础认证功能。
此处实现了一个newAuth中间件,该中间件会判断用户是否输入账号|密码,并通过judgeUserExist来判断账号|密码是否正确,正确则返回用户信息,不正确则返回http.StatusUnauthorized, 具体案例如下。
此处为了简洁方便,此处直接内置了3个用户到users中,并用 judgeUserExist 查询用户账号密码是否正确。实际项目中可将该方法更改为查询db,无需在项目中hardcode内置用户。
package main
import (
"encoding/base64"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
"strings"
)
var users = gin.H{
"foo": gin.H{"email": "foo@bar.com", "phone": "123433", "pwd": "bar"},
"austin": gin.H{"email": "austin@example.com", "phone": "666", "pwd": "123"},
"lena": gin.H{"email": "lena@guapa.com", "phone": "523443", "pwd": "456"},
}
func help() string {
helpStr := `hello gin:
127.0.0.1:8088/your-api
/auth/user
`
return helpStr
}
func judgeUserExist(userName, userPwd string) (string, bool) {
// 实际项目中将该函数更改为从db查询即可,此处为了简单直接从预定的users中查询。
msg := ""
tag := false
if userInfo, ok := users[userName]; ok {
pwd, ok := userInfo.(gin.H)["pwd"]
if ok && pwd == userPwd {
msg = fmt.Sprintf("用户%v密码正确", userName)
tag = true
} else {
msg = fmt.Sprintf("用户%v密码不正确", userName)
}
} else {
msg = fmt.Sprintf("用户%v不存在", userName)
}
return msg, tag
}
func getUserPwdFromAuthorization(auth string) (user, pwd string) {
// auth[:6]="Basic "
base64UserPwd, err := base64.StdEncoding.DecodeString(auth[6:])
if err != nil {
panic(err)
}
base64UserPwdStr := string(base64UserPwd)
colonIndex := strings.Index(base64UserPwdStr, ":")
user = base64UserPwdStr[:colonIndex]
pwd = base64UserPwdStr[colonIndex+1:]
return user, pwd
}
func newAuth(realm string) func(c *gin.Context) {
if realm == "" {
realm = "Authorization Required"
}
realm = "Basic realm=" + strconv.Quote(realm)
return func(c *gin.Context) {
authHeader := c.Request.Header.Get("Authorization") // 获取请求头中的数据
if authHeader == "" {
c.Header("WWW-Authenticate", realm)
c.AbortWithStatus(http.StatusUnauthorized)
return
} else {
user, pwd := getUserPwdFromAuthorization(authHeader)
// fmt.Printf("user=%v,pwd=%v\n", user, pwd)
msg, tag := judgeUserExist(user, pwd)
if !tag {
// c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": msg, "tag": tag})
fmt.Println(msg)
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.Set(gin.AuthUserKey, user)
}
}
}
func userHandler(c *gin.Context) {
user := c.MustGet(gin.AuthUserKey).(string)
c.IndentedJSON(http.StatusOK, gin.H{
"status": 200,
"msg": "it's fine",
"userInfo": users[user],
})
}
func main() {
app := gin.Default()
app.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, help())
})
authorized := app.Group("/auth", newAuth(""))
authorized.GET("/user", userHandler)
app.Run(":8088")
}
输出:
注意事项
- c.Header中需要添加 WWW-Authenticate 字段,否则初次访问的时候不会弹出输入用户名、密码的框!!!
说明
- 测试环境
ubuntu22.04 Desktop
go1.20.7 - 参考文档
using-basicauth-middleware
Gin框架 -- 中间件