Golang:BasicAuth + JWT 校驗用戶權限

為什么需要權限

在平常瀏覽網(wǎng)頁中,大多數(shù)網(wǎng)站對用戶分為游客和普通用戶,還有會員,那么游客瀏覽一些網(wǎng)頁需要登錄才能看到,普通用戶查看一些帖子需要積分,而會員則像是開了一條綠色通道,什么都能訪問,這就是權限的作用

使用 BasicAuth 認證

BasicAuth 是開放平臺的認證方式,每次訪問 API 都會攜帶 用戶的 username 和 password 認證,那么 BasicAuth 會對 username 和 password 進行加密,那我們可以使用 PostMan 這個工具來測試 BasicAuth 的加密效果。為了測試,使用如下代碼,這里,我用了echo這個框架(當然也可以使用gin,iris,beego等框架)搭建了一個 login 的接口,在 login 這個接口中,我們從請求頭中獲取 BasicAuth 的加密信息(在 http 請求中,對請求認證的信息是放在請求頭的 Authorization 中)

// main.go
package main

import (
    "fmt"

    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    e.GET("/login", login)
    e.Logger.Fatal(e.Start(":1323"))
}

func login(c echo.Context) error {
    fmt.Println(c.Request().Header.Get("Authorization"))
    return nil
}

運行這段代碼,會在 1323 端口上進行監(jiān)聽,那么需要在 PostMan 中發(fā)起請求:


我們點擊 Send 后,在運行 go 總端中會打印出, username 和 password 加密后的結果


那么我們怎么拿到加密后的用戶名和密碼?在 echo 框架中內(nèi)置了簡單 BasicAuth 中間件,我們可以直接使用官方的實例代碼改造:

// 官方示例
// 這里的 username 和 password 就是我們傳過來的 username 和 password
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
    // Be careful to use constant time comparison to prevent timing attacks
    if subtle.ConstantTimeCompare([]byte(username), []byte("joe")) == 1 &&
        subtle.ConstantTimeCompare([]byte(password), []byte("secret")) == 1 {
        return true, nil
    }
    return false, nil
}))

改造我們的代碼:

// main.go
package main

import (
    "fmt"

    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
)

func main() {
    e := echo.New()
    e.Use(middleware.BasicAuth(auth))
    e.GET("/login", login)
    e.Logger.Fatal(e.Start(":1323"))
}

func auth(username, password string, c echo.Context) (bool, error) {
    fmt.Println("username", username)
    fmt.Println("password", password)
    return false, nil
}

func login(c echo.Context) error {
    fmt.Println(c.Request().Header.Get("Authorization"))
    return nil
}

那么再運行代碼,監(jiān)聽 1323 端口,再用 PostMan 發(fā)一次請求:


此時運行的結果為


我們可以看到,直接把加密的結果給解密出來了,使用 BasicAuth 中間件,解密過程就不需要我們來做了,直接交給 BasicAuth 中間件就好了。那么由于我們在 auth 中返回的是false,所以這個中間件會一直認證不通過,還會給 PostMan 返回一個 json 信息:

{
  "message": "Unauthorized"
}

之后,我們就可以使用 BasicAuth 做認證,改下 auth 的代碼

func auth(username, password string, c echo.Context) (bool, error) {
    if username != "startdusk" || password != "root" {
        return false, nil
    }
    return true, nil
}

使用 BasicAuth + JWT 認證

在 BasicAuth 中直接傳遞用戶的 username 和 password 顯然是不安全的,那么用戶的 username 和 password 只會在用戶登錄的時候傳遞一次,那么,可以用戶登錄過后給用戶頒發(fā)一個令牌(這個令牌里邊可以寫入用戶的 id 等不容易被識別的東西),然后 BasicAuth 就傳遞這個令牌,用戶可以拿著這個令牌去訪問其他接口,這樣我們就避免了傳遞 username 和 password 的風險。
這里推薦使用 jwt-go 這個庫,具體的生成 JWT 令牌代碼如下:


// 拓展我們要寫入token的信息
type Claims struct {
    Uid int `json:"uid"` // 擴展寫入用戶的id
    jwt.StandardClaims
}

func genToken(uid int) (string, error) {
    secretKey := "secretKey" // 加密的key
    expiresIn := time.Duration(24 * 30) // 設置過期時間
    claims := Claims{
        Uid: uid,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(time.Hour.Truncate(expiresIn)).Unix(),
            Issuer:    "startdusk", // 簽發(fā)的用戶(自定義名字)
        },
    }
    tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // 使用sha256進行加密claims這個結構體
    token, err := tokenClaims.SignedString([]byte(secretKey)) // 生成簽名
    return token, err
}

