今天给之前的demo增加登录验证.
现在验证流行使用JWT(JSON web tokens),我们也选择用github.com/dgrijalva/jwt-go.
还是从models开始,增加user.go,内容如下:
package models
import "errors"
type User struct {
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
var accountsMock = []User{
User{
Id: 1,
Username: "admin",
Password: "1234",
},
User{
Id: 2,
Username: "guest",
Password: "5678",
},
}
// Get User Info
func (u *User) GetUserByID(id int) (*User, error) {
for _, user := range accountsMock {
if user.Id == id {
return &user, nil
}
}
return nil, errors.New("User not found")
}
// 校验用户名与密码
func (u *User) Authenticate() (*User, error) {
for _, user := range accountsMock {
if user.Username == u.Username && user.Password == u.Password {
return &user, nil
}
}
return nil, errors.New("User not found or password is wrong")
}
新建utils目录,增加jwt.go文件,里面的三个函数一个是生成token,一个是解析token,最后一个是验证token的有效性:
package utils
import (
"errors"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
var jwtSecret = []byte("graphqldemo")
type Claims struct {
Username string `json:"username"`
Password string `json:"password"`
jwt.StandardClaims
}
func GenerateToken(username, password string) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(168 * time.Hour) // 有效期一周
claims := Claims{
username,
password,
jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
Issuer: "demo",
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(jwtSecret)
return token, err
}
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
func ValidateJWT(token string) error {
if token == "" {
return errors.New("Authorization token must be present")
}
claims, err := ParseToken(token)
if err != nil {
return err
} else if time.Now().Unix() > claims.ExpiresAt {
return errors.New("Error. Token is expired")
}
return nil
}
在utils目录下,还要新建一个文件auth.go,主要给login的handler func:
package utils
import (
"encoding/json"
"graphqldemo/models"
"log"
"net/http"
)
// login handler
func CreateTokenEndpoint(response http.ResponseWriter, request *http.Request) {
var user models.User
_ = json.NewDecoder(request.Body).Decode(&user)
if _, err := user.Authenticate(); err != nil {
log.Println(err)
response.WriteHeader(http.StatusUnauthorized)
result := `{
"code": 401,
"msg": "未授权的访问",
}`
response.Write([]byte(result))
return
}
token, err := GenerateToken(user.Username, user.Password)
if err != nil {
log.Println(err)
}
response.Header().Set("content-type", "application/json")
response.Header().Set("token", token)
response.Write([]byte(`{ "token": "` + token + `" }`))
}
"_ = json.NewDecoder(request.Body).Decode(&user)" 表明我们传递用户名和密码到后端的时候,Content-Type:application/json, 如果是在postman里面,应该要按如下的格式:
response.Header().Set("token", token)
response.Write([]byte(`{ "token": "` + token + `" }`))
这两句表示通过验证后,可以在header和body里看到token的内容.如上图所示.
然后我们要对schema做一些修改,首先增加schema.go文件,把以下内容从article.go中转移到这里,这样当我们新建user.go后,修改或者增加schema.go里的query和mutation部分的"Fields: graphql.Fields{}"
package schema
import "github.com/graphql-go/graphql"
// query
// 定义根查询节点及各种查询
var rootQuery = graphql.NewObject(graphql.ObjectConfig{
Name: "RootQuery",
Description: "Root Query",
Fields: graphql.Fields{
"articles": &queryArticles,
"article": &queryArticle,
},
})
// mutation
// 定义增删改方法
var mutationType = graphql.NewObject(graphql.ObjectConfig{
Name: "mutation",
Description: "增删改",
Fields: graphql.Fields{
"add": &addArticle,
"update": &updateArticle,
"delete": &deleteArticle,
},
})
// 定义Schema用于http handler处理
var Schema, _ = graphql.NewSchema(graphql.SchemaConfig{
Query: rootQuery,
Mutation: mutationType,
})
我们修改schema/article.go文件,拿查看单篇文章详情来做测试:
// 查询单篇文章
var queryArticle = graphql.Field{
Name: "QueryArticle",
Description: "Query Article",
Type: articleType,
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.Int,
},
},
// Resolve是一个处理请求的函数,具体处理逻辑可在此进行
Resolve: func(p graphql.ResolveParams) (result interface{}, err error) {
// 在这里获取token并验证
err = utils.ValidateJWT(p.Context.Value("token").(string))
if err != nil {
return nil, err
}
id, ok := p.Args["id"].(int)
if !ok {
return nil, errors.New("missing required arguments: id. ")
}
result, err = models.GetArticleByID(id)
return result, err
},
}
我们在Resolve里增加了这么一段:
err = utils.ValidateJWT(p.Context.Value("token").(string))
if err != nil {
return nil, err
}
最最关键的是,p.Context.Value("token")的数据怎么传进来呢? 我在这折腾了好久.
最后修改main.go.
- 增加login的路由和handler
- 修改graphql的handler. 因为要传token,之前处理跨域的方式行不通了,所以改成如下的形式.
- 初始化handler部分,改成
GraphiQL: false,
Playground: true,
主要是因为GraphiQL不知道怎么设置request header,所以改成用Playground. 必须要有Playground或者GraphiQL, 否则之前说的采用graphql的优势之一:接口文档就没有地方查看了. 平时测试接口,还是习惯使用postman.
package main
import (
"context"
"graphqldemo/schema"
"graphqldemo/utils"
"log"
"net/http"
"github.com/graphql-go/handler"
)
// main
func main() {
h := Register()
// 跨域
c := cors.New(cors.Options{
AllowedOrigins: []string{"http://localhost:8080"},
AllowedHeaders: []string{"*"},
AllowCredentials: true,
// Debug: true,
})
http.Handle("/graphql", c.Handler(h))
http.HandleFunc("/login", utils.CreateTokenEndpoint)
log.Println("Now server is running on port 9090")
log.Fatal(http.ListenAndServe(":9090", nil))
}
// 初始化handler
func Register() http.Handler {
h := handler.New(&handler.Config{
Schema: &schema.Schema,
Pretty: true,
GraphiQL: false,
Playground: true,
})
// token传参
hdl := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
ctx = context.WithValue(ctx, "token", r.Header.Get("token"))
h.ContextHandler(ctx, w, r)
})
return hdl
}
这样改造后,api应该可以用了.我们用postman来测试.
- 首先是login,如之前的图示,获取token.
- 请求如果不带上token
- 请求带上错误的token
- 请求带上正确的token