利用Springboot實現(xiàn)Jwt認(rèn)證的示例代碼
JSON Web Token是目前最流行的跨域認(rèn)證解決方案,,適合前后端分離項目通過Restful API進(jìn)行數(shù)據(jù)交互時進(jìn)行身份認(rèn)證

關(guān)于Shiro整合JWT,可以看這里:Springboot實現(xiàn)Shiro+JWT認(rèn)證
概述
由于概念性內(nèi)容網(wǎng)上多的是,所以就不詳細(xì)介紹了
具體可以看這里:阮一峰大佬的博客
我總結(jié)幾個重點:
JWT,全稱Json Web Token,是一種令牌認(rèn)證的方式
長相:

- 頭部:放有簽名算法和令牌類型(這個就是JWT)
- 載荷:你在令牌上附帶的信息:比如用戶的id,用戶的電話號碼,這樣以后驗證了令牌之后就可以直接從這里獲取信息而不用再查數(shù)據(jù)庫了
- 簽名:用來加令牌的
安全性:由于載荷里的內(nèi)容都是用BASE64處理的,所以是沒有保密性的(因為BASE64是對稱的),但是由于簽名認(rèn)證的原因,其他人很難偽造數(shù)據(jù)。不過這也意味著,你不能把敏感信息比如密碼放入載荷中,畢竟這種可以被別人直接看到的,但是像用戶id這種就無所謂了
工作流程
登錄階段
用戶首次登錄,通過賬號密碼比對,判定是否登錄成功,如果登錄成功的話,就生成一個jwt字符串,然后放入一些附帶信息,返回給客戶端。

這個jwt字符串里包含了有用戶的相關(guān)信息,比如這個用戶是誰,他的id是多少,這個令牌的有效時間是多久等等。下次用戶登錄的時候,必須把這個令牌也一起帶上。
認(rèn)證階段
這里需要和前端統(tǒng)一約定好,在發(fā)起請求的時候,會把上次的token放在請求頭里的某個位置一起發(fā)送過來,后端接受到請求之后,會解析jwt,驗證jwt是否合法,有沒有被偽造,是否過期,到這里,驗證過程就完成了。

