1 什么是鉴权

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

2 cookie

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

cookie实现鉴权步骤:

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

Go中使用Cookie:

// 登录时设置cookie
expiration := time.Now()
expiration = expiration.AddDate(1, 0, 0)
cookie := http.Cookie{Name: "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数据。

以上方式中,生成的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