jwt以及如何使用jwt實現登錄
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: '//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 = "/";
})
})
},