3 GO语言鉴权与操作数据库
Posted 行走的皮卡丘
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3 GO语言鉴权与操作数据库相关的知识,希望对你有一定的参考价值。
3 GO语言鉴权与操作数据库
1 什么是鉴权
在网站中,有些页面是登录后的用户才能访问的,由于http是无状态的协议,我们无法确认用户的状态(如是否登录)。这时候浏览器在访问这些页面时,需要额外传输一些用户的账户信息给后台,让后台知道该用户是否登录、是哪个用户在访问。
2 cookie
cookie是浏览器实现的技术,在浏览器中可以存储用户是否登录的凭证,每次请求都会将该凭证发送给服务器。
cookie实现鉴权步骤:
- 用户登录成功后,后端向浏览器设置一个
cooki
e: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标准,而有些是采用了自己的实现接口。
推荐使用:
- https://github.com/go-sql-driver/mysql (Go编写,维护频繁,支持Gosql接口,底层支持keepalive)
- https://github.com/jmoiron/sqlx(推荐)
代码示例:
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语言鉴权与操作数据库的主要内容,如果未能解决你的问题,请参考以下文章