不過服務(wù)器同樣可以從驗證后的jwt里獲取用戶的相關(guān)信息,從而減少對數(shù)據(jù)庫的查詢。
比如我們有這樣一個業(yè)務(wù):“通過用戶電話號碼查詢用戶余額”
如果我們在jwt的載荷里事先就放有電話號碼這個屬性,那么我們就可以避免先去數(shù)據(jù)庫根據(jù)用戶id查詢用戶電話號碼,而直接拿到電話號碼,然后執(zhí)行接下里的業(yè)務(wù)邏輯。
關(guān)于有效期
由于jwt是直接給用戶的,只要能驗證成功的jwt都可以被視作登錄成功,所以,如果不給jwt設(shè)置一個過期時間的話,用戶只要存著這個jwt,就相當(dāng)于永遠(yuǎn)登錄了,而這是不安全的,因為如果這個令牌泄露了,那么服務(wù)器是沒有任何辦法阻止該令牌的持有者訪問的(因為拿到這個令牌就等于隨便冒充你身份訪問了),所以往往jwt都會有一個有效期,通常存在于載荷部分,下面是一段生成jwt的java代碼:
return JWT.create().withAudience(userId)
.withIssuedAt(new Date()) <---- 發(fā)行時間
.withExpiresAt(expiresDate) <---- 有效期
.withClaim("sessionId", sessionId)
.withClaim("userName", userName)
.withClaim("realName", realName)
.sign(Algorithm.HMAC256(userId+"HelloLehr"));
在實際的開發(fā)中,令牌的有效期往往是越短越安全,因為令牌會頻繁變化,即使有某個令牌被別人盜用,也會很快失效。但是有效期短也會導(dǎo)致用戶體驗不好(總是需要重新登錄),所以這時候就會出現(xiàn)另外一種令牌—refresh token刷新令牌。刷新令牌的有效期會很長,只要刷新令牌沒有過期,就可以再申請另外一個jwt而無需登錄(且這個過程是在用戶訪問某個接口時自動完成的,用戶不會感覺到令牌替換),對于刷新令牌的具體實現(xiàn)這里就不詳細(xì)講啦(其實因為我也沒深入研究過XD…)
對比Session
在傳統(tǒng)的session會話機(jī)制中,服務(wù)器識別用戶是通過用戶首次訪問服務(wù)器的時候,給用戶一個sessionId,然后把用戶對應(yīng)的會話記錄放在服務(wù)器這里,以后每次通過sessionId來找到對應(yīng)的會話記錄。這樣雖然所有的數(shù)據(jù)都存在服務(wù)器上是安全的,但是對于分布式的應(yīng)用來說,就需要考慮session共享的問題了,不然同一個用戶的sessionId的請求被自動分配到另外一個服務(wù)器上就等于失效了
而Jwt不但可以用于登錄認(rèn)證,也把相應(yīng)的數(shù)據(jù)返回給了用戶(就是載荷里的內(nèi)容),通過簽名來保證數(shù)據(jù)的真實性,該應(yīng)用的各個服務(wù)器上都有統(tǒng)一的驗證方法,只要能通過驗證,就說明你的令牌是可信的,我就可以從你的令牌上獲取你的信息,知道你是誰了,從而減輕了服務(wù)器的壓力,而且也對分布式應(yīng)用更為友好。(畢竟就不用擔(dān)心服務(wù)器session的分布式存儲問題了)
整合Springboot
導(dǎo)入java-jwt包
導(dǎo)入java-jwt包:
這個包里實現(xiàn)了一系列jwt操作的api(包括上面講到的怎么校驗,怎么生成jwt等等)
如果你是Maven玩家:
pom.xml里寫入
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.3</version> </dependency>
如果你是Gradle玩家:
build.gradle里寫入
compile group: 'com.auth0', name: 'java-jwt', version: '3.8.3'
如果你是其他玩家:
maven中央倉庫地址點這里
工具類的編寫
代碼如下:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
/**
* @author Lehr
* @create: 2020-02-04
*/
public class JwtUtils {
/**
簽發(fā)對象:這個用戶的id
簽發(fā)時間:現(xiàn)在
有效時間:30分鐘
載荷內(nèi)容:暫時設(shè)計為:這個人的名字,這個人的昵稱
加密密鑰:這個人的id加上一串字符串
*/
public static String createToken(String userId,String realName, String userName) {
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE,30);
Date expiresDate = nowTime.getTime();
return JWT.create().withAudience(userId) //簽發(fā)對象
.withIssuedAt(new Date()) //發(fā)行時間
.withExpiresAt(expiresDate) //有效時間
.withClaim("userName", userName) //載荷,隨便寫幾個都可以
.withClaim("realName", realName)
.sign(Algorithm.HMAC256(userId+"HelloLehr")); //加密
}
/**
* 檢驗合法性,其中secret參數(shù)就應(yīng)該傳入的是用戶的id
* @param token
* @throws TokenUnavailable
*/
public static void verifyToken(String token, String secret) throws TokenUnavailable {
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret+"HelloLehr")).build();
jwt = verifier.verify(token);
} catch (Exception e) {
//效驗失敗
//這里拋出的異常是我自定義的一個異常,你也可以寫成別的
throw new TokenUnavailable();
}
}
/**
* 獲取簽發(fā)對象
*/
public static String getAudience(String token) throws TokenUnavailable {
String audience = null;
try {
audience = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
//這里是token解析失敗
throw new TokenUnavailable();
}
return audience;
}
/**
* 通過載荷名字獲取載荷的值
*/
public static Claim getClaimByName(String token, String name){
return JWT.decode(token).getClaim(name);
}
}
一點小說明:
關(guān)于jwt生成時的加密和驗證方法:
jwt的驗證其實就是驗證jwt最后那一部分(簽名部分)。這里在指定簽名的加密方式的時候,還傳入了一個字符串來加密,所以驗證的時候不但需要知道加密算法,還需要獲得這個字符串才能成功解密,提高了安全性。我這里用的是id來,比較簡單,如果你想更安全一點,可以把用戶密碼作為這個加密字符串,這樣就算是這段業(yè)務(wù)代碼泄露了,也不會引發(fā)太大的安全問題(畢竟我的id是誰都知道的,這樣令牌就可以被偽造,但是如果換成密碼,只要數(shù)據(jù)庫沒事那就沒人知道)
關(guān)于獲得載荷的方法:
可能有人會覺得奇怪,為什么不需要解密不需要verify就能夠獲取到載荷里的內(nèi)容呢?原因是,本來載荷就只是用Base64處理了,就沒有加密性,所以能直接獲取到它的值,但是至于可不可以相信這個值的真實性,就是要看能不能通過驗證了,因為最后的簽名部分是和前面頭部和載荷的內(nèi)容有關(guān)聯(lián)的,所以一旦簽名驗證過了,那就說明前面的載荷是沒有被改過的。
注解類的編寫
在controller層上的每個方法上,可以使用這些注解,來決定訪問這個方法是否需要攜帶token,由于默認(rèn)是全部檢查,所以對于某些特殊接口需要有免驗證注解
免驗證注解
@PassToken:跳過驗證,通常是入口方法上用這個,比如登錄接口
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Lehr
* @create: 2020-02-03
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
攔截器的編寫
配置類
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author lehr
*/
@Configuration
public class JwtInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//默認(rèn)攔截所有路徑
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
@Bean
public JwtAuthenticationInterceptor authenticationInterceptor() {
return new JwtAuthenticationInterceptor();
}
}
攔截器
import com.auth0.jwt.interfaces.Claim;
import com.imlehr.internship.annotation.PassToken;
import com.imlehr.internship.dto.AccountDTO;
import com.imlehr.internship.exception.NeedToLogin;
import com.imlehr.internship.exception.UserNotExist;
import com.imlehr.internship.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @author Lehr
* @create: 2020-02-03
*/
public class JwtAuthenticationInterceptor implements HandlerInterceptor {
@Autowired
AccountService accountService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 從請求頭中取出 token 這里需要和前端約定好把jwt放到請求頭一個叫token的地方
String token = httpServletRequest.getHeader("token");
// 如果不是映射到方法直接通過
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//檢查是否有passtoken注釋,有則跳過認(rèn)證
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//默認(rèn)全部檢查
else {
System.out.println("被jwt攔截需要驗證");
// 執(zhí)行認(rèn)證
if (token == null) {
//這里其實是登錄失效,沒token了 這個錯誤也是我自定義的,讀者需要自己修改
throw new NeedToLogin();
}
// 獲取 token 中的 user Name
String userId = JwtUtils.getAudience(token);
//找找看是否有這個user 因為我們需要檢查用戶是否存在,讀者可以自行修改邏輯
AccountDTO user = accountService.getByUserName(userId);
if (user == null) {
//這個錯誤也是我自定義的
throw new UserNotExist();
}
// 驗證 token
JwtUtils.verifyToken(token, userId)
//獲取載荷內(nèi)容
String userName = JwtUtils.getClaimByName(token, "userName").asString();
String realName = JwtUtils.getClaimByName(token, "realName").asString();
//放入attribute以便后面調(diào)用
request.setAttribute("userName", userName);
request.setAttribute("realName", realName);
return true;
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
這段代碼的執(zhí)行邏輯大概是這樣的:
- 目標(biāo)方法是否有注解?如果有PassToken的話就不用執(zhí)行后面的驗證直接放行,不然全部需要驗證
- 開始驗證:有沒有token?沒有?那么返回錯誤
- 從token的audience中獲取簽發(fā)對象,查看是否有這個用戶(有可能客戶端造假,有可能這個用戶的賬戶被凍結(jié)了),查看用戶的邏輯就是調(diào)用Service方法直接比對即可
- 檢驗Jwt的有效性,如果無效或者過期了就返回錯誤
- Jwt有效性檢驗成功:把Jwt的載荷內(nèi)容獲取到,可以在接下來的controller層中直接使用了(具體使用方法看后面的代碼)
接口的編寫
這里設(shè)計了兩個接口:登錄和查詢名字,來模擬一個迷你業(yè)務(wù),其中后者需要登錄之后才能使用,大致流程如下:

登錄代碼
/**
* 用戶登錄:獲取賬號密碼并登錄,如果不對就報錯,對了就返回用戶的登錄信息
* 同時生成jwt返回給用戶
*
* @return
* @throws LoginFailed 這個LoginFailed也是我自定義的
*/
@PassToken
@GetMapping(value = "/login")
public AccountVO login(String userName, String password) throws LoginFailed{
try{
service.login(userName,password);
}
catch (AuthenticationException e)
{
throw new LoginFailed();
}
//如果成功了,聚合需要返回的信息
AccountVO account = accountService.getAccountByUserName(userName);
//給分配一個token 然后返回
String jwtToken = JwtUtils.createToken(account);
//我的處理方式是把token放到accountVO里去了
account.setToken(jwtToken);
return account;
}
業(yè)務(wù)代碼
這里列舉一個需要登錄,用來測試用戶名字的接口(其中用戶的名字來源于jwt的載荷部分)
@GetMapping(value = "/username")
public String checkName(HttpServletRequest req) {
//之前在攔截器里設(shè)置好的名字現(xiàn)在可以取出來直接用了
String name = (String) req.getAttribute("userName");
return name;
}
到此這篇關(guān)于利用Springboot實現(xiàn)Jwt認(rèn)證的示例代碼的文章就介紹到這了,更多相關(guān)Springboot Jwt認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 詳解Spring Boot實戰(zhàn)之Filter實現(xiàn)使用JWT進(jìn)行接口認(rèn)證
- Vue+Jwt+SpringBoot+Ldap完成登錄認(rèn)證的示例代碼
- Springboot集成Spring Security實現(xiàn)JWT認(rèn)證的步驟詳解
- SpringBoot整合SpringSecurity和JWT和Redis實現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringBoot使用Jwt處理跨域認(rèn)證問題的教程詳解
- 詳解SpringBoot如何使用JWT實現(xiàn)身份認(rèn)證和授權(quán)
- springboot+jwt實現(xiàn)token登陸權(quán)限認(rèn)證的實現(xiàn)
- SpringBoot整合SpringSecurity實現(xiàn)JWT認(rèn)證的項目實踐
- Spring Boot整合JWT實現(xiàn)認(rèn)證與授權(quán)的項目實踐
相關(guān)文章
SpringBoot結(jié)果封裝和異常攔截的實現(xiàn)示例
SpringBoot 項目中,我們通常需要將結(jié)果數(shù)據(jù)封裝成特定的格式,以方便客戶端進(jìn)行處理,本文主要介紹了SpringBoot?優(yōu)雅的結(jié)果封裝和異常攔截,感興趣的可以了解一下2023-08-08
SpringCloud學(xué)習(xí)筆記之Feign遠(yuǎn)程調(diào)用
Feign是一個聲明式的http客戶端。其作用就是幫助我們優(yōu)雅的實現(xiàn)http請求的發(fā)送。本文將具體為大家介紹一下Feign的遠(yuǎn)程調(diào)用,感興趣的可以了解一下2021-12-12
Java CompletableFuture 異步超時實現(xiàn)深入研究
這篇文章主要為大家介紹了Java CompletableFuture 異步超時實現(xiàn)深入研究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
springboot集成springCloud中g(shù)ateway時啟動報錯的解決
這篇文章主要介紹了springboot集成springCloud中g(shù)ateway時啟動報錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
解讀線程池-Executors的newSingleThreadExecutor和newFixedThreadPool(1
這篇文章主要介紹了解讀線程池-Executors的newSingleThreadExecutor和newFixedThreadPool(1)區(qū)別,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08
SpringMVC實現(xiàn)前端后臺交互傳遞數(shù)據(jù)
本篇文章主要介紹了SpringMVC實現(xiàn)前端后臺傳遞數(shù)據(jù)的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03

