個人學習筆記分享挟纱,當前能力有限羞酗,請勿貶低,菜鳥互學紊服,大佬繞道
如有勘誤檀轨,歡迎指出和討論,本文后期也會進行修正和補充
前言
隨著分布式的普及围苫,session的成本正變得越來越高裤园,因而一種不需要session,而直接將身份信息放在token中的方案應運而生--JWT
請留意剂府,本文主要整理相關思路拧揽,為方便理解,示例代碼并不完整腺占,更談不上嚴謹
若需要實際使用的demo淤袜,請直接查看整理后的代碼,完整demo會傳到github或碼云
1.介紹
1.1.什么是JWT
JWT全程為Json web token衰伯,是一種開放標準(RFC 7519)铡羡,定義了一種緊湊的之景、自包含的方式腰奋,用于作為JSON對象在各方之間安全地傳輸信息贩幻。
這種數(shù)字簽名的設計,緊密且安全潦嘶,特別適用與分布式登錄和單點登錄的場景床佳。
JWT一般用于驗證身份怕品,也可以根據(jù)業(yè)務峡钓,添加其他業(yè)務邏輯所需要的的信息,如權限夭委。
JWT可以直接用于認證幅狮,也可以進行加密。
1.2.架構
1.3.與傳統(tǒng)session認證的區(qū)別
打個比方:一個用戶在權限森嚴的地區(qū)活動
- session認證:登記時告知用戶一個編號株灸,用戶到一個地方就報出自己的編號崇摄,然后工作人員去查詢這個編號能去哪不能去哪,以此來判斷是否放行
- JWT認證:登記時發(fā)給用戶一個證件慌烧,用戶到一個地方就出示證件逐抑,工作人員確認證件是自己家的,然后直接看證件上寫了能去哪杏死,以此判斷是否放行
兩者的利弊顯而易見泵肄,
傳統(tǒng)session認證的方案:傳統(tǒng)session認證,一般僅在前后端傳遞cookie淑翼,作為session的關鍵詞腐巢,后端再根據(jù)cookie查詢對應的session,從而確認登陸者的身份和權限等信息玄括。session通常存于緩存冯丙、數(shù)據(jù)庫或者redis等中間件,redis最為常見遭京。
-
傳統(tǒng)session認證的弊端:無論將session存于何處胃惜,用戶登錄的時候都必須存儲認證信息,且大部分請求都執(zhí)行一次session查詢哪雕,因而
- 隨著用戶越來越多船殉,服務器的開銷必然越來越大,認證速度也必然受到影響斯嚎。
- 每次查詢session都必須請求其存儲的服務器利虫,無疑限制了分布式中負載均衡的能力
JWT的方案:JWT不需要將認證信息進行保存,直接將其加密后在前后端傳遞堡僻,后端進行解密即可獲取身份和其他聲明信息
-
JWT的優(yōu)勢:JWT的優(yōu)勢即解決了session認證的弊端糠惫,JWT將認證信息在前后端傳遞,而后端本身不存儲信息钉疫,因而
- 后端不存儲信息硼讽,故服務器開銷極低,且認證速度不會受用戶數(shù)量影響
- 后端直接解析token牲阁,無需請求其他的服務器固阁,不會給負載均衡帶來影響壤躲,易于擴展
- 業(yè)務json的通用性,JWT也擁有了跨平臺的能力您炉,在Java柒爵、JS役电、NodeJS赚爵、PHP等語言均可使用
- jwt一般存放于請求頭中,結構簡單且數(shù)據(jù)量很小法瑟,非常便于傳輸
JWT的弊端:有點多冀膝,留在文末說,不然可能會打消你繼續(xù)看下去的想法霎挟。窝剖。。綜合考慮其實我不建議JWT替代session認證
2.結構
JWT由三段信息組成:頭部(header)酥夭、載荷(payload)赐纱、簽證(signature)
https://jwt.io/可以模擬JWT的生成和解碼
2.1.header
頭部包含兩部分信息:
-
聲明類型(typ):默認值即
JWT
, -
加密算法(alg):默認值為
HS256
,也可選擇其他加密算法
示例:
{
'typ': 'JWT',
'alg': 'HS256'
}
對應base64UrlEncode
編碼為:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2.2.payload
載荷即存放有效數(shù)據(jù)的地方疙描,但是因為==可被解碼讶隐,不建議存放敏感信息==
包括三個部分:
- 標準中注冊的聲明:建議但不強制使用,存放JWT相關的數(shù)據(jù)
- iss: jwt簽發(fā)者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間效五,這個過期時間必須要大于簽發(fā)時間
- nbf: 定義在什么時間之前炉峰,該jwt都是不可用的.
- iat: jwt的簽發(fā)時間
- jti: jwt的唯一身份標識,主要用來作為一次性token疼阔,從而回避重放攻擊
- 公共的聲明:可存放任何信息,一般添加用戶信息和相關業(yè)務的數(shù)據(jù)
- 私有的聲明:提供者和消費者所共同定義的聲明
示例:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
對應base64UrlEncode
編碼為:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
2.3.signature
簽證實際上就是將header和payload進行base64編碼谱仪,再通過秘鑰加密后的密文,用于保證jwt不會被偽造或人為修改否彩,生成方式如下
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
因此秘鑰非常重要疯攒,必須保證其不會被泄露或破解,否則整個驗證系統(tǒng)將如同虛設
示例:
將前面兩個示例信息列荔,使用秘鑰echo
進行加密的結果為QrtQpbkSSLyGt1qRQ4nZ3K0OcyO7CCv0HxIdsvYYSFU
3.使用流程
3.1.前端使用
前端只需在登錄成功后保存返回的token敬尺,在發(fā)起其他請求的時候枚尼,向請求頭中加入字段Authorization
,并加上Bearer標注即可
fetch('api/user/info', {
headers: {
'Authorization': 'Bearer ' + token
}
})
發(fā)起的請求頭如下所示
3.2.后端使用
登錄:
- 接收登錄請求砂吞,驗證賬號密碼等信息署恍,確認其身份驗證無誤
- 查詢其他業(yè)務相關信息,如身份權限等蜻直,組裝成payload數(shù)據(jù)
- 通過預設規(guī)則盯质,生成JWT
- 通過http的response返回JWT給前端,結束請求
其余請求:
- 接收請求概而,取出頭字段
Authorization
呼巷,即認證信息 - 根據(jù)預設規(guī)則,解碼獲得明文信息赎瑰,對JWT進行驗證
- 獲取JWT中的業(yè)務相關信息王悍,并處理此請求相關業(yè)務
- 返回業(yè)務結果給前端,結束請求
4.集成(基于Java+SpringBoot+AOP)
還有其他集成方法餐曼,有興趣的可以自行查閱
4.1.添加依賴
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
添加一個依賴就行了,其余的沒必要
# JWT
# 發(fā)行者
spring.jwt.name=echo
# 密鑰
spring.jwt.base64Secret=23333
# jwt中過期時間設置(分)
spring.jwt.jwtExpires=120
秘鑰是最核心的數(shù)據(jù)集惋,是保證令牌不被偽造的唯一防線芋膘,無論如何也不能泄露为朋,安全起見建議每個項目都生成隨機的字符串作為秘鑰
4.2.自定義注解
注解用于標記需要認證的目標厚脉,當然也可以標記不需要認證的目標霞溪,最好使用AOP自定義注解攔截中捆,來應對不同業(yè)務需求
-
注解
@JwtCheck
泄伪,用于標記接口需要驗證染厅,只需要@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface JwtCheck { boolean required() default true; }
其余注解請根據(jù)實際業(yè)務添加
4.3.JWT工具類
核心就是生成和解析token,還有驗證token
- 生成token使用
JWT.create()
再組裝需要的參數(shù)即可孤页,因為方法都是限定好的行施,比較簡單悲龟,幾乎不可能出錯吧。斩芭。划乖。- 解析token直接解析請求頭
Authorization
的內容就行了琴庵,自行去除最前面的Bearer
迷殿,剩余內容使用base64解碼即可- 驗證token是最關鍵的一步庆寺,也最好理解懦尝,重復一遍加密過程陵霉,然后把加密結果與簽名對比踊挠,兩者匹配即保證令牌不是偽造的
@Component
public class JwtUtils {
/**
* gson對象止毕,提前初始化
*/
private final static Gson gson = new Gson();
/**
* jwt秘鑰
*/
private static String jwtSecret;
/**
* jwt有效時間
*/
private static Long jwtExpires;
/**
* jwt發(fā)行者名稱
*/
private static String jwtName;
@Value("${spring.jwt.base64Secret}")
public void setJwtSecret(String jwtSecret) {
this.jwtSecret = jwtSecret;
}
@Value("${spring.jwt.jwtExpires}")
public void setJwtExpires(Long jwtExpires) {
this.jwtExpires = jwtExpires;
}
@Value("${spring.jwt.name}")
public void setJwtName(String jwtName) {
this.jwtName = jwtName;
}
/**
* 獲取token字符串
*
* @param data 對象
* @return token字符串
*/
public static String getToken(Object data) {
String token = "";
// 計算時間
Date expiredDate = new Date(System.currentTimeMillis() + jwtExpires * 1000L);
Date issuedDate = new Date();
// 創(chuàng)建jwt
token = JWT
.create()
.withAudience(gson.toJson(data))
.withIssuer(jwtName)
.withIssuedAt(issuedDate)
.withExpiresAt(expiredDate)
.sign(Algorithm.HMAC256(jwtSecret));
return token;
}
/**
* gson解碼
*
* @param encoded json字符串
* @return 解碼后的對象
*/
public static UserInfo decode(String encoded) {
return gson.fromJson(encoded, UserInfo.class);
}
/**
* 驗證token
*
* @param token token字符串
* @throws Exception
*/
public static void verifyToken(String token) throws Exception {
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecret)).build();
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new BaseException("身份驗證失敗");
}
}
/**
* 從http請求中解析token
*
* @param request http請求
* @throws Exception 解析異常
* @returntoken字符串
*/
public static String getToken(HttpServletRequest request) throws Exception {
String authorization = Strings.nullToEmpty(request.getHeader("Authorization"));
if (!authorization.startsWith("Bearer")) {
throw new BaseException("Token非JWT標準");
}
return authorization.substring(7);
}
}
4.4.數(shù)據(jù)查詢
為方便測試僅做模擬查詢,實際應用請移步從數(shù)據(jù)庫查詢谨朝,可參考文末整理后的代碼
@Service
public class UserService {
/**
* 模擬數(shù)據(jù)庫的數(shù)據(jù)
*/
private List<UserInfo> userInfoList;
public UserService() {
userInfoList = new ArrayList<>();
userInfoList.add(new UserInfo(1, "user1", "pwd1", 1));
userInfoList.add(new UserInfo(2, "user2", "pwd2", 2));
userInfoList.add(new UserInfo(3, "user3", "pwd3", 3));
}
/**
* 根據(jù)id查詢用戶
*
* @param id 用戶id
* @return 用戶信息
*/
public UserInfo getUserById(Long id) {
for (UserInfo item : userInfoList) {
if (Objects.equals(item.getId(), id)) {
return item;
}
}
return null;
}
/**
* 校驗賬號
*
* @param username 賬號
* @param password 密碼
* @return 若查詢到賬號則返回賬號信息则披,否則返回null
*/
public UserInfo checkUser(String username, String password) {
for (UserInfo item : userInfoList) {
if (Objects.equals(item.getUsername(), username) && Objects.equals(item.getPassword(), password)) {
return item;
}
}
return null;
}
}
4.5.切面層攔截器
這里應該是最關鍵的地方士复,用于攔截注解標記的方法阱洪,在這里進行身份驗證菠镇,失敗則不再繼續(xù)利耍,成功則回到切入點
- 使用
@Pointcut
設置切入點的條件- 在使用
@Around
設定切入的方法程癌,若中途驗證失敗席楚,則拋出異常烦秩,若成功則繼續(xù)執(zhí)行原有邏輯
@Aspect
@Component
@Slf4j
public class JwtInterceptAspect {
/**
* 切入點
*/
@Pointcut("execution(* com.yezi_tool.demo_basic.controller..*(..))&&@annotation(com.yezi_tool.demo_basic.jwt.JwtCheck)")
public void controllerAspect() {
}
/**
* 切入方法
*/
@Around("controllerAspect() ")
public Object aroundMethod(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = null;
UserInfo user = null;
JwtCheck jwtCheck = ((MethodSignature) point.getSignature()).getMethod().getAnnotation(JwtCheck.class);
if (jwtCheck.required()) {
request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String token = JwtUtils.getToken(request);
//獲取token的userid
List<String> audience = JWT.decode(token).getAudience();
user = JwtUtils.decode(audience.get(0));
if (user == null) {
throw new BaseException("身份驗證失敗");
}
//驗證token只祠,秘鑰為用戶的密碼
JwtUtils.verifyToken(token);
}
Object[] args = point.getArgs();
UserInfo finalUser = user;
args = Arrays.stream(args).map(arg -> {
if (Objects.nonNull(arg) && UserInfo.class.isAssignableFrom(arg.getClass()))
arg = finalUser;
return arg;
}).toArray();
return point.proceed(args);
}
}
4.6.控制層
簡單的寫兩個方法,一個用于登錄,一個用于測試登錄結果
@Controller
@RequestMapping("/login")
public class LoginController extends BaseController {
/**
* 用戶信息業(yè)務層
*/
private final UserService userService;
public LoginController(UserService userService) {
this.userService = userService;
}
@Data
private static class LoginRequest {
private String username;
private String password;
private Boolean rememberMe;
}
@PostMapping("/login")
@ResponseBody
public ReturnMsg login(@RequestBody LoginRequest loginRequest) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
UserInfo userInfo = userService.checkUser(loginRequest.getUsername(), loginRequest.getPassword());
if (userInfo == null) {
throw new BaseException("賬號或密碼不正確");
}
Map<String, Object> data = new HashMap<>();
data.put("id", userInfo.getId());
data.put("username", userInfo.getUsername());
returnMsg.setData(JwtUtils.getToken(data));
return returnMsg;
}
@PostMapping("/test")
@ResponseBody
@JwtCheck
public ReturnMsg test(Integer mark) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
if (mark == null) {
throw new BaseException("缺少參數(shù)");
}
returnMsg.setData(userInfo);
return returnMsg;
}
}
執(zhí)行結果
-
執(zhí)行登錄接口,若賬號密碼正確則返回token字符串
<img src="https://gitee.com/echo_ye/assets/raw/master/img/image-20200826181729989.png" alt="image-20200826181729989" />
-
執(zhí)行測試接口川陆,將token放在請求頭里
image-20200827094946884
5.JWT的弊端
以下均不考慮查詢數(shù)據(jù)庫或者緩存较沪,否則就相當于放棄自己僅有的優(yōu)勢
5.1.安全性
- JWT令牌使用base64編碼尸曼,對于前端如同明文(http://jwt.calebb.net/不信你試試控轿?)解幽,那么敏感信息不適合放在令牌里,但如果從數(shù)據(jù)庫獲取便放棄了自己的優(yōu)勢
- 秘鑰一旦泄露镣衡,客戶端便可以自己簽發(fā)令牌廊鸥,除非修改秘鑰辖所,那么所有令牌全部失效
5.2.不可修改
- 如果令牌相關的內容被修改吆视,如賬號啦吧,身份授滓,姓名等般堆,只能讓用戶重新登錄
- 令牌無法續(xù)簽淮摔,到時間即失效噩咪,除非將時間設定的極長
5.3.不可銷毀
如果需要強制下線胃碾,或者報廢舊的令牌仆百,JWT完全無法做到
就算下發(fā)新的令牌俄周,舊的令牌在時效內依然有效
5.4.性能
JWT的payload內容越多峦朗,令牌便越大翎朱,開銷也會越來越大拴曲,甚至超出cookie的長度限制(cookie一般限制在4k澈灼,而redis是512M叁熔。者疤。)驹马,請求帶的參數(shù)往往只有幾個糯累,本末倒置了吧效拭。缎患。挤渔。
以上問題在session認證均不會出現(xiàn)E械肌Q廴小擂红!
6.真正適合JWT的場景
6.1.一次性驗證
如用戶注冊后發(fā)送一封激活郵件,包括一個鏈接吩抓,需要用戶在時限內點擊,超時即失效雨饺,
那么需要的以下條件:
- 能標記用戶额港,一般是id或者賬號進行標記
- 有時效性移斩,通常只有幾個小時時效肠套,超時便失效
- 不可被修改你稚,用于其他賬號或者用途
JWT的payload刁赖、expires宇弛、簽名都完美契合以上條件
6.2.restful api 的無狀態(tài)認證
jwt不在服務端存儲任何狀態(tài)涯肩。RESTful API的原則之一是無狀態(tài)病苗,發(fā)出請求時硫朦,總會返回帶有參數(shù)的響應,不會產(chǎn)生附加影響破婆。用戶的認證狀態(tài)引入這種附加影響祷舀,這破壞了這一原則裳扯。另外jwt的載荷中可以存儲一些常用信息饰豺,用于交換信息冤吨,有效地使用 JWT其馏,可以降低服務器查詢數(shù)據(jù)庫的次數(shù)叛复。
7.整理后代碼
7.1.整理內容
優(yōu)化代碼結構
可對類或者方法標記需要或跳過認證褐奥,并指定認證的身份
統(tǒng)一使用AOP切面攔截請求撬码,進行身份認證呜笑,并在認證成功后將身份信息插入切入點
允許不同身份的用戶登錄,并使用不同格式的令牌
密碼使用鹽和MD5加密
使用策略設計模式
7.2.核心源碼
自定義注解
package com.yezi_tool.demo_basic.jwt;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Echo_Ye
* @title 注解-jwt驗證
* @description 用于標記接口jwt驗證
* @date 2020/8/26 13:55
* @email echo_yezi@qq.com
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
Class<?> value();
}
package com.yezi_tool.demo_basic.jwt;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Echo_Ye
* @title 注解-jwt不進行驗證
* @description 用于標記接口jwt不進行驗證
* @date 2020/8/26 13:55
* @email echo_yezi@qq.com
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoAuth {
}
自定義認證實體
package com.yezi_tool.demo_basic.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.hash.Hashing;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @title JWT認證虛擬類
* @description
* @author Echo_Ye
* @date 2020/9/3 17:11
* @email echo_yezi@qq.com
*/
@SuppressWarnings("UnstableApiUsage")
@Slf4j
public abstract class AbstractAuthByJWT<T> {
/**
* 抽象方法-獲取類型
*/
public abstract byte getUserType();
/**
* 抽象方法-解碼
*/
protected abstract T decode(String encoded);
/**
* 抽象方法-編碼
*/
protected abstract String encode(T bean);
/**
* 抽象方法-獲取bean對象
*/
protected abstract T getBean(HttpServletRequest request) throws Exception;
/**
* 抽象方法-獲取默認時間
*/
protected abstract Duration getDefaultDuration();
/**
* 相關鉤子-暫不使用
*/
protected void hookOnCreate(JWTCreator.Builder builder) {
}
protected void hookOnAddHeader(Map<String, Object> header) {
}
protected void hookOnVerify(DecodedJWT jwt) {
}
/**
* 加密算法
*/
private final Algorithm algorithm;
/**
* jwt驗證器
*/
private final JWTVerifier verifier;
/**
* 發(fā)布者,取當前class
*/
private final String identity = this.getClass().getName();
/**
* 初始化
*
* @param secret 秘鑰
*/
public AbstractAuthByJWT(String secret) {
algorithm = Algorithm.HMAC256(Hashing.sha256().hashBytes(secret.getBytes()).asBytes());
verifier = JWT.require(algorithm).build();
}
/**
* 創(chuàng)建token
*/
public String create(T auth) throws Exception {
return create(auth, getDefaultDuration());
}
/**
* 創(chuàng)建token
*/
public String create(T auth, Duration duration) throws Exception {
try {
// 計算時間
Date expiredDate = new Date(System.currentTimeMillis() + duration.getSeconds() * 1000L);
Date issuedDate = new Date();
// 序列化主數(shù)據(jù)
String data = encode(auth);
Preconditions.checkState(data.length() > 0);
// 添加頭部信息
HashMap<String, Object> header = Maps.newHashMap();
hookOnAddHeader(header);
JWTCreator.Builder builder = JWT.create()
.withHeader(header)
.withClaim(JwtConstants.JWT_REQUEST_CLAIM_KEY, data)
.withIssuer(identity)
.withIssuedAt(issuedDate)
.withExpiresAt(expiredDate);
hookOnCreate(builder);
return builder.sign(algorithm);
} catch (Throwable e) {
throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CREATE_TOKEN));
}
}
/**
* 驗證request
*
* @param request request請求
* @return 返回泛型對象
* @throws Exception
*/
protected T verify(HttpServletRequest request) throws Exception {
return verify(JwtUtils.getTokenFromHttpRequest(request));
}
/**
* 驗證request
*
* @param token token字符串
* @return 返回泛型對象
*/
public T verify(String token) {
DecodedJWT jwt = verifier.verify(token);
//校驗簽發(fā)者
Preconditions.checkState(0 == jwt.getIssuer().compareTo(identity));
//校驗數(shù)據(jù)
Claim data = jwt.getClaim(JwtConstants.JWT_REQUEST_CLAIM_KEY);
Preconditions.checkNotNull(data);
//鉤子
hookOnVerify(jwt);
//解析
T bean = decode(Strings.nullToEmpty(data.asString()));
Preconditions.checkNotNull(bean);
return bean;
}
}
package com.yezi_tool.demo_basic.jwt;
import com.yezi_tool.demo_basic.commons.constants.CustomConstants;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.List;
/**
* @author Echo_Ye
* @title jwt學生驗證實體
* @description jwt用于學生身份驗證的實體
* @date 2020/8/28 13:47
* @email echo_yezi@qq.com
*/
@Component
public class StudentAuthByJWT extends AbstractAuthByJWT<StudentAuthByJWT.Instance> {
/**
* 用戶類型
*/
@Getter
private final byte userType = CustomConstants.USER_TYPE_STUDENT;
/**
* 初始化
*
* @param secret 秘鑰
*/
public StudentAuthByJWT(@Value("${spring.jwt.base64Secret}") String secret) {
super(secret);
}
@Getter
public static class Instance extends BaseAuthInstance {
/**
* 學號
*/
private String num;
/**
* 年級id
*/
private Long gradeId;
public Instance(Integer id, List<String> permissionList, String userName, Byte type, Integer personId, String name, Byte gender, String num, Long gradeId) {
super(id, permissionList, userName, type, personId, name, gender);
this.num = num;
this.gradeId = gradeId;
}
}
@Bean("studentAuthByJWTInstance")
@Scope(value = WebApplicationContext.SCOPE_REQUEST) // 該bean僅在本次http request內有效
@Override
protected Instance getBean(HttpServletRequest request) throws Exception {
return super.verify(request);
}
@Override
protected Instance decode(String encoded) {
return JwtUtils.decode(encoded, Instance.class);
}
@Override
protected String encode(Instance bean) {
return JwtUtils.encode(bean);
}
@Override
protected Duration getDefaultDuration() {
return Duration.ofDays(365);
}
}
package com.yezi_tool.demo_basic.jwt;
import com.yezi_tool.demo_basic.commons.constants.CustomConstants;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import com.yezi_tool.demo_basic.entity.UserInfo;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.List;
/**
* @author Echo_Ye
* @title jwt教師驗證實體
* @description jwt用于教師身份驗證的實體
* @date 2020/8/28 13:47
* @email echo_yezi@qq.com
*/
@Component
public class TeacherAuthByJWT extends AbstractAuthByJWT<TeacherAuthByJWT.Instance> {
/**
* 用戶類型
*/
@Getter
private final byte userType= CustomConstants.USER_TYPE_TEACHER;
/**
* 初始化
*
* @param secret 秘鑰
*/
public TeacherAuthByJWT(@Value("${spring.jwt.base64Secret}") String secret) {
super(secret);
}
@Getter
public static class Instance extends BaseAuthInstance {
/**
* 學院id
*/
private Long collegeId;
public Instance(Integer id, List<String> permissionList, String userName, Byte type, Integer personId, String name, Byte gender, Long collegeId) {
super(id, permissionList, userName, type, personId, name, gender);
this.collegeId = collegeId;
}
}
@Bean("teacherAuthByJWTInstance")
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
@Override
protected Instance getBean(HttpServletRequest request) throws Exception {
return super.verify(request);
}
@Override
protected Instance decode(String encoded) {
return JwtUtils.decode(encoded, Instance.class);
}
@Override
protected String encode(Instance bean) {
return JwtUtils.encode(bean);
}
@Override
protected Duration getDefaultDuration() {
return Duration.ofDays(365);
}
}
package com.yezi_tool.demo_basic.jwt;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* @author Echo_Ye
* @title jwt基礎instance
* @description 基礎instance
* @date 2020/8/28 17:54
* @email echo_yezi@qq.com
*/
@Getter
@AllArgsConstructor
public class BaseAuthInstance {
/**
* id
*/
private Integer id;
/**
* 權限列表
*/
private List<String> permissionList;
/**
* 用戶名
*/
private String userName;
/**
* 用戶類型
*/
private Byte type;
/**
* 信息表id
*/
private Integer personId;
/**
* 用戶姓名
*/
private String name;
/**
* 用戶性別
*/
private Byte gender;
}
自定義切面攔截器
package com.yezi_tool.demo_basic.jwt;
import com.google.common.base.Preconditions;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.commons.utils.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Objects;
/**
* @author Echo_Ye
* @title 身份AOP攔截器
* @description 使用AOP攔截身份
* @date 2020/9/3 15:28
* @email echo_yezi@qq.com
*/
@Slf4j
@Aspect
@Component
public class AuthInterceptor {
/**
* AOP切入點
*/
@Pointcut("(@annotation(com.yezi_tool.demo_basic.jwt.Auth) || @within(com.yezi_tool.demo_basic.jwt.Auth)) && !@annotation(com.yezi_tool.demo_basic.jwt.NoAuth)")
public void authPointcut() {
}
/**
* AOP方法切入點
*/
@Pointcut("@annotation(com.yezi_tool.demo_basic.jwt.Auth) && !@annotation(com.yezi_tool.demo_basic.jwt.NoAuth)")
public void authAnnotationPointcut() {
}
/**
* AOP對象切入點
*/
@Pointcut("@within(com.yezi_tool.demo_basic.jwt.Auth) && !@annotation(com.yezi_tool.demo_basic.jwt.NoAuth)")
public void authWithinPointcut() {
}
/**
* AOP對象切入內容
*/
@Around("authWithinPointcut() && @within(auth)")
public Object checkWithinAuth(ProceedingJoinPoint joinPoint, Auth auth) throws Exception {
return checkAuth(joinPoint, auth);
}
/**
* AOP方法切入內容
*/
@Around("authAnnotationPointcut() && @annotation(auth)")
public Object checkAnnotationAuth(ProceedingJoinPoint joinPoint, Auth auth) throws Exception {
return checkAuth(joinPoint, auth);
}
/**
* AOP切入內容
*
* @param joinPoint 切入點
* @param auth 切入注解
* @return
* @throws Exception 拋出異常为牍,驗證失敗
*/
public Object checkAuth(ProceedingJoinPoint joinPoint, Auth auth) throws Exception {
try {
//認證實體
Object authBean = SpringContextHolder.getBean(auth.value());
Preconditions.checkNotNull(authBean);
//插入數(shù)據(jù)到切入點
Object[] args = joinPoint.getArgs();
args = Arrays.stream(args).map(arg -> {
if (Objects.nonNull(arg) && arg.getClass().isAssignableFrom(auth.value()))
// if (Objects.nonNull(arg) && arg instanceof BaseAuthInstance)//效果一樣,但上面的更利于擴展
arg = authBean;
return arg;
}).toArray();
return joinPoint.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CHECK_AUTH));
}
}
}
jwt服務
package com.yezi_tool.demo_basic.service;
import com.yezi_tool.demo_basic.jwt.AbstractAuthByJWT;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Echo_Ye
* @title jwt服務
* @description jwt服務蛀恩,待擴充
* @date 2020/9/3 17:17
* @email echo_yezi@qq.com
*/
@Service
public class JwtService {
Map<Byte, AbstractAuthByJWT> jwtAuthMap = new HashMap<>();
public JwtService(List<AbstractAuthByJWT> abstractAuthByJWTList) {
for (AbstractAuthByJWT auth : abstractAuthByJWTList) {
jwtAuthMap.put(auth.getUserType(), auth);
}
}
public AbstractAuthByJWT getAuth(byte type) {
return jwtAuthMap.get(type);
}
}
服務層實現(xiàn)類
package com.yezi_tool.demo_basic.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yezi_tool.demo_basic.commons.constants.CommonConstants;
import com.yezi_tool.demo_basic.commons.constants.CustomConstants;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.constants.ResponseConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import com.yezi_tool.demo_basic.entity.PermissionInfo;
import com.yezi_tool.demo_basic.entity.StudentInfo;
import com.yezi_tool.demo_basic.entity.TeacherInfo;
import com.yezi_tool.demo_basic.entity.UserInfo;
import com.yezi_tool.demo_basic.jwt.StudentAuthByJWT;
import com.yezi_tool.demo_basic.jwt.TeacherAuthByJWT;
import com.yezi_tool.demo_basic.mapper.PermissionInfoMapper;
import com.yezi_tool.demo_basic.mapper.StudentInfoMapper;
import com.yezi_tool.demo_basic.mapper.TeacherInfoMapper;
import com.yezi_tool.demo_basic.mapper.UserInfoMapper;
import com.yezi_tool.demo_basic.service.IUserInfoService;
import com.yezi_tool.demo_basic.service.JwtService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
/**
* @title 用戶信息服務層
* @description
* @author Echo_Ye
* @date 2020/9/3 17:18
* @email echo_yezi@qq.com
*/
@Slf4j
@Service("userInfoService")
public class UserInfoServiceImpl extends BaseServiceImpl<UserInfo> implements IUserInfoService {
private final UserInfoMapper userInfoMapper;
private final PermissionInfoMapper permissionInfoMapper;
private final StudentInfoMapper studentInfoMapper;
private final TeacherInfoMapper teacherInfoMapper;
private final JwtService jwtService;
public UserInfoServiceImpl(UserInfoMapper userInfoMapper, PermissionInfoMapper permissionInfoMapper, StudentInfoMapper studentInfoMapper, TeacherInfoMapper teacherInfoMapper, JwtService jwtService) {
this.userInfoMapper = userInfoMapper;
this.permissionInfoMapper = permissionInfoMapper;
this.studentInfoMapper = studentInfoMapper;
this.teacherInfoMapper = teacherInfoMapper;
this.jwtService = jwtService;
}
@Override
public UserInfo selectByUserName(String userName) {
return userInfoMapper.selectOne(new QueryWrapper<UserInfo>().eq(UserInfo.COL_USERNAME, userName));
}
@Override
public UserInfo selectByMobile(String mobile) {
return userInfoMapper.selectOne(new QueryWrapper<UserInfo>().eq(UserInfo.COL_MOBILE, mobile));
}
@Override
public List<String> queryPermission(String username) {
List<Map<String, Object>> list = permissionInfoMapper.selectByUsername(username);
List<String> permissionList = list.size() > 0 ? list.stream().
map(m -> String.valueOf(m.get(PermissionInfo.COL_PERMISSION_NAME))).
collect(Collectors.toList()) : new ArrayList<>();
return permissionList;
}
@Override
public UserInfo checkUser(String username, String password) {
return userInfoMapper.selectOne(new QueryWrapper<UserInfo>()
.eq(UserInfo.COL_USERNAME, username)
.eq(UserInfo.COL_PASSWORD, password)
);
}
@Override
public String loginByMobileAndPassword(String mobile, String password) throws Exception {
return loginGeneric(selectByMobile(mobile)
, user -> JwtUtils.verifyPassword(user.getUsername(), user.getPassword(), user.getSalt(), password)
);
}
@Override
public String loginByUserNameAndPassword(String username, String password) throws Exception {
return loginGeneric(selectByUserName(username)
, user -> JwtUtils.verifyPassword(user.getUsername(), user.getPassword(), user.getSalt(), password)
);
}
/**
* 通用登錄邏輯
*
* @param user 用戶實體
* @param callback 驗證回調
* @return Token
* @throws BaseException 登錄異常
*/
private String loginGeneric(UserInfo user, Function<UserInfo, Boolean> callback) throws Exception {
//檢查賬號
if (null == user) {
throw new BaseException(ResponseConstants.RETURN_MSG_LOGIN_INCORRECT_USERNAME);
}
//檢查密碼
if (!callback.apply(user)) {
throw new BaseException(ResponseConstants.RETURN_MSG_LOGIN_INCORRECT_PASSWORD);
}
//檢查賬號是否被禁用
if (user.getStatus() == CommonConstants.STATE_DISABLED) {
throw new BaseException(ResponseConstants.RETURN_MSG_LOGIN_ACCOUNT_DISABLE);
}
//判斷登陸者類型
String token = makeToken(user);
if (StringUtils.isBlank(token)) {
throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CREATE_TOKEN));
}
return token;
}
/**
* 根據(jù)用戶類型生成token谓厘,主要用戶判斷用戶類型
*
* @param userInfo 用戶信息
* @throws Exception 可能拋出自定義異常
* @return生成的toen字符串
*/
public String makeToken(UserInfo userInfo) throws Exception {
//開始生成token
String token = "";
switch (userInfo.getType()) {
case CustomConstants.USER_TYPE_ADMIN:
//管理員竟稳,暫不處理該類型人員
break;
case CustomConstants.USER_TYPE_STUDENT:
//學生
StudentInfo studentInfo = studentInfoMapper.selectById(userInfo.getPersonId());
token = jwtService.
getAuth(CustomConstants.USER_TYPE_STUDENT).
create(new StudentAuthByJWT.Instance(
userInfo.getId(),
queryPermission(userInfo.getUsername()),
userInfo.getUsername(),
userInfo.getType(),
userInfo.getPersonId(),
studentInfo.getName(),
studentInfo.getGender(),
studentInfo.getNum(),
studentInfo.getGradeId()));
break;
case CustomConstants.USER_TYPE_TEACHER:
//老師
TeacherInfo teacherInfo = teacherInfoMapper.selectById(userInfo.getPersonId());
token = jwtService.
getAuth(CustomConstants.USER_TYPE_TEACHER).
create(new TeacherAuthByJWT.Instance(
userInfo.getId(),
queryPermission(userInfo.getUsername()),
userInfo.getUsername(),
userInfo.getType(),
userInfo.getPersonId(),
teacherInfo.getName(),
teacherInfo.getGender(),
teacherInfo.getCollegeId()));
break;
default:
break;
}
return token;
}
}
jwt相關工具類
package com.yezi_tool.demo_basic.commons.utils;
import com.google.common.base.Strings;
import com.google.common.hash.Hashing;
import com.google.gson.Gson;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @author Echo_Ye
* @title jwt工具
* @description 封裝jwt相關方法
* @date 2020/8/26 17:46
* @email echo_yezi@qq.com
*/
@Component
public class JwtUtils {
/**
* gson對象果善,提前初始化
*/
private final static Gson gson = new Gson();
/**
* gson解碼
*
* @param encoded json字符串
* @param t 解碼目標類型
* @param <T> 解碼目標類型
* @return 解碼后的對象
*/
public static <T> T decode(String encoded, Class<T> t) {
return gson.fromJson(encoded, t);
}
/**
* gson編碼
*
* @param t 需要編碼的對象
* @return 編碼后的結果
*/
public static String encode(Object t) {
return gson.toJson(t);
}
/**
* 從http請求中解析token
*
* @param request http請求
* @return token字符串
* @throws Exception 解析異常
*/
public static String getTokenFromHttpRequest(HttpServletRequest request) throws Exception {
String authorization = Strings.nullToEmpty(request.getHeader(JwtConstants.JWT_REQUEST_HEAD_KEY));
if (!authorization.startsWith(JwtConstants.JWT_REQUEST_HEAD_PREFIX)) {
throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CHECK_AUTH));
}
return authorization.substring(7);
}
/**
* 生成鹽
*/
public static String generateSalt() {
return RandomStringUtils.randomAlphanumeric(16);
}
/**
* 密碼加密
*/
public static String encryptPassword(String password, String salt) {
return Hashing.hmacMd5(salt.getBytes()).hashBytes(password.getBytes()).toString();
}
/**
* 密碼驗證
*/
public static boolean verifyPassword(String username,String password, String salt, String encryptedPassword) {
return encryptPassword(Strings.nullToEmpty(encryptedPassword), salt).equals(password);
}
}
控制層接口
package com.yezi_tool.demo_basic.controller;
import com.yezi_tool.demo_basic.commons.constants.ResponseConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.jwt.*;
import com.yezi_tool.demo_basic.service.IUserInfoService;
import lombok.Data;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @author Echo_Ye
* @title 登錄接口
* @description 用于登錄相關接口
* @date 2020/8/17 9:39
* @email echo_yezi@qq.com
*/
@Controller
@RequestMapping("/login")
public class LoginController extends BaseController {
/**
* 用戶信息業(yè)務層
*/
private final IUserInfoService userInfoService;
public LoginController(IUserInfoService userInfoService) {
this.userInfoService = userInfoService;
}
@Data
private static class LoginRequest {
private String username;
private String password;
private Boolean rememberMe;
}
@PostMapping("/login")
@ResponseBody
public ReturnMsg login(@RequestBody LoginRequest loginRequest) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
String token = userInfoService.loginByUserNameAndPassword(loginRequest.getUsername(), loginRequest.getPassword());
returnMsg.setData(token);
return returnMsg;
}
@PostMapping("/testTeacher")
@ResponseBody
@Auth(TeacherAuthByJWT.Instance.class)
public ReturnMsg testTeacher(BaseAuthInstance auth, Integer mark) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
if (mark == null) {
throw new BaseException(ResponseConstants.RETURN_MSG_ABNORMAL_PARAM);
}
returnMsg.setData(auth);
return returnMsg;
}
@PostMapping("/testStudent")
@ResponseBody
@Auth(StudentAuthByJWT.Instance.class)
public ReturnMsg testStudent(BaseAuthInstance auth, Integer mark) throws Exception {
ReturnMsg returnMsg = ReturnMsg.success();
if (mark == null) {
throw new BaseException(ResponseConstants.RETURN_MSG_ABNORMAL_PARAM);
}
returnMsg.setData(auth);
return returnMsg;
}
}
運行截圖
-
登錄
image-20200903172126432 -
驗證
image-20200904171723793
7.3.全部代碼
demo地址:https://gitee.com/echo_ye/jwt-demo
demo已能正常運轉預期所有功能晾匠,但僅供參考混聊,請視實際業(yè)務自行刪減和修改句喜,有疑問或者建議可以留言或者聯(lián)系我~
BB兩句
其實考慮到JWT的弊端咳胃,JWT在與傳統(tǒng)session認證的比較之下展懈,并不具備太多優(yōu)勢存崖,甚至是部分地方有著無法彌補的劣勢
==權衡之下睡毒,個人不建議使用JWT取代session認證==
作者:Echo_Ye
WX:Echo_YeZ
EMAIL :echo_yezi@qq.com
個人站點:在搭了在搭了供搀。葛虐。。(右鍵 - 新建文件夾)