jwt以及如何使用jwt實現登入

雷leileilei發表於2021-03-09

jwt的使用和使用jwt進行登入

什麼是jwt

jwt是 JSON WEB TOKEN英文的縮寫,它是一個開放標準,它定義了一種緊湊的、自包含的方式,用於作為JSON物件在各方之間安全傳輸資訊。該資訊可以被驗證和信任,因為它是數字簽名用的。

使用jwt最多的場景就是登陸,一旦使用者登陸,那麼後續的每個請求都應該包含jwt。

jwt的組成

jwt由三部分組成,每一部分之間用符號"."進行分割,整體可以看做是一個長字串。
一個經典的jwt的樣子:
	xxx.xxx.xxx

jwt的三部分分別是:

  • Header 頭部

    • 頭部由兩部分組成:第一部分是宣告型別,在jwt中宣告型別就jwt,第二部分是宣告加密的演算法,加密演算法通常使用HMAC SHA256

    • 一個經典的頭部:

      {
        'typ': 'JWT',      //  'typ':'宣告型別'
        'alg': 'HS256'	//	'alg':'宣告的加密演算法'
      }
      
  • Payload 載體、載荷

    • 這一部分是jwt的主體部分,這一部分也是json物件,可以包含需要傳遞的資料,其中jwt指定了七個預設的欄位選擇,這七個欄位是推薦但是不強制使用的:
      iss:發行人
      exp:到期時間
      sub:主題
      aud:使用者
      nbf:在此之前不可用
      iat:釋出時間
      jti:JWT ID用於識別該JWt

    • 除了上述的七個預設欄位之外,還可以自定義欄位,通常我們說JWT用於使用者登陸,就可以在這個地方放置使用者的id和使用者名稱

    • 下面這個json物件是一個jwt的Payload部分

      {
      
      "sub": "1234567890",
      
      "nickname": "leileilei",
      
      "id": "1234123"
      
      }
      
      • 這裡注意雖然可以放自定的資訊,但是不要存放一些敏感資訊,除非是加密過的,因為這裡的資訊可能會被截獲
  • signature 簽證

    • 這部分是對前兩部分進行base64編碼在進行加密,這個加密的方式使用的是jwt的頭部宣告中的加密方式,在加上一個密碼(secret)組成的,secret通常是一個隨機的字串,這個secret是伺服器特有的,不能夠讓其他人知道。這部分的組成公式是:

      HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
      

為什麼選擇jwt

session的缺點
  • 首先在我的認知裡jwt用處最多的就是作為使用者登陸的憑證,以往這個憑證是使用session和cookie進行儲存的,session技術的儲存在伺服器端的一種技術,構造一個類似於雜湊表儲存使用者id和使用者的一些資訊,將這個使用者id放在cookie裡返回給使用者,使用者每次登陸的時候帶上這個cookie,在雜湊表中如果可以查到資訊,那麼說明使用者登陸並且得到對應使用者的資訊。
  • 但是session存放在伺服器端,當使用者量很大時,佔用了伺服器過多的寶貴的記憶體資源。同時因為如果有多臺伺服器,那麼當使用者登陸時訪問了伺服器A,那麼就只有伺服器A上會儲存這個使用者的資訊,當使用者訪問其他頁面時,也許請求會發給伺服器B,這時伺服器B中是沒有使用者的資訊的,會判定使用者處於非登入的狀態。也就是說session無法很好的在微服務的架構之中使用。
  • 因為session是和cookie結合使用的,如果cookie被截獲,那麼就會存在安全危機。
jwt的優點
  • json形式,而json非常通用性可以讓它在很多地方使用
  • jwt所佔位元組很小,便於傳輸資訊
  • 需要伺服器儲存資訊,易於擴充套件

一個jwt的工具類

如果需要使用jwt可以直接拿去使用

package com.lei.commonutils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * @author leileilei
 * @since 2019/10/16
 */
public class JwtUtils {

    //常量
    public static final long EXPIRE = 1000 * 60 * 60 * 24; //token過期時間
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //祕鑰,加鹽

