3 GO语言鉴权与操作数据库

Posted 行走的皮卡丘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3 GO语言鉴权与操作数据库相关的知识,希望对你有一定的参考价值。


3 GO语言鉴权与操作数据库

1 什么是鉴权

在网站中,有些页面是登录后的用户才能访问的,由于http是无状态的协议,我们无法确认用户的状态(如是否登录)。这时候浏览器在访问这些页面时,需要额外传输一些用户的账户信息给后台,让后台知道该用户是否登录、是哪个用户在访问。

2 cookie

cookie是浏览器实现的技术,在浏览器中可以存储用户是否登录的凭证,每次请求都会将该凭证发送给服务器。

cookie实现鉴权步骤

  • 用户登录成功后,后端向浏览器设置一个cookie:username=lisi
  • 每次请求,浏览器会自动把该cookie发送给服务端
  • 服务端处理请求时,从cookie中取出username,就知道是哪个用户了
  • 如果没过期,则鉴权通过,过期了,则重定向到登录页

Go中使用Cookie:

// 登录时设置cookie
expiration := time.Now()
expiration = expiration.AddDate(1, 0, 0)
cookie := http.CookieName: "username", Value: "张三", Expires: expiration
http.SetCookie(w, &cookie)

// 再次访问时,获取浏览器传递的cookie
// 获取cookie方式一
username, _ := r.Cookie("username")
// 获取cookie方式二
for _, cookie := range r.Cookies() 
	fmt.Println(cookie.Username)

但是这样做,风险很大,黑客很容易知道cookie中传递的内容,即用户的真实账户信息。

3 session

3.1 session原理

为了解决cookie的安全问题,基于cookie,衍生了session技术。session技术将用户的信息存储在了服务端,保证了安全,其实现步骤为:

  • 服务端设置cookie时,不再存储username,而是存储一个随机生成的字符串,比如32位的uuid,服务端额外存储一个uuid与用户名的映射
  • 用户再次请求时,会自动把cookie中的uuid带入给服务器
  • 服务器使用uuid进行鉴权

一般上述的uuid在cookie中存储的键都是sid(session_id),也就是常说的session方案,服务端此时需要额外开辟空间存储sid与用户真实信息的对应映射。

3.2 session实现

如果要手动实现session,需要注意以下方面:

  • 全局session管理器:
  • 保证sessionid 的全局唯一性
  • 为每个客户关联一个session
  • session 过期处理
  • session 的存储(可以存储到内存、文件、数据库等)

关于session数据(sid与真实用户的映射)的存储,可以存放在服务端的一个文件中,比如该session第三方库:https://github.com/gorilla/sessions

使用示例:

package main

import(
	"fmt"
    "net/http"
    "github.com/gorilla/sessions"
)

// 利用cookie方式创建session,秘钥为 mykey
var store = sessions.NewCookieStore([]byte("mykey"))

func setSession(w http.ResponseWriter, r *http.Request)
    session, _ := store.Get(r, "sid")
    session.Values["username"] = "张三"
    session.Save(r, w)


func profile(w http.ResponseWriter, r *http.Request)

    session, _ := store.Get(r, "sid")

    if session.Values["username"] == nil 
        fmt.Fprintf(w, `未登录,请前往 localhost:8080/setSession`)
        return
    

    fmt.Fprintf(w, `已登录,用户是:%s`, session.Values["username"])
    return


func main() 

    // 访问隐私页面
    http.HandleFunc("/profile", profile)

    // 设置session
    http.HandleFunc("/setSession", setSession)

    server := http.Server
        Addr: ":8080",
    
	server.ListenAndServe()


在企业级开发中,经常使用额外的数据库redis来存储session数据。

2.3 禁用cookie时候session方案

以上方式中,生成的sid都存储在cookie中,如果用户禁用了cookie,则每次请求服务端无法收到sid!我们需要想别的办法来让浏览器的每次请求都携带上sid,常用方式是URL重写:在返回给用户的页面里的所有的URL后面追加session标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。

4 JWT

4.1 jwt介绍

session将数据存储在了服务端,无端造成了服务端空间浪费,可否像cookie那样将用户数据存储在客户端,而不被黑客破解到呢?

