Why you need permissions

In ordinary browsing web pages, most websites are divided into tourists and ordinary users, as well as members, so tourists need to log in to browse some web pages to see, ordinary users need points to view some posts, and members are like open a green channel, what can access, this is the role of authority

Use BasicAuth authentication

BasicAuth is an open platform authentication method. Each access to the API carries the user’s username and password authentication, so BasicAuth encrypts the username and password. We can use the PostMan tool to test BasicAuth encryption. To test this, use the following code. Here, I use the Echo framework (gin, Iris, Beego, etc.) to set up a login interface. In the LOGIN interface, we get BasicAuth’s encrypted information from the request header (in the HTTP request, The authentication information for the request is placed in the Authorization of the request header.

// main.go
package main

import (
	"fmt"

	"github.com/labstack/echo"
)

func main(a) {
	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
}

Copy the code

Run this code to listen on port 1323, so you need to make a request in PostMan:

When we click Send, when we run go, we’ll print out the encrypted result of username and password

So how do we get the encrypted username and password? Echo framework is built with simple BasicAuth middleware, we can directly use the official example code:

// Official example
// The username and password are the same as the 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
}))
Copy the code

Modify our code:

// main.go
package main

import (
	"fmt"

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

func main(a) {
	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
}
Copy the code

So run the code again, listen on port 1323, and make another request with PostMan:

The result of this run is

We can see that the result of encryption is directly decrypted out, using the BasicAuth middleware, the decryption process does not need us to do, directly to the BasicAuth middleware. Since we return false in auth, the middleware will never be authenticated and will return a JSON message to PostMan:

{
  "message": "Unauthorized"
}
Copy the code

After that, we can use BasicAuth for authentication and change the auth code

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

Copy the code

Use BasicAuth + JWT authentication

In BasicAuth, the user’s username and password are passed only once when the user logs in. You can issue a token to the user after login (the token can write the user id and other things that are not easy to identify). BasicAuth then passes this token, and the user can use this token to access other interfaces. This way we avoid the risk of passing username and password. The jWt-go library is recommended here, and the specific JWT token generation code is as follows:


// extend the information we want to write token
type Claims struct {
	Uid int `json:"uid"` // Extend the id of the write user
	jwt.StandardClaims
}

func genToken(uid int) (string, error) {
	secretKey := "secretKey" // The encrypted key
	expiresIn := time.Duration(24 * 30) // Set the expiration time
	claims := Claims{
		Uid: uid,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(time.Hour.Truncate(expiresIn)).Unix(),
			Issuer:    "startdusk".// Signed user (custom name)
		},
	}
	tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Encrypt claims with SHA256
	token, err := tokenClaims.SignedString([]byte(secretKey)) // Generate a signature
	return token, err
}

Copy the code

Then our user request flow becomes:

According to the flow, we will change the login part of the code (login also changed to POST access mode) :



// Define a user structure to receive user data
type User struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

// Simulate the process of looking up the user ID from the database
func findUserIDFormDB(user *User) (int.bool) {
	ifuser.Username ! ="startdusk"|| user.Password ! ="root" {
		return 0.false
	}

	return 1.true // For simplicity, set the id of startDusk to 1
}

func login(c echo.Context) error {
	var u User
	err := c.Bind(&u)
	iferr ! =nil {
		return err
	}
	id, ok := findUserIDFormDB(&u)
	if! ok {return errors.New("There is no such user")
	}
	token, err := genToken(id)
	iferr ! =nil {
		return err
	}
	return c.JSON(http.StatusOK, map[string]string{
		"token": token,
	})
}
Copy the code

Then we define another interface for token access other:

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

BasicAuth: BasicAuth: BasicAuth: BasicAuth: BasicAuth: BasicAuth: BasicAuth: BasicAuth: BasicAuth: BasicAuth: BasicAuth: BasicAuth: BasicAuth Password does not need to pass parameters, so the auth part of the code is changed to:

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
		})
	iferr ! =nil {
		return false, err
	}
	uid := getIntFromClaims("uid", token.Claims)
	ifuid ! =1 { // The default id is 1 when the token is generated
		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 implements request.Extractor interface (jwT-go request)
func (e basicAuthExtractor) ExtractToken(*http.Request) (string, error) {
	return e.content, nil
}
Copy the code

The modified code is complete as follows:

// 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(a) {
	e := echo.New()
	e.POST("/login", login)
	// Set BasicAuth to be used only on the /test path
	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
		})
	iferr ! =nil {
		return false, err
	}
	uid := getIntFromClaims("uid", token.Claims)
	ifuid ! =1 { // The default id is 1 when the token is generated
		return false.nil
	}
	return true.nil
}