那我們的用戶請求流程變?yōu)椋?/p>

更據(jù)流程,我們將 login 部分的代碼修改(訪問 login 也改為POST的方式):



// 定義一個用戶的結構體,接收用戶數(shù)據(jù)
type User struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

// 模擬從數(shù)據(jù)庫查找用戶id的過程
func findUserIDFormDB(user *User) (int, bool) {
    if user.Username != "startdusk" || user.Password != "root" {
        return 0, false
    }

    return 1, true // 這里出于簡單,直接把用戶startdusk的id設置為1
}

func login(c echo.Context) error {
    var u User
    err := c.Bind(&u)
    if err != nil {
        return err
    }
    id, ok := findUserIDFormDB(&u)
    if !ok {
        return errors.New("沒有該用戶")
    }
    token, err := genToken(id)
    if err != nil {
        return err
    }
    return c.JSON(http.StatusOK, map[string]string{
        "token": token,
    })
}

然后,我們再定義一個用來用 token 訪問的其他接口 other :

func other(c echo.Context) error {
    return c.JSON(http.StatusOK, map[string]string{
        "content": "ok",
    })
}

到這步,我們既然用了 jwt 頒發(fā)令牌(即 token),那么在 BasicAuth 中要傳遞的兩個參數(shù) username 和 password 只用
在一個參數(shù)中傳遞 token ,一般是在 username 中傳遞,password 不用傳參數(shù),那么 auth 部分的代碼修改為:

func auth(username, password string, c echo.Context) (bool, error) {
    if username == "" {
        return false, nil
    }
    extractor := basicAuthExtractor{content: username}
    token, err := request.ParseFromRequest(
        c.Request(),
        extractor,
        func(token *jwt.Token) (interface{}, error) {
            return []byte("secretKey"), nil
        })
    if err != nil {
        return false, err
    }
    uid := getIntFromClaims("uid", token.Claims)
    if uid != 1 { // 我們在生成token的時候默認id為1
        return false, nil
    }
    return true, nil
}

func getStringFromClaims(key string, claims jwt.Claims) string {
    v := reflect.ValueOf(claims)
    if v.Kind() == reflect.Map {
        for _, k := range v.MapKeys() {
            value := v.MapIndex(k)
            if fmt.Sprintf("%s", k.Interface()) == key {
                return fmt.Sprintf("%v", value.Interface())
            }
        }
    }
    return ""
}

type basicAuthExtractor struct {
    content string
}

// basicAuthExtractor 實現(xiàn) request.Extractor 接口(jwt-go下的request)
func (e basicAuthExtractor) ExtractToken(*http.Request) (string, error) {
    return e.content, nil
}

修改后的代碼完整如下:

// main.go
package main

import (
    "errors"
    "fmt"
    "net/http"
    "reflect"
    "strconv"
    "time"

    "github.com/dgrijalva/jwt-go"
    "github.com/dgrijalva/jwt-go/request"
    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
)

func main() {
    e := echo.New()
    e.POST("/login", login)
    // 設置只在/test的路徑下使用BasicAuth
    test := e.Group("/test", middleware.BasicAuth(auth))
    {
        test.GET("/other", other)
    }
    e.Logger.Fatal(e.Start(":1323"))
}

func auth(username, password string, c echo.Context) (bool, error) {
    if username == "" {
        return false, nil
    }
    extractor := basicAuthExtractor{content: username}
    token, err := request.ParseFromRequest(
        c.Request(),
        extractor,
        func(token *jwt.Token) (interface{}, error) {
            return []byte("secretKey"), nil
        })
    if err != nil {
        return false, err
    }
    uid := getIntFromClaims("uid", token.Claims)
    if uid != 1 { // 我們在生成token的時候默認id為1
        return false, nil
    }
    return true, nil
}

// 解析 jwt token 方法
func getIntFromClaims(key string, claims jwt.Claims) int {
    s := getStringFromClaims(key, claims)
    value, err := strconv.Atoi(s)
    if err != nil {
        return 0
    }
    return value
}

func getStringFromClaims(key string, claims jwt.Claims) string {
    v := reflect.ValueOf(claims)
    if v.Kind() == reflect.Map {
        for _, k := range v.MapKeys() {
            value := v.MapIndex(k)
            if fmt.Sprintf("%s", k.Interface()) == key {
                return fmt.Sprintf("%v", value.Interface())
            }
        }
    }
    return ""
}

type basicAuthExtractor struct {
    content string
}

// basicAuthExtractor 實現(xiàn) request.Extractor 接口(jwt-go下的request)
func (e basicAuthExtractor) ExtractToken(*http.Request) (string, error) {
    return e.content, nil
}