JWT是json web token缩写,它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。
JWT和session有所不同,session需要在服务器端生成,服务器保存session,只返回给客户端sessionid,客户端下次请求时带上sessionid即可。因session是储存在服务器中,有多台服务器时会出现一些麻烦,需要同步多台主机的信息,不然会出现在请求A服务器时能获取信息,但是请求B服务器身份信息无法通过。JWT能很好的解决这个问题,服务器端不用保存jwt,只需要保存加密用的secret,在用户登录时将jwt加密生成并发送给客户端,由客户端存储,以后客户端的请求带上,由服务器解析jwt并验证。这样服务器不用浪费空间去存储登录信息,也不用浪费时间去做同步。

4.2 jwt构成

一个 JWT token包含3部分:

  • header: 告诉我们使用的算法和 token 类型
  • Payload: 必须使用 sub key 来指定用户 ID, 还可以包括其他信息比如 email, username 等.
  • Signature: 用来保证 JWT 的真实性. 可以使用不同算法

header:

// base64编码的字符串`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9`
// 这里规定了加密算法,hash256

  "alg": "HS256",
  "typ": "JWT"

payload


  "sub": "1234567890",
  "name": "John Doe",
  "admin": true

// base64编码的字符串`eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9`

这里的内容没有强制要求,因为 Payload 就是为了承载内容而存在的,不过想用规范的话也可以参考下面的:

* iss: jwt签发者
* sub: jwt所面向的用户
* aud: 接收jwt的一方
* exp: jwt的过期时间,这个过期时间必须要大于签发时间
* nbf: 定义在什么时间之前,该jwt都是不可用的.
* iat: jwt的签发时间
* jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

signature是用 header + payload + secret组合起来加密的,公式是:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

这里 secret就是自己定义的一个随机字符串,这一个过程只能发生在 server 端,会随机生成一个 hash 值,这样组合起来之后就是一个完整的 jwt 了:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.4c9540f793ab33b13670169bdf444c1eb1c37047f18e861981e14e34587b1e04

4.3 jwt执行流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ky4Qg2Li-1651328587727)(…/images/go/jwt.jpg)]

4.4 Go使用jwt

整体操作步骤:

  • 1.从request获取tokenstring
  • 2.将tokenstring转化为未解密的token对象
  • 3.将未解密的token对象解密得到解密后的token对象
  • 4.从解密后的token对象里取参数

使用第三方包:go get github.com/dgrijalva/jwt-go 示例:

// 生成Token:
// SecretKey 是一个 const 常量
func CreateToken(SecretKey []byte, issuer string, Uid uint, isAdmin bool) (tokenString string, err error)   
    claims := &jwtCustomClaims  
       jwt.StandardClaims
           ExpiresAt: int64(time.Now().Add(time.Hour * 72).Unix()),  
           Issuer:    issuer,  
       ,  
       Uid,  
       isAdmin,  
     
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)  
   tokenString, err = token.SignedString(SecretKey)  
   return  


// 解析Token
func ParseToken(tokenSrt string, SecretKey []byte) (claims jwt.Claims, err error)   
   var token *jwt.Token  
   token, err = jwt.Parse(tokenSrt, func(*jwt.Token) (interface, error)   
      return SecretKey, nil  
   )  
   claims = token.Claims  
   return  

5 Go数据库接口

Go官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动,这样做的好处是:框架迁移极其方便。

Go数据库标准包位于以下两个包中:

  • database/sql:提供了保证SQL或类SQL数据库的泛用接口
  • database/sql/driver:定义了应被数据库驱动实现的接口,这些接口会被sql包使用

6 Go操作mysql

Go中支持MySQL的驱动目前比较多,有如下几种,有些是支持database/sql标准,而有些是采用了自己的实现接口。

推荐使用

代码示例:

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

func main() 
	
	db, err := sql.Open("mysql", "root:123456@/mydata?charset=utf8")
	checkErr(err)

	//插入数据
	sql1 := "INSERT INTO user SET name=?,age=?"
	stmt, err := db.Prepare(sql1)
	checkErr(err)
	res, err := stmt.Exec("zs", "30",)
	checkErr(err)

	id, err := res.LastInsertId()
	checkErr(err)
	fmt.Println("插入id=", id)


	//更新数据
	sql2 := "UPDATE user SET name=? WHERE id=?";
	stmt, err = db.Prepare(sql2)
	checkErr(err)
	res, err = stmt.Exec("lisi", id)
	checkErr(err)
	affect, err := res.RowsAffected()
	checkErr(err)
	fmt.Println("更新行数=", affect)

	//查询数据
	rows, err := db.Query("SELECT * FROM user")
	checkErr(err)
	for rows.Next() 
		var id int
		var name string
		var age int
		err = rows.Scan(&id, &name, &age)
		checkErr(err)
		fmt.Println("id=", id)
		fmt.Println("name=", name)
		fmt.Println("age=", age)
	

	//删除数据
	// stmt, err = db.Prepare("DELETE FROM user WHERE uid=?")
	// checkErr(err)
	// res, err = stmt.Exec(id)
	// checkErr(err)
	// affect, err = res.RowsAffected()
	// checkErr(err)
	// fmt.Println(affect)

	db.Close()



