什么是JWT?
JWT是JSON Web Token的缩写,定义了一种简介自暴寒的方法用于通信双方之间以Json对象的形式安全的传递信息。因为特定的数字签名,所以这些通信的信息能够被校验和信任。 JWT可以使用HMAC算法或者RSA的公钥私钥对进行签名。
让我们进一步的解释下关于JWT的定义:
- 简约(Compact): JWT通信中使用的数据量比较小,JWT可以通过URL、POST参数,或者直接在HTTP header进行传递。 并且,因为比较小的数据量,这也意味着传输速度会更加快速。
- 自包含(Self-contained): 负载(payload)中(可以)包含所需的所有用户部分的信息,可以避免对服务端数据库的多次查询。
JWT组成
JWT由三部分组成,使用.
分隔:
- 头部(Header)
- 载荷(Payload)
- 签名(Signature)
所以,JWT最终样式为:XXXXX.XXXXX.XXXXX
Header
JWT的头部包含描述该JWT的最基本的信息:token的类型, 如JWT, 以及token使用的加密算法, 如 HMAC SHA256或者RSA.。可以被表示为一个JSON对象:
{
"typ": "JWT",
"alg": "HS256"
}
Payload
JWT token的第二部分是payload, Payload包含claims. Claims是一些实体(通常指用户)的状态信息和其他元数据。
{
"iss": "sysu",
"iat": 1233458243,
"exp": 1448333419,
"aud": "sysu-ss",
"sub": "sysu-ss",
}
iss(issuer 签发者),是否使用是可选的;
iat(issued at签发时间),这里是一个Unix时间戳,是否使用是可选的;
exp(expiration time 过期时间) ,这里是一个Unix时间戳,是否使用是可选的;
aud(audience 接收方 ),是否使用是可选的;
sub(subject 面向的用户),是否使用是可选的;
关于更多有关Payload字段的介绍,请查看JWT官网
Signature
要创建签名部分,需要使用经过编码后的头部(header)和负载(payload)以及一个密钥,将header和payload用.
连接起来后,使用header中制定的算法进行签名。
该签名是用户验证JWT的请求发送者以及确保数据信息在传输过程中的消息是未经篡改的。
JWT使用(golang)
我使用的是jwt-go、negroni web中间件以及mux搭建服务。关于这三个库的使用请自行谷歌。
- 首先定义自己的Token:
// service/jwt.go
var tokens []Token // 作为全局变量存储登录用户的token,可以存储到数据库中
const TokenName = "SW-TOKEN"
const Issuer = "Go-GraphQL-Group"
const SecretKey = "StarWars"
type Token struct {
SW_TOKEN string `json:"SW-TOKEN"`
}
type jwtCustomClaims struct {
jwt.StandardClaims
Admin bool `json:"admin"`
}
func CreateToken(secretKey []byte, issuer string, isAdmin bool) (token Token, err error) {
claims := &jwtCustomClaims{
jwt.StandardClaims{
ExpiresAt: int64(time.Now().Add(time.Hour * 1).Unix()),
Issuer: issuer,
},
isAdmin,
}
tokenStr, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secretKey)
token = Token{
tokenStr,
}
return
}
func ParseToken(tokenStr string, secretKey []byte) (claims jwt.Claims, err error) {
var token *jwt.Token
token, err = jwt.Parse(tokenStr, func(*jwt.Token) (interface{}, error) {
return secretKey, nil
})
claims = token.Claims
return
}
- 之后生成服务并注册路由:
// main.go
func NewServer() *negroni.Negroni {
router := mux.NewRouter()
initRoutes(router)
n := negroni.Classic() // negroni.Classic() 返回带有默认中间件的Negroni实例指针
n.UseHandler(router) // 将router中http.Handler加入到negroni的中间件栈中
return n
}
func initRoutes(router *mux.Router) {
router.HandleFunc("/login", service.LoginHandler).Methods("POST")
// 使用中间件进行token认证
router.Use(service.TokenMiddleware)
// query服务
router.HandleFunc("/query", handler.GraphQL(GraphQL_Service.NewExecutableSchema(GraphQL_Service.Config{Resolvers: &GraphQL_Service.Resolver{}})))
// 退出路由
router.HandleFunc("/logout", service.LogoutHandler).Methods("POST", "GET")
}
- login路由处理函数签发Token:
func LoginHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
fmt.Println(r.Form.Get("username"))
if err != nil {
w.WriteHeader(http.StatusForbidden)
fmt.Fprint(w, "Error in request")
return
}
// 使用固定的username和password做测试
if strings.ToLower(r.Form.Get("username")) != "admin" || r.Form.Get("password") != "password" {
w.WriteHeader(http.StatusForbidden)
fmt.Println("Error logging in")
fmt.Fprint(w, "Invalid credentials")
return
}
token, err := CreateToken([]byte(SecretKey), Issuer, false)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Error extracting the key")
log.Fatal(err)
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
tokenBytes, err := json.Marshal(token)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Error marshal the token")
log.Fatal(err)
}
tokens = append(tokens, token)
w.Write(tokenBytes)
}
- 中间件定义如下:
func TokenMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 不是login的请求需要进行token认证
if r.RequestURI[1:] != "login" {
/*
// token位于Authorization中,用此方法
token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) {
return []byte(SecretKey), nil
})
*/
tokenStr := ""
for k, v := range r.Header {
if strings.ToUpper(k) == TokenName {
tokenStr = v[0]
break
}
}
validToken := false
for _, token := range tokens {
if token.SW_TOKEN == tokenStr {
validToken = true
}
}
if validToken {
ctx := context.WithValue(r.Context(), TokenName, tokenStr)
next.ServeHTTP(w, r.WithContext(ctx))
} else {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized access to this resource"))
//fmt.Fprint(w, "Unauthorized access to this resource")
}
} else {
next.ServeHTTP(w, r)
}
})
}
- logout路由处理函数(删除token):
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
tokenStr := ""
for k, v := range r.Header {
if strings.ToUpper(k) == TokenName {
tokenStr = v[0]
break
}
}
for i, token := range tokens {
if token.SW_TOKEN == tokenStr {
tokens = append(tokens[:i], tokens[i+1:]...)
break
}
}
w.Write([]byte("logout"))
}
- 启动服务
// main.go
func main() {
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
server := NewServer()
server.Run(":" + port)
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
}
- 测试服务
- 未登录,query
- 登录login
- query
- 登出logout
- query
代码源地址:Github