利用Springboot實(shí)現(xiàn)Jwt認(rèn)證的示例代碼
JSON Web Token是目前最流行的跨域認(rèn)證解決方案,,適合前后端分離項(xiàng)目通過Restful API進(jìn)行數(shù)據(jù)交互時(shí)進(jìn)行身份認(rèn)證
關(guān)于Shiro整合JWT,可以看這里:Springboot實(shí)現(xiàn)Shiro+JWT認(rèn)證
概述
由于概念性內(nèi)容網(wǎng)上多的是,所以就不詳細(xì)介紹了
具體可以看這里:阮一峰大佬的博客
我總結(jié)幾個(gè)重點(diǎn):
JWT,全稱Json Web Token,是一種令牌認(rèn)證的方式
長(zhǎng)相:
- 頭部:放有簽名算法和令牌類型(這個(gè)就是JWT)
- 載荷:你在令牌上附帶的信息:比如用戶的id,用戶的電話號(hào)碼,這樣以后驗(yàn)證了令牌之后就可以直接從這里獲取信息而不用再查數(shù)據(jù)庫(kù)了
- 簽名:用來加令牌的
安全性:由于載荷里的內(nèi)容都是用BASE64處理的,所以是沒有保密性的(因?yàn)锽ASE64是對(duì)稱的),但是由于簽名認(rèn)證的原因,其他人很難偽造數(shù)據(jù)。不過這也意味著,你不能把敏感信息比如密碼放入載荷中,畢竟這種可以被別人直接看到的,但是像用戶id這種就無所謂了
工作流程
登錄階段
用戶首次登錄,通過賬號(hào)密碼比對(duì),判定是否登錄成功,如果登錄成功的話,就生成一個(gè)jwt字符串,然后放入一些附帶信息,返回給客戶端。
這個(gè)jwt字符串里包含了有用戶的相關(guān)信息,比如這個(gè)用戶是誰(shuí),他的id是多少,這個(gè)令牌的有效時(shí)間是多久等等。下次用戶登錄的時(shí)候,必須把這個(gè)令牌也一起帶上。
認(rèn)證階段
這里需要和前端統(tǒng)一約定好,在發(fā)起請(qǐng)求的時(shí)候,會(huì)把上次的token放在請(qǐng)求頭里的某個(gè)位置一起發(fā)送過來,后端接受到請(qǐng)求之后,會(huì)解析jwt,驗(yàn)證jwt是否合法,有沒有被偽造,是否過期,到這里,驗(yàn)證過程就完成了。
不過服務(wù)器同樣可以從驗(yàn)證后的jwt里獲取用戶的相關(guān)信息,從而減少對(duì)數(shù)據(jù)庫(kù)的查詢。
比如我們有這樣一個(gè)業(yè)務(wù):“通過用戶電話號(hào)碼查詢用戶余額”
如果我們?cè)趈wt的載荷里事先就放有電話號(hào)碼這個(gè)屬性,那么我們就可以避免先去數(shù)據(jù)庫(kù)根據(jù)用戶id查詢用戶電話號(hào)碼,而直接拿到電話號(hào)碼,然后執(zhí)行接下里的業(yè)務(wù)邏輯。
關(guān)于有效期
由于jwt是直接給用戶的,只要能驗(yàn)證成功的jwt都可以被視作登錄成功,所以,如果不給jwt設(shè)置一個(gè)過期時(shí)間的話,用戶只要存著這個(gè)jwt,就相當(dāng)于永遠(yuǎn)登錄了,而這是不安全的,因?yàn)槿绻@個(gè)令牌泄露了,那么服務(wù)器是沒有任何辦法阻止該令牌的持有者訪問的(因?yàn)槟玫竭@個(gè)令牌就等于隨便冒充你身份訪問了),所以往往jwt都會(huì)有一個(gè)有效期,通常存在于載荷部分,下面是一段生成jwt的java代碼:
return JWT.create().withAudience(userId) .withIssuedAt(new Date()) <---- 發(fā)行時(shí)間 .withExpiresAt(expiresDate) <---- 有效期 .withClaim("sessionId", sessionId) .withClaim("userName", userName) .withClaim("realName", realName) .sign(Algorithm.HMAC256(userId+"HelloLehr"));
在實(shí)際的開發(fā)中,令牌的有效期往往是越短越安全,因?yàn)榱钆茣?huì)頻繁變化,即使有某個(gè)令牌被別人盜用,也會(huì)很快失效。但是有效期短也會(huì)導(dǎo)致用戶體驗(yàn)不好(總是需要重新登錄),所以這時(shí)候就會(huì)出現(xiàn)另外一種令牌—refresh token刷新令牌
。刷新令牌的有效期會(huì)很長(zhǎng),只要刷新令牌沒有過期,就可以再申請(qǐng)另外一個(gè)jwt而無需登錄(且這個(gè)過程是在用戶訪問某個(gè)接口時(shí)自動(dòng)完成的,用戶不會(huì)感覺到令牌替換),對(duì)于刷新令牌的具體實(shí)現(xiàn)這里就不詳細(xì)講啦(其實(shí)因?yàn)槲乙矝]深入研究過XD…)
對(duì)比Session
在傳統(tǒng)的session會(huì)話機(jī)制中,服務(wù)器識(shí)別用戶是通過用戶首次訪問服務(wù)器的時(shí)候,給用戶一個(gè)sessionId,然后把用戶對(duì)應(yīng)的會(huì)話記錄放在服務(wù)器這里,以后每次通過sessionId來找到對(duì)應(yīng)的會(huì)話記錄。這樣雖然所有的數(shù)據(jù)都存在服務(wù)器上是安全的,但是對(duì)于分布式的應(yīng)用來說,就需要考慮session共享的問題了,不然同一個(gè)用戶的sessionId的請(qǐng)求被自動(dòng)分配到另外一個(gè)服務(wù)器上就等于失效了
而Jwt不但可以用于登錄認(rèn)證,也把相應(yīng)的數(shù)據(jù)返回給了用戶(就是載荷里的內(nèi)容),通過簽名來保證數(shù)據(jù)的真實(shí)性,該應(yīng)用的各個(gè)服務(wù)器上都有統(tǒng)一的驗(yàn)證方法,只要能通過驗(yàn)證,就說明你的令牌是可信的,我就可以從你的令牌上獲取你的信息,知道你是誰(shuí)了,從而減輕了服務(wù)器的壓力,而且也對(duì)分布式應(yīng)用更為友好。(畢竟就不用擔(dān)心服務(wù)器session的分布式存儲(chǔ)問題了)
整合Springboot
導(dǎo)入java-jwt包
導(dǎo)入java-jwt
包:
這個(gè)包里實(shí)現(xiàn)了一系列jwt操作的api(包括上面講到的怎么校驗(yàn),怎么生成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中央倉(cāng)庫(kù)地址點(diǎn)這里
工具類的編寫
代碼如下:
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ā)對(duì)象:這個(gè)用戶的id 簽發(fā)時(shí)間:現(xiàn)在 有效時(shí)間:30分鐘 載荷內(nèi)容:暫時(shí)設(shè)計(jì)為:這個(gè)人的名字,這個(gè)人的昵稱 加密密鑰:這個(gè)人的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ā)對(duì)象 .withIssuedAt(new Date()) //發(fā)行時(shí)間 .withExpiresAt(expiresDate) //有效時(shí)間 .withClaim("userName", userName) //載荷,隨便寫幾個(gè)都可以 .withClaim("realName", realName) .sign(Algorithm.HMAC256(userId+"HelloLehr")); //加密 } /** * 檢驗(yàn)合法性,其中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) { //效驗(yàn)失敗 //這里拋出的異常是我自定義的一個(gè)異常,你也可以寫成別的 throw new TokenUnavailable(); } } /** * 獲取簽發(fā)對(duì)象 */ 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); } }
一點(diǎn)小說明:
關(guān)于jwt生成時(shí)的加密和驗(yàn)證方法:
jwt的驗(yàn)證其實(shí)就是驗(yàn)證jwt最后那一部分(簽名部分)。這里在指定簽名的加密方式的時(shí)候,還傳入了一個(gè)字符串來加密,所以驗(yàn)證的時(shí)候不但需要知道加密算法,還需要獲得這個(gè)字符串才能成功解密,提高了安全性。我這里用的是id來,比較簡(jiǎn)單,如果你想更安全一點(diǎn),可以把用戶密碼作為這個(gè)加密字符串,這樣就算是這段業(yè)務(wù)代碼泄露了,也不會(huì)引發(fā)太大的安全問題(畢竟我的id是誰(shuí)都知道的,這樣令牌就可以被偽造,但是如果換成密碼,只要數(shù)據(jù)庫(kù)沒事那就沒人知道)
關(guān)于獲得載荷的方法:
可能有人會(huì)覺得奇怪,為什么不需要解密不需要verify就能夠獲取到載荷里的內(nèi)容呢?原因是,本來載荷就只是用Base64處理了,就沒有加密性,所以能直接獲取到它的值,但是至于可不可以相信這個(gè)值的真實(shí)性,就是要看能不能通過驗(yàn)證了,因?yàn)樽詈蟮暮灻糠质呛颓懊骖^部和載荷的內(nèi)容有關(guān)聯(lián)的,所以一旦簽名驗(yàn)證過了,那就說明前面的載荷是沒有被改過的。
注解類的編寫
在controller層上的每個(gè)方法上,可以使用這些注解,來決定訪問這個(gè)方法是否需要攜帶token,由于默認(rèn)是全部檢查,所以對(duì)于某些特殊接口需要有免驗(yàn)證注解
免驗(yàn)證注解
@PassToken
:跳過驗(yàn)證,通常是入口方法上用這個(gè),比如登錄接口
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 { // 從請(qǐng)求頭中取出 token 這里需要和前端約定好把jwt放到請(qǐng)求頭一個(gè)叫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攔截需要驗(yàn)證"); // 執(zhí)行認(rèn)證 if (token == null) { //這里其實(shí)是登錄失效,沒token了 這個(gè)錯(cuò)誤也是我自定義的,讀者需要自己修改 throw new NeedToLogin(); } // 獲取 token 中的 user Name String userId = JwtUtils.getAudience(token); //找找看是否有這個(gè)user 因?yàn)槲覀冃枰獧z查用戶是否存在,讀者可以自行修改邏輯 AccountDTO user = accountService.getByUserName(userId); if (user == null) { //這個(gè)錯(cuò)誤也是我自定義的 throw new UserNotExist(); } // 驗(yàn)證 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í)行后面的驗(yàn)證直接放行,不然全部需要驗(yàn)證
- 開始驗(yàn)證:有沒有token?沒有?那么返回錯(cuò)誤
- 從token的audience中獲取簽發(fā)對(duì)象,查看是否有這個(gè)用戶(有可能客戶端造假,有可能這個(gè)用戶的賬戶被凍結(jié)了),查看用戶的邏輯就是調(diào)用Service方法直接比對(duì)即可
- 檢驗(yàn)Jwt的有效性,如果無效或者過期了就返回錯(cuò)誤
- Jwt有效性檢驗(yàn)成功:把Jwt的載荷內(nèi)容獲取到,可以在接下來的controller層中直接使用了(具體使用方法看后面的代碼)
接口的編寫
這里設(shè)計(jì)了兩個(gè)接口:登錄和查詢名字,來模擬一個(gè)迷你業(yè)務(wù),其中后者需要登錄之后才能使用,大致流程如下:
登錄代碼
/** * 用戶登錄:獲取賬號(hào)密碼并登錄,如果不對(duì)就報(bào)錯(cuò),對(duì)了就返回用戶的登錄信息 * 同時(shí)生成jwt返回給用戶 * * @return * @throws LoginFailed 這個(gè)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); //給分配一個(gè)token 然后返回 String jwtToken = JwtUtils.createToken(account); //我的處理方式是把token放到accountVO里去了 account.setToken(jwtToken); return account; }
業(yè)務(wù)代碼
這里列舉一個(gè)需要登錄,用來測(cè)試用戶名字的接口(其中用戶的名字來源于jwt的載荷部分)
@GetMapping(value = "/username") public String checkName(HttpServletRequest req) { //之前在攔截器里設(shè)置好的名字現(xiàn)在可以取出來直接用了 String name = (String) req.getAttribute("userName"); return name; }
到此這篇關(guān)于利用Springboot實(shí)現(xiàn)Jwt認(rèn)證的示例代碼的文章就介紹到這了,更多相關(guān)Springboot Jwt認(rèn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項(xiàng)目實(shí)現(xiàn)關(guān)閉數(shù)據(jù)庫(kù)配置和springSecurity
這篇文章主要介紹了SpringBoot項(xiàng)目實(shí)現(xiàn)關(guān)閉數(shù)據(jù)庫(kù)配置和springSecurity的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08java 最新Xss攻擊與防護(hù)(全方位360°詳解)
這篇文章主要介紹了java 最新Xss攻擊與防護(hù)(全方位360°詳解),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Java實(shí)戰(zhàn)之課程信息管理系統(tǒng)的實(shí)現(xiàn)
這篇文章主要介紹了如何利用Java實(shí)現(xiàn)課程信息管理系統(tǒng),文中采用到的技術(shù)有:Springboot、SpringMVC、MyBatis、FreeMarker等,感興趣的可以了解一下2022-04-04MyBatis利用MyCat實(shí)現(xiàn)多租戶的簡(jiǎn)單思路分享
這篇文章主要給大家介紹了關(guān)于MyBatis利用MyCat實(shí)現(xiàn)多租戶的簡(jiǎn)單思路的相關(guān)資料,文中的多租戶是基于多數(shù)據(jù)庫(kù)進(jìn)行實(shí)現(xiàn)的,數(shù)據(jù)是通過不同數(shù)據(jù)庫(kù)進(jìn)行隔離,需要的朋友可以參考借鑒,下面來一起看看吧。2017-06-06Java concurrency之互斥鎖_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
本文通過示例代碼給大家介紹了Java concurrency之互斥鎖的相關(guān)知識(shí),非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-06-06