func checkErr(err error) 
	if err != nil 
		panic(err)
	

相关API:

  • sql.Open():打开一个注册过的数据库驱动,第二个参数格式有:
    • user@unix(/path/to/socket)/dbname?charset=utf8
    • user:password@tcp(localhost:5555)/dbname?charset=utf8
    • user:password@/dbname
    • user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname
  • db.Prepare():返回准备要执行的sql操作,然后返回准备完毕的执行状态
  • db.Query():直接执行Sql返回Rows结果
  • stmt.Exec():执行stmt准备好的SQL语句

7 Go操作redis

7.1 Go操作Redis

推荐驱动:

  • https://github.com/go-redis/redis
  • https://github.com/gomodule/redigo

基本操作:

package main

import (
	"fmt"
	"go get github.com/gomodule/redigo/redis"
)


func main() 

	c, err := redis.Dial("tcp", "localhost:6379")

	if err != nil 
		fmt.Println("Redis connect err:", err)
		return
	

	defer c.Close()

	_, err = c.Do("Set", "first", "hello")
	if err != nil 
		fmt.Println(err)
		return
	

	r, err := redis.Int(c.Do("Get", "first"))
	if err != nil 
		fmt.Println(err)
		return
	

	fmt.Println(r)
	

7.2 连接池

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
	"go get github.com/gomodule/redigo/redis"
)

var (
	Pool *redis.Pool
)

func init() 
	redisHost := ":6379"
	Pool = newPool(redisHost)
	close()


func newPool(server string) *redis.Pool 

	return &redis.Pool

		MaxIdle:     3,
		IdleTimeout: 240 * time.Second,

		Dial: func() (redis.Conn, error) 
			c, err := redis.Dial("tcp", server)
			if err != nil 
				return nil, err
			
			return c, err
		,

		TestOnBorrow: func(c redis.Conn, t time.Time) error 
			_, err := c.Do("PING")
			return err
		
	


func close() 
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	signal.Notify(c, syscall.SIGTERM)
	signal.Notify(c, syscall.SIGKILL)
	go func() 
		<-c
		Pool.Close()
		os.Exit(0)
	()


func Get(key string) ([]byte, error) 

	conn := Pool.Get()
	defer conn.Close()

	var data []byte
	data, err := redis.Bytes(conn.Do("GET", key))
	if err != nil 
		return data, fmt.Errorf("error get key %s: %v", key, err)
	
	return data, err


func main() 
	test, err := Get("test")
	fmt.Println(test, err)


func ExampleNewClient() 
	client := redis.NewClient(&redis.Options
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	)

	pong, err := client.Ping().Result()
	fmt.Println(pong, err)
	// Output: PONG <nil>


func ExampleClient() 
	err := client.Set("key", "value", 0).Err()
	if err != nil 
		panic(err)
	

	val, err := client.Get("key").Result()
	if err != nil 
		panic(err)
	
	fmt.Println("key", val)

	val2, err := client.Get("key2").Result()
	if err == redis.Nil 
		fmt.Println("key2 does not exist")
	 else if err != nil 
		panic(err)
	 else 
		fmt.Println("key2", val2)
	
	// Output: key value
	// key2 does not exist
)

Go操作MongoDB

MongoDB为Go提供了官方驱动:

https://github.com/mongodb/mongo-go-driver

以上是关于3 GO语言鉴权与操作数据库的主要内容,如果未能解决你的问题,请参考以下文章

3 GO语言鉴权与操作数据库

3 GO语言鉴权与操作数据库

鉴权与OAuth2.0插件功能介绍

认证鉴权与API权限控制在微服务架构中的设计与实现

认证鉴权与API权限控制在微服务架构中的设计与实现

认证鉴权与API权限控制在微服务架构中的设计与实现