// 定義一個用戶的結構體,接收用戶數(shù)據(jù)
type User struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

// 模擬從數(shù)據(jù)庫查找用戶id的過程
func findUserIDFormDB(user *User) (int, bool) {
    if user.Username != "startdusk" || user.Password != "root" {
        return 0, false
    }

    return 1, true // 這里出于簡單,直接把用戶startdusk的id設置為1
}

func login(c echo.Context) error {
    var u User
    err := c.Bind(&u)
    if err != nil {
        return err
    }
    id, ok := findUserIDFormDB(&u)
    if !ok {
        return errors.New("沒有該用戶")
    }
    token, err := genToken(id)
    if err != nil {
        return err
    }
    return c.JSON(http.StatusOK, map[string]string{
        "token": token,
    })
}

func other(c echo.Context) error {
    return c.JSON(http.StatusOK, map[string]string{
        "content": "ok",
    })
}

// 拓展我們要寫入token的信息
type Claims struct {
    Uid int `json:"uid"`
    jwt.StandardClaims
}

func genToken(uid int) (string, error) {
    secretKey := "secretKey"
    expiresIn := time.Duration(24 * 30)
    claims := Claims{
        Uid: uid,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(time.Hour.Truncate(expiresIn)).Unix(),
            Issuer:    "startdusk",
        },
    }
    tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    token, err := tokenClaims.SignedString([]byte(secretKey))
    return token, err
}

那么,到這里,運行代碼,我們按流程測試:
先登錄 login 獲取 token



獲取結果如下:



然后,我們拿這個token去訪問 other 接口:

測試結果:


我們成功的用token訪問到了 other 接口,當然,我們也測試下反例:

  • 1.沒有傳 token 的情況:
    返回
{
  "message": "Unauthorized"
}
  • 2.傳一個錯誤的 token 的情況:
    返回
{
  "message": "Internal Server Error"
}
  • 3.token 過期的情況:
    返回
{
  "message": "Internal Server Error"
}

測試的結果是正確,當然這個錯誤不是太好,需要另作錯誤處理,使得提示更友好,那在 Go 語言中,錯誤處理又是一個非常重要的東西,這里就不再展開,有興趣的同學可以試著去處理下。

使用 scope 區(qū)分權限

對于權限這塊,我們可以簡單點的方式來處理,這里使用 scope 來處理,即用數(shù)字的范圍來處理權限,比如:在用戶上加一個 scope 字段,它是一個數(shù)字,那么在 other 接口上也有一個 scope ,只要用戶的這個 scope 比 other 接口上的 scope 的數(shù)字大,那么用戶就可以訪問這個接口,反之則無權限訪問。

那么,這個 scope 可以寫入到用戶的數(shù)據(jù)庫中,然后用戶登錄的使用獲取 id 和用戶的 scope,在生成用戶的 token 的時候將 scope 寫入到用戶的 token 中
那么就需要修改三處代碼:

    1. 查找用戶 id 和 scope 的過程
// 模擬從數(shù)據(jù)庫查找用戶的id和scope的過程
func findUserIDFormDB(user *User) (int, int, bool) {
    if user.Username != "startdusk" || user.Password != "root" {
      return 0, 0, false
    }

    return 1, 1, true // 這里出于簡單,直接把用戶startdusk的id設置為1,scope也設置為1
}
    1. 生成 token 的過程中加入 scope
// 拓展我們要寫入token的信息
type Claims struct {
    Uid   int `json:"uid"`
    Scope int `json:"scope"`
    jwt.StandardClaims
}

func genToken(uid, scope int) (string, error) {
    secretKey := "secretKey"
    expiresIn := time.Duration(24 * 30)
    claims := Claims{
        Uid:   uid,
        Scope: scope,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(time.Hour.Truncate(expiresIn)).Unix(),
            Issuer:    "startdusk",
        },
    }
    tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    token, err := tokenClaims.SignedString([]byte(secretKey))
    return token, err
}
  • 3.修改 auth 中間件
func auth(username, password string, c echo.Context) (bool, error) {
    if username == "" {
        return false, nil
    }
    extractor := basicAuthExtractor{content: username}
    token, err := request.ParseFromRequest(
        c.Request(),
        extractor,
        func(token *jwt.Token) (interface{}, error) {
            return []byte("secretKey"), nil
        })
    if err != nil {
        return false, err
    }
    uid := getIntFromClaims("uid", token.Claims)
    if uid != 1 { // 我們在生成token的時候默認id為1
        return false, nil
    }
    // 校驗用戶是否有權限,scope 是否大于 8
    scope := getIntFromClaims("scope", token.Claims)
    if scope < 8 {
        return false, nil
    }
    return true, nil
}

