赞
踩
今天练习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"`
- }
这里解释各个字段的意义:
第一个参数是指定一个加密方法,最后这个函数会返回一个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验证啦!如果想要了解得更深入,可以自行去查看下源码
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。