认证和授权是大部分的服务端系统都会涉及到的一个功能模块,特别是在一些大型的企业中,由于系统的数量众多,不可能登录任何一个系统都需要相关人员逐个的输入系统的用户名和密码,在这样的情形下,如何实现一次登录,多系统自动登录或者一次授权后续自动登录的问题成为一个迫切需要解决的问题。
在实战实现jwt授权方式之前,我们需要理解基于jwt的token认证机制,jwt机制生成的token由三部分组成:头部(header是生成签名的算法);有效载荷(payload包含一些特定的信息,如用户名或者token有效期等);签名(signature通过将前两部分的内容与一个秘钥合并和散列而生成)。
jwt格式:header.payload.signature
package main
import (
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go"
"log"
"net/http"
"time"
)
//设置jwt的认证签名部分的key
var jwtKey = []byte("my_secret_key")
//模拟用户数据(一般从MySQL中读取)
var users = map[string]string {
"user1": "password1",
"user2": "password2",
}
//请求认证时的对应结构体
type Credentials struct {
Password string `json:"password"`
Username string `json:"username"`
}
//加密成jwt对应结构体
type Claims struct {
Username string `json:"username"`
jwt.StandardClaims
}
//登录校验
func Login(w http.ResponseWriter, r *http.Request) {
var creds Credentials
//解析请求的凭证是否合法
err := json.NewDecoder(r.Body).Decode(&creds)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
//数据库存储的合法用户
expectedPassword, ok := users[creds.Username]
if !ok || expectedPassword != creds.Password {
w.WriteHeader(http.StatusBadRequest)
return
}
//认证通过的情况,刷新token有效期
expirationTime := time.Now().Add(time.Minute * 5)
//将相关信息写入jwt认证结构体中
claims := Claims{
Username: creds.Username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
//生成token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
//将生成的token存入客户端(cookie或者localStore)
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: tokenString,
Expires: expirationTime,
})
}
//后续请求token校验
func Verify (w http.ResponseWriter, r *http.Request) {
//从cookie中获取token
c, err := r.Cookie("token")
if err != nil {
//cookie不存在的情况
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusUnauthorized)
return
}
//其他情况
w.WriteHeader(http.StatusBadRequest)
return
}
//获取cookie中的token值
tknStr := c.Value
claims := &Claims{}
//校验从cookie中获取的token是否发生变化
tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
if !tkn.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Write([]byte(fmt.Sprintf("Welcome %s!", claims.Username)))
}
//刷新token的有效期
func Refresh (w http.ResponseWriter, r *http.Request) {
//获取cookie中的token
c, err := r.Cookie("token")
if err != nil {
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
//构建token
tknStr := c.Value
claims := &Claims{}
tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
if !tkn.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
//token认证通过, 并且token有效期少于30秒才会刷新token
if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) > 30*time.Second {
w.WriteHeader(http.StatusBadRequest)
return
}
expirationTime := time.Now().Add(time.Minute * 5)
claims.ExpiresAt = expirationTime.Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString(jwtKey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
//更新cookie的有效期时间
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: tokenStr,
Expires: expirationTime,
})
}
func main() {
http.HandleFunc("/login", Login)
http.HandleFunc("/verify", Verify)
http.HandleFunc("/refresh", Refresh)
log.Fatal(http.ListenAndServe(":9000", nil))
}
在实现oauth2之前,我们同样需要理解oauth2的相关概念:资源所有者(resource owner)、资源服务器(resource server)、客户端(client)、authorization server(认证服务器)、用户代理(user-agent)。
资源所有者:资源的拥有者
资源服务器:服务提供商存放用户资源的服务器
客户端:需要得到资源的用户程序
认证服务器:服务商专门用来处理认证的服务器
用户代理:用户访问客户端的程序,例如浏览器
登录流程:
1、用户在第三方应用上点击登录,应用向认证服务器发送请求,说有用户希望进行授权操作,同时说明自己是谁、用户授权完成后的回调url
2、认证服务器展示给用户自己的授权界面
3、用户进行授权操作,认证服务器验证成功后,生成一个授权编码code,并跳转到第三方的回调url
4、第三方应用拿到code后,连同自己在平台上的身份信息(ID密码)发送给认证服务器,再一次进行验证请求,说明自己的身份正确,并且用户也已经授权我了,来换取访问用户资源的权限
5、认证服务器对请求信息进行验证,如果没问题,就生成访问资源服务器的令牌access_token,交给第三方应用
6、第三方应用使用access_token向资源服务器请求资源
7、资源服务器验证access_token成功后返回响应资源
以下展示一个通过github授权的oauth2案例
前端登录授权页面
index
Login with github
服务端代码处理逻辑
//oauth2.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
//认证中心的注册信息
//github注册应用的地址:https://github.com/settings/applications/new
const (
clientID = "xxx"
clientSecret = "yyy"
)
var httpClient = http.Client{}
type OAuthAccessResponse struct {
AccessToken string `json:"access_token"`
}
func HandleOauthRedirect(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
log.Printf("could not parse query: %v", err)
w.WriteHeader(http.StatusBadRequest)
}
code := r.FormValue("code")
//通过clientID、clientSecret、code获取授权秘钥
reqURL := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", clientID, clientSecret, code)
req, err := http.NewRequest(http.MethodPost, reqURL, nil)
if err != nil {
log.Printf("could not create HTTP request: %v", err)
w.WriteHeader(http.StatusBadRequest)
}
//设置返回的格式为json格式
req.Header.Set("accept", "application/json")
//发送http请求
res, err := httpClient.Do(req)
if err != nil {
log.Printf("could not send HTTP request: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
defer res.Body.Close()
//解析结果
var t OAuthAccessResponse
if err := json.NewDecoder(res.Body).Decode(&t); err != nil {
log.Printf("could not parse JSON response: %v", err)
w.WriteHeader(http.StatusBadRequest)
}
w.Header().Set("Location", "/welcome.html?access_token="+t.AccessToken)
w.WriteHeader(http.StatusFound)
}
func main() {
fs := http.FileServer(http.Dir("./jwt"))
http.Handle("/", fs)
http.HandleFunc("/oauth/redirect", HandleOauthRedirect)
http.ListenAndServe(":8080", nil)
}
认证通过后数据请求处理页面
welcome
这篇文章主要讲解了基于go的JWT、OAuth2授权方案的实现, 只要理解了相关的概念,实现起来应该不是很困难,希望能够帮助到需要的人。
参考资料1:https://blog.csdn.net/neweastsun/article/details/105919915
参考资料2:https://razeencheng.com/post/oauth2-protocol-details.html