修改完代碼后我們來試下:
先登錄 login 獲取 token



獲取結果如下:


然后,我們拿這個token去訪問 other 接口:



測試結果:



因為我們設置了用戶返回的 scope 為 1,小于 8,所以會沒權限訪問 other 接口,那我們把 scope 改為 9 看看:
// 模擬從數(shù)據(jù)庫查找用戶的id和scope的過程
func findUserIDFormDB(user *User) (int, int, bool) {
    if user.Username != "startdusk" || user.Password != "root" {
      return 0, 0, false
    }

    return 1, 9, true // 這里出于簡單,直接把用戶startdusk的id設置為1,scope也設置為9
}

重新測試:
先登錄 login 獲取 token



獲取結果如下:



然后,我們拿這個token去訪問 other 接口:

測試結果:

那么,當用戶的scope的大于接口的scope的時候,我們就能訪問到接口了,這就是簡單的權限控制,后面,大家可以把auth的scope當參數(shù)傳進來,靈活使用。

完整代碼:

package main

import (
    "errors"
    "fmt"
    "net/http"
    "reflect"
    "strconv"
    "time"

    "github.com/dgrijalva/jwt-go"
    "github.com/dgrijalva/jwt-go/request"
    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
)

func main() {
    e := echo.New()
    e.POST("/login", login)
    // 設置只在/test的路徑下使用BasicAuth
    test := e.Group("/test", middleware.BasicAuth(auth))
    {
        test.GET("/other", other)
    }
    e.Logger.Fatal(e.Start(":1323"))
}

func auth(username, password string, c echo.Context) (bool, error) {
    if username == "" {
        return false, nil
    }
    extractor := basicAuthExtractor{content: username}
    token, err := request.ParseFromRequest(
        c.Request(),
        extractor,
        func(token *jwt.Token) (interface{}, error) {
            return []byte("secretKey"), nil
        })
    if err != nil {
        return false, err
    }
    uid := getIntFromClaims("uid", token.Claims)
    if uid != 1 { // 我們在生成token的時候默認id為1
        return false, nil
    }
    // 校驗用戶是否有權限,scope 是否大于 8
    scope := getIntFromClaims("scope", token.Claims)
    if scope < 8 {
        return false, nil
    }
    return true, nil
}

// 解析 jwt token 方法
func getIntFromClaims(key string, claims jwt.Claims) int {
    s := getStringFromClaims(key, claims)
    value, err := strconv.Atoi(s)
    if err != nil {
        return 0
    }
    return value
}

func getStringFromClaims(key string, claims jwt.Claims) string {
    v := reflect.ValueOf(claims)
    if v.Kind() == reflect.Map {
        for _, k := range v.MapKeys() {
            value := v.MapIndex(k)
            if fmt.Sprintf("%s", k.Interface()) == key {
                return fmt.Sprintf("%v", value.Interface())
            }
        }
    }
    return ""
}

type basicAuthExtractor struct {
    content string
}

// basicAuthExtractor 實現(xiàn) request.Extractor 接口(jwt-go下的request)
func (e basicAuthExtractor) ExtractToken(*http.Request) (string, error) {
    return e.content, nil
}

// 定義一個用戶的結構體,接收用戶數(shù)據(jù)
type User struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

// 模擬從數(shù)據(jù)庫查找用戶的id和scope的過程
func findUserIDFormDB(user *User) (int, int, bool) {
    if user.Username != "startdusk" || user.Password != "root" {
        return 0, 0, false
    }

    return 1, 9, true // 這里出于簡單,直接把用戶startdusk的id設置為1,scope設置為9
}

func login(c echo.Context) error {
    var u User
    err := c.Bind(&u)
    if err != nil {
        return err
    }
    id, scope, ok := findUserIDFormDB(&u)
    if !ok {
        return errors.New("沒有該用戶")
    }
    token, err := genToken(id, scope)
    if err != nil {
        return err
    }
    return c.JSON(http.StatusOK, map[string]string{
        "token": token,
    })
}

func other(c echo.Context) error {
    return c.JSON(http.StatusOK, map[string]string{
        "content": "ok",
    })
}

// 拓展我們要寫入token的信息
type Claims struct {
    Uid   int `json:"uid"`
    Scope int `json:"scope"`
    jwt.StandardClaims
}

func genToken(uid, scope int) (string, error) {
    secretKey := "secretKey"
    expiresIn := time.Duration(24 * 30)
    claims := Claims{
        Uid:   uid,
        Scope: scope,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(time.Hour.Truncate(expiresIn)).Unix(),
            Issuer:    "startdusk",
        },
    }
    tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    token, err := tokenClaims.SignedString([]byte(secretKey))
    return token, err
}

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容