今天练习token登录认证,发现网上查的资料大多都看不懂,然后自己琢磨了半天勉强实现了这个功能,记录一下,博主是小白,大伙勿喷……
首先我们的前端发送一个http请求给后端,一般登录验证为POST请求,这里使用fetch方法来发起请求,具体fetch的用法可以自行上网查,原理跟axios相差不大:
const handleLogin = () => {
let url='api/user/login';
fetch(url,{
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: new Headers({
'Content-Type': 'application/json',
}),
redirect: 'follow',
body: JSON.stringify(formData)
}).then((v)=>{
return v.json(); //用json序列化函数处理响应的数据
}).then((v)=>{
//如果该响应的结果为空
if(!v){
message.error("null respone!")
}
if(v.status===2005){
message.error(v.msg);
return
}else if(v.status===200||v.status===201){
message.success("进入聊天室成功")
// console.log(v.data.token)
localStorage.setItem("token",v.data.token);
setTimeout(() => {
routers.push('/chat')
}, 1500);
}
}).catch((err) => {
// console.log(err);
message.error(err);
});
};
PS:上面的body中是携带了username和password,最后使用JSON.stringify()函数转化为json字符串传给后端
当请求成功后,后端会返回一个token,我们需要将token通过localStorage对象存储在本地浏览器中
localStorage.setItem("token",v.data.token);
此时前端就拿到了token,之后便可以使用这个token去完成相应的路由守卫和请求拦截了。
不过我们先来看看后端是如何生成token的:
首先这里直接先给出整个Login函数的处理:
//用户登录
func Login(w http.ResponseWriter, r *http.Request) {
var err error
//msg用于响应客户端,,msg.data里存放着用户信息
msg := define.ReplyProto{
Status: 200,
Msg: "success",
}
//如果不是请求方法不是post
if strings.ToLower(r.Method) != "post" {
msg.Status = -400
msg.Msg = "invalid request,should be post"
respone.Resp(w, &msg)
return
}
//buf接收请求发送过来的用户信息
buf, err := ioutil.ReadAll(r.Body)
if err != nil {
msg.Status = -403
msg.Msg = err.Error()
return
}
//如果请求的参数为空
if buf == nil {
msg.Status = -500
msg.Msg = "invalid/nil request param"
return
}
jsonMap := make(map[string]interface{})
err = json.Unmarshal(buf, &jsonMap)
if err != nil {
fmt.Println("1:" + err.Error())
}
//获取用户名和密码
username := jsonMap["username"]
password := jsonMap["password"]
//如果用户名或密码为空
if username == "" || password == "" {
msg.Status = -500
msg.Msg = "invalid/empty user/password"
respone.Resp(w, &msg)
return
}
//连接数据库比对用户名和密码
s := `select id,username from t_user where username = $1 and password=crypt($2,password) `
var userID int
result := dao.DB.QueryRow(context.Background(), s, username, password)
err = result.Scan(&userID, &username)
nonexistent := err == pgx.ErrNoRows
//密码错误或者用户不存在
if nonexistent {
msg.Status = 2005
msg.Msg = "用户名/密码错误,请重新登录!"
respone.Resp(w, &msg)
return
}
//创建token
token, err := createToken(userID, username)
if err != nil {
msg.Status = 501
msg.Msg = "生成token失败!"
respone.Resp(w, &msg)
return
}
msg.Data = []byte(fmt.Sprintf(`
{"id":%d,"username":"%s","token":"%s"}`, userID, username, token))
//响应客户端
respone.Resp(w, &msg)
return
}
具体的封装函数就不细追究,我们只需要关注一下如何生成token即可,登录成功后我们可以得到username和userid(password出于安全,我们一般是不操作这个数据的),这里的userid是用户注册时会生成的,我这里只要在查询数据库比对密码的时候返回一下userid和username即可(具体看每个人的思路是如何的),而这两个数据是可以拿来生成token的,此时我们就可以引进jwt包
import "github.com/dgrijalva/jwt-go"
封装一个CreateToken的函数,参数为username和userid来生成token
//自定义令牌
var mySigningKey = []byte("Key of Chery")
//创建token
func createToken(userid int, username interface{}) (s string, err error) {
// Create the Claims
claims := MyClaim{
Username: username,
Id: userid,
StandardClaims: jwt.StandardClaims{
NotBefore: time.Now().Unix() - 60, //生效时间,这里是一分钟前生效
ExpiresAt: time.Now().Unix() + 60*60, //过期时间,这里是一小时过期
Issuer: "chery", //签发人
},
}
//SigningMethodHS256,HS256对称加密方式
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
//通过自定义令牌加密
ss, err := token.SignedString(mySigningKey)
if err != nil {
fmt.Println("生成token失败")
}
return ss, err
}
查看官方文档,我们可以使用jwt.NewWithClaims()函数来生成token,参数中claims是一个结构体(因此我们可以自定义自己的claims结构体,我上面定义的是MyClaims),而结构体里的jwt.StandardClaims{}类型则是定义token的规则,如过期时间,签发人,生效时间等……(因为token存在被盗的风险,因此建议有效时间设置短一些)
源码中给出的结构:
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
这里解释各个字段的意义:
- 发行人:iss
- 到期时间:exp
- 主题:sub
- 受众:aud
- 编号:jti
- 生效时间:nbf
- 有效时间:exp
- 签发时间:iat
第一个参数是指定一个加密方法,最后这个函数会返回一个Token的结构体,
func NewWithClaims(method SigningMethod, claims Claims) *Token {
return &Token{
Header: map[string]interface{}{
"typ": "JWT",
"alg": method.Alg(),
},
Claims: claims,
Method: method,
}
}
最后通过SignedString()函数使用我们自己定义的唯一令牌去生成token字符串,这个token就可以返回给前端了
func (t *Token) SignedString(key interface{}) (string, error) {
var sig, sstr string
var err error
if sstr, err = t.SigningString(); err != nil {
return "", err
}
if sig, err = t.Method.Sign(sstr, key); err != nil {
return "", err
}
return strings.Join([]string{sstr, sig}, "."), nil
}
核心代码就只有几行罢了
到这里我们的token是如何生成的就搞清楚了,那么接下来回到前端,我们需要设置一下路由守卫,和如何将每个请求都带上token:
这里就简单的判断本地浏览器有无token,如果没有就说明一定没有登录,跳回到登录页。如果token存在则放行。但是这样做还不够,试想一下,如果有人知道我们的api接口和参数,是不是就可以直接使用postman等工具就可以把获得我们后端的数据了呢?因此前端需要在每次请求中都带上token,而后端则会再检验这个token的合法性,从而判断该请求是否合法……
//路由守卫
router.beforeEach((to,from,next)=>{
console.log(to.name)
let token=localStorage.getItem('token');
//如果token存在则放行
if(token){
next();
}else{
if(to.name==='login'||to.name==='register'){
next()
}else{
next('/login')
}
}
})
让每次请求都带上token有两种方法,一种是放到cookie中,另一种则是放在请求头header里,这里我们使用第二种方法,第一种方法有兴趣可以自行查找资料(这种方法没有第二种安全,容易被CSRF(跨站请求伪造)攻击):
很简单,只需要在请求前先获取本地的token,如果没有token则让用户重新登录,如果有,则添加进请求头里:
let token=localStorage.getItem('token');
if(!token){
message.warning("身份已过期,请重新登录!")
routers.push('/login');
return
}else{
let url='/api/user/userList';
fetch(url,{
method: 'GET',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: new Headers({
'Content-Type': 'application/json',
'Authorization':'Bearer '+token,
}),
redirect: 'follow',
}).then((v)=>{
return v.json()
}).then((v)=>{
if(!v){
message.info("null respone!")
return;
}
if(v.status===401||v.status===402){
message.error("身份已过期,请重新登录!")
routers.push('/login');
return
}else if(v.status===200||v.status===201){
this.userList=v.data;
console.log(this.userList);
}
}).catch((err)=>{
console.log(err);
message.error("系统错误!");
})
}
核心就是在请求头里添加一个Authorization字段来存放token,一般我们会再拼接一个"Bearer "字符串在前面。
headers: new Headers({
'Content-Type': 'application/json',
'Authorization':'Bearer '+token,
}),
axois同理:
axios.get(url,
{
headers: {
'Authorization': 'Bearer ' + token,
},
params: {
……
}
}
)
这样我们的后端就能获得这个token了,那么后端要如何验证这个token是否合法呢?
这里的处理方法也十分简单,就是直接通过request.Header即可获得请求头了,而header是一个map[string] []string类型的map,因此直接取就可以了,最后除去"Bearer ",即我们想要的token了,注意这里我们还需要判断token的长度是否小于或等于7,如果是的话证明只有"Bearer "或者是非法的token,为了避免我们下面截取字符串的操作不会发生越界,因此这个很重要!
var err error
//连接成功
msg := define.ReplyProto{
Status: 0,
Msg: "success",
}
//从请求头中获取token
header := r.Header
//如果token为空
if header["Authorization"] == nil {
msg.Status = 402
msg.Msg = "token为空"
respone.Resp(w, &msg)
return
}
//获得想要的token部分
token := header["Authorization"][0]
if len(token)<=7{
msg.Status = 402
msg.Msg = "非法token"
respone.Resp(w, &msg)
return
}
token = token[7:]
//验证token是否合法和过期
err = user.ConfirmToken(token)
if err != nil {
msg.Status = 401
msg.Msg = "token过期或者为非法token"
respone.Resp(w, &msg)
return
}
这里我们就可以再封装一个检验token的ConfirmToken函数了,根据官方文档,我们可以使用jwt.ParseWithClaims函数去解析,需要传入一个token字符串,和我们自定义的MyClaims结构体的空接口实现以及一个keyfun()函数,需要我们去返回一个我们自定义的令牌。这个函数最后会返回一个Token结构体,通过断言即可获得我们想要的参数,从而可以使用这些参数去进行进一步的验证和使用:
func ConfirmToken(token string) (err error) {
Token, err := jwt.ParseWithClaims(token, &MyClaim{}, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if err != nil {
fmt.Println(err.Error())
return err
}
fmt.Println(Token.Claims.(*MyClaim).Username)
fmt.Println(Token.Claims.(*MyClaim).Id)
return err
}
如果token是非法的或者是过期的,则会返回一个err,打印出来就类似于我下面那个token is expired by 55m39s,意思就是这个token已经过期了
OK,到这里就基本完成了整个前后端分离的token验证啦!如果想要了解得更深入,可以自行去查看下源码