為什么需要權限
在平常瀏覽網(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 中
那么就需要修改三處代碼:
- 查找用戶 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
}
- 生成 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
}