     //	@param id 當前使用者ID
     //	@param issuer 該JWT的簽發者,是否使用是可選的
     //	@param subject 該JWT所面向的使用者,是否使用是可選的
     //	@param ttlMillis 什麼時候過期,這裡是一個Unix時間戳,是否使用是可選的
     //	@param audience 接收該JWT的一方,是否使用是可選的
    //生成token字串的方法
    public static String getJwtToken(String id, String nickname){

        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")	//頭部資訊
                .setHeaderParam("alg", "HS256")	//頭部資訊
				//下面這部分是payload部分
            		// 設定預設標籤
                .setSubject("leileilei")	//設定jwt所面向的使用者
                .setIssuedAt(new Date())	//設定簽證生效的時間
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))	//設定簽證失效的時間
					//自定義的資訊,這裡儲存id和姓名資訊
                .claim("id", id)  //設定token主體部分 ,儲存使用者資訊
                .claim("nickname", nickname)
				//下面是第三部分
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
		// 生成的字串就是jwt資訊,這個通常要返回出去
        return JwtToken;
    }

    /**
     * 判斷token是否存在與有效
     * 直接判斷字串形式的jwt字串
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判斷token是否存在與有效
     * 因為通常jwt都是在請求頭中攜帶,此方法傳入的引數是請求
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");//注意名字必須為token才能獲取到jwt
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根據token字串獲取會員id
     * 這個方法也直接從http的請求中獲取id的
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
}

將jwt和登入進行結合

controller層

	//實現前端登入
    @PostMapping("login")
    public R login(@RequestBody UcenterMember ucenterMember){
        //ucenterMember封裝了手機號和密碼
        //在service裡面進行驗證
        //並且需要返回token,作為單點登入的憑證
        String token = ucenterMemberService.login(ucenterMember);
        return R.ok().data("token",token);
    }

service層

	//判斷登入
    @Override
    public String login(UcenterMember ucenterMember) {
        //首先獲取到正在進行登入的手機和密碼
        String mobile = ucenterMember.getMobile();
        String password = ucenterMember.getPassword();

        //判斷手機號和密碼是否是空值
        if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
            throw new DiyException(20001,"使用者名稱或者密碼為空");
        }

        //判斷是否存在這個使用者
        QueryWrapper<UcenterMember> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("mobile",mobile);
        UcenterMember member = baseMapper.selectOne(queryWrapper);

        //如果這個member物件是空,那麼說明非法訪問
        if(member == null){
            throw new DiyException(20001,"該使用者不存在");
        }

        if(!member.getPassword().equals(MD5.encrypt(password))){
            throw new DiyException(20001,"使用者名稱或者密碼錯誤");
        }

        //走到這裡說明登入是成功的
        //生成token,使用封裝好的工具類
        //將該使用者的id和使用者名稱放入到token中
        String token = JwtUtils.getJwtToken(member.getId(), member.getNickname());
        return token;
    }

當存在某些需求,需要使用者登入之後再進行操作可以使用如下程式碼

	//根據課程id進行下單操作
    //最後需要返回訂單號,通過訂單號生成支付的二維碼
    @GetMapping("toBuy/{courseId}")
    public R toBuy(@PathVariable String courseId, HttpServletRequest request){
        //通過jwt工具類,在request中獲取到使用者id
        String userId = JwtUtils.getMemberIdByJwtToken(request);
        //如果沒有登入則無法購買
        if(StringUtils.isEmpty(userId)){
            return R.error().message("請先登入後在進行操作");
        }
        //確認為登入狀態之後往下進行操作
        String orderNo = orderService.toBuyGetOrderNo(courseId,userId);
        return R.ok().data("orderNo",orderNo);
    }

axios方式將jwt放在header中

import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import cookie from 'js-cookie'

// 建立axios例項
const service = axios.create({
  baseURL: 'http://localhost:9002', // api的base_url
  timeout: 20000 // 請求超時時間
})

//第三步,將cookie中的token放入到header(請求頭)中
// http request 攔截器

service.interceptors.request.use(
  config => {
  //debugger
    if (cookie.get('token')) {
      config.headers['token'] = cookie.get('token');
    }
      return config
    },
    err => {
    return Promise.reject(err);
})

前端的js程式碼

	//實現登入
      //第一步呼叫介面實現登入
      submitLogin(){
        loginApi.login(this.user)
          .then(response => {
              //第二步,獲取到token,並將token放入到cookie中
              cookie.set('token',response.data.data.token,{domain: 'localhost'})

              //第四步,請求介面獲得使用者資料
              loginApi.getInfoByToken()
                .then(response => {
                  this.loginInfo = response.data.data.userInfo
                  //獲取返回使用者資訊,放到cookie裡面
                  cookie.set('ucenter',this.loginInfo,{domain: 'localhost'})

                  //跳轉頁面
                  window.location.href = "/";
                })
          })
      },

相關文章