今天练习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验证啦!如果想要了解得更深入,可以自行去查看下源码

Logo

前往低代码交流专区

更多推荐