// Parse the JWT token method
func getIntFromClaims(key string, claims jwt.Claims) int {
	s := getStringFromClaims(key, claims)
	value, err := strconv.Atoi(s)
	iferr ! =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 implements request.Extractor interface (jwT-go request)
func (e basicAuthExtractor) ExtractToken(*http.Request) (string, error) {
	return e.content, nil
}

// Define a user structure to receive user data
type User struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

// Simulate the process of looking up the user ID from the database
func findUserIDFormDB(user *User) (int.bool) {
	ifuser.Username ! ="startdusk"|| user.Password ! ="root" {
		return 0.false
	}

	return 1.true // For simplicity, set the id of startDusk to 1
}

func login(c echo.Context) error {
	var u User
	err := c.Bind(&u)
	iferr ! =nil {
		return err
	}
	id, ok := findUserIDFormDB(&u)
	if! ok {return errors.New("There is no such user")
	}
	token, err := genToken(id)
	iferr ! =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"})},// extend the information we want to write 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
}

Copy the code

So, go here, run the code, and let’s test the process: First login to get the token

Test results:

We successfully accessed the other interface using token. Of course, we also tested the following counter example:

  • 1. If no token is transmitted: Returns
{
  "message": "Unauthorized"
}
Copy the code
  • 2. If an incorrect token is passed: return
{
  "message": "Internal Server Error"
}
Copy the code
  • 3. If the token expires: Returns
{
  "message": "Internal Server Error"
}
Copy the code

It turns out that it’s correct, of course it’s not a very good error, so you need to do some error handling to make it a little bit friendlier, and error handling is a very important thing in Go, so I’m not going to do it here, but if you’re interested, you can try to do it.

Use scope to distinguish permissions

For permission, we can deal with it in a simpler way. Here we use scope to deal with permissions, that is, the range of numbers to deal with permissions, for example: Add a scope field to the user, which is a number, then there is also a scope on the other interface. As long as the scope of the user is larger than the number of the scope on the other interface, then the user can access this interface, otherwise there is no access.

Then, this scope can be written to the user’s database, and then the user login uses to obtain the ID and the user’s scope, and writes the scope into the user’s token when the user’s token is generated. Then three codes need to be modified:

    1. The process of finding the user ID and scope
// Simulate the process of looking up the user id and scope from the database
func findUserIDFormDB(user *User) (int.int.bool) {
    ifuser.Username ! ="startdusk"|| user.Password ! ="root" {
      return 0.0.false
    }

    return 1.1.true // For simplicity, set the id of startDusk to 1 and scope to 1
}
Copy the code
    1. Add scope to token generation process
// extend the information we want to write 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
}
Copy the code
  • 3. Modify auth middleware
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
        })
    iferr ! =nil {
        return false, err
    }
    uid := getIntFromClaims("uid", token.Claims)
    ifuid ! =1 { // The default id is 1 when the token is generated
        return false.nil
    }
    // Check whether the user has permission and whether the scope is greater than 8
    scope := getIntFromClaims("scope", token.Claims)
    if scope < 8 {
        return false.nil
    }
    return true.nil
}
Copy the code

After modifying the code, let’s try: First login to obtain the token

We then use this token to access the other interface:

// Simulate the process of looking up the user id and scope from the database
func findUserIDFormDB(user *User) (int.int.bool) {
    ifuser.Username ! ="startdusk"|| user.Password ! ="root" {
      return 0.0.false
    }

    return 1.9.true // For simplicity, set the id of startDusk to 1 and scope to 9
}
Copy the code

Retest: login to login to obtain the token

Complete code:

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(a) {
	e := echo.New()
	e.POST("/login", login)
	// Set BasicAuth to be used only on the /test path
	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
		})
	iferr ! =nil {
		return false, err
	}
	uid := getIntFromClaims("uid", token.Claims)
	ifuid ! =1 { // The default id is 1 when the token is generated
		return false.nil
	}
	// Check whether the user has permission and whether the scope is greater than 8
	scope := getIntFromClaims("scope", token.Claims)
	if scope < 8 {
		return false.nil
	}
	return true.nil
}

// Parse the JWT token method
func getIntFromClaims(key string, claims jwt.Claims) int {
	s := getStringFromClaims(key, claims)
	value, err := strconv.Atoi(s)
	iferr ! =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 implements request.Extractor interface (jwT-go request)
func (e basicAuthExtractor) ExtractToken(*http.Request) (string, error) {
	return e.content, nil
}

// Define a user structure to receive user data
type User struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

// Simulate the process of looking up the user id and scope from the database
func findUserIDFormDB(user *User) (int.int.bool) {
	ifuser.Username ! ="startdusk"|| user.Password ! ="root" {
		return 0.0.false
	}

	return 1.9.true // For simplicity, set the id of startDusk to 1 and scope to 9
}

func login(c echo.Context) error {
	var u User
	err := c.Bind(&u)
	iferr ! =nil {
		return err
	}
	id, scope, ok := findUserIDFormDB(&u)
	if! ok {return errors.New("There is no such user")
	}
	token, err := genToken(id, scope)
	iferr ! =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"})},// extend the information we want to write 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
}

Copy the code