您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何在Springboot項目中實現一個Jwt認證功能,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
JSON Web Token是目前最流行的跨域認證解決方案,,適合前后端分離項目通過Restful API進行數據交互時進行身份認證
關于Shiro整合JWT,可以看這里:Springboot實現Shiro+JWT認證
由于概念性內容網上多的是,所以就不詳細介紹了
具體可以看這里:阮一峰大佬的博客
我總結幾個重點:
JWT,全稱Json Web Token,是一種令牌認證的方式
長相:
頭部:放有簽名算法和令牌類型(這個就是JWT)
載荷:你在令牌上附帶的信息:比如用戶的id,用戶的電話號碼,這樣以后驗證了令牌之后就可以直接從這里獲取信息而不用再查數據庫了
簽名:用來加令牌的
安全性:由于載荷里的內容都是用BASE64處理的,所以是沒有保密性的(因為BASE64是對稱的),但是由于簽名認證的原因,其他人很難偽造數據。不過這也意味著,你不能把敏感信息比如密碼放入載荷中,畢竟這種可以被別人直接看到的,但是像用戶id這種就無所謂了
用戶首次登錄,通過賬號密碼比對,判定是否登錄成功,如果登錄成功的話,就生成一個jwt字符串,然后放入一些附帶信息,返回給客戶端。
這個jwt字符串里包含了有用戶的相關信息,比如這個用戶是誰,他的id是多少,這個令牌的有效時間是多久等等。下次用戶登錄的時候,必須把這個令牌也一起帶上。
這里需要和前端統一約定好,在發起請求的時候,會把上次的token放在請求頭里的某個位置一起發送過來,后端接受到請求之后,會解析jwt,驗證jwt是否合法,有沒有被偽造,是否過期,到這里,驗證過程就完成了。
不過服務器同樣可以從驗證后的jwt里獲取用戶的相關信息,從而減少對數據庫的查詢。
比如我們有這樣一個業務:“通過用戶電話號碼查詢用戶余額”
如果我們在jwt的載荷里事先就放有電話號碼這個屬性,那么我們就可以避免先去數據庫根據用戶id查詢用戶電話號碼,而直接拿到電話號碼,然后執行接下里的業務邏輯。
由于jwt是直接給用戶的,只要能驗證成功的jwt都可以被視作登錄成功,所以,如果不給jwt設置一個過期時間的話,用戶只要存著這個jwt,就相當于永遠登錄了,而這是不安全的,因為如果這個令牌泄露了,那么服務器是沒有任何辦法阻止該令牌的持有者訪問的(因為拿到這個令牌就等于隨便冒充你身份訪問了),所以往往jwt都會有一個有效期,通常存在于載荷部分,下面是一段生成jwt的java代碼:
return JWT.create().withAudience(userId) .withIssuedAt(new Date()) <---- 發行時間 .withExpiresAt(expiresDate) <---- 有效期 .withClaim("sessionId", sessionId) .withClaim("userName", userName) .withClaim("realName", realName) .sign(Algorithm.HMAC256(userId+"HelloLehr"));
在實際的開發中,令牌的有效期往往是越短越安全,因為令牌會頻繁變化,即使有某個令牌被別人盜用,也會很快失效。但是有效期短也會導致用戶體驗不好(總是需要重新登錄),所以這時候就會出現另外一種令牌—refresh token刷新令牌
。刷新令牌的有效期會很長,只要刷新令牌沒有過期,就可以再申請另外一個jwt而無需登錄(且這個過程是在用戶訪問某個接口時自動完成的,用戶不會感覺到令牌替換),對于刷新令牌的具體實現這里就不詳細講啦(其實因為我也沒深入研究過XD…)
在傳統的session會話機制中,服務器識別用戶是通過用戶首次訪問服務器的時候,給用戶一個sessionId,然后把用戶對應的會話記錄放在服務器這里,以后每次通過sessionId來找到對應的會話記錄。這樣雖然所有的數據都存在服務器上是安全的,但是對于分布式的應用來說,就需要考慮session共享的問題了,不然同一個用戶的sessionId的請求被自動分配到另外一個服務器上就等于失效了
而Jwt不但可以用于登錄認證,也把相應的數據返回給了用戶(就是載荷里的內容),通過簽名來保證數據的真實性,該應用的各個服務器上都有統一的驗證方法,只要能通過驗證,就說明你的令牌是可信的,我就可以從你的令牌上獲取你的信息,知道你是誰了,從而減輕了服務器的壓力,而且也對分布式應用更為友好。(畢竟就不用擔心服務器session的分布式存儲問題了)
導入java-jwt
包:
這個包里實現了一系列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 { /** 簽發對象:這個用戶的id 簽發時間:現在 有效時間:30分鐘 載荷內容:暫時設計為:這個人的名字,這個人的昵稱 加密密鑰:這個人的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) //簽發對象 .withIssuedAt(new Date()) //發行時間 .withExpiresAt(expiresDate) //有效時間 .withClaim("userName", userName) //載荷,隨便寫幾個都可以 .withClaim("realName", realName) .sign(Algorithm.HMAC256(userId+"HelloLehr")); //加密 } /** * 檢驗合法性,其中secret參數就應該傳入的是用戶的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(); } } /** * 獲取簽發對象 */ 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); } }
一點小說明:
關于jwt生成時的加密和驗證方法:
jwt的驗證其實就是驗證jwt最后那一部分(簽名部分)。這里在指定簽名的加密方式的時候,還傳入了一個字符串來加密,所以驗證的時候不但需要知道加密算法,還需要獲得這個字符串才能成功解密,提高了安全性。我這里用的是id來,比較簡單,如果你想更安全一點,可以把用戶密碼作為這個加密字符串,這樣就算是這段業務代碼泄露了,也不會引發太大的安全問題(畢竟我的id是誰都知道的,這樣令牌就可以被偽造,但是如果換成密碼,只要數據庫沒事那就沒人知道)
關于獲得載荷的方法:
可能有人會覺得奇怪,為什么不需要解密不需要verify就能夠獲取到載荷里的內容呢?原因是,本來載荷就只是用Base64處理了,就沒有加密性,所以能直接獲取到它的值,但是至于可不可以相信這個值的真實性,就是要看能不能通過驗證了,因為最后的簽名部分是和前面頭部和載荷的內容有關聯的,所以一旦簽名驗證過了,那就說明前面的載荷是沒有被改過的。
在controller層上的每個方法上,可以使用這些注解,來決定訪問這個方法是否需要攜帶token,由于默認是全部檢查,所以對于某些特殊接口需要有免驗證注解
免驗證注解
@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) { //默認攔截所有路徑 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注釋,有則跳過認證 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //默認全部檢查 else { System.out.println("被jwt攔截需要驗證"); // 執行認證 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) //獲取載荷內容 String userName = JwtUtils.getClaimByName(token, "userName").asString(); String realName = JwtUtils.getClaimByName(token, "realName").asString(); //放入attribute以便后面調用 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 { } }
這段代碼的執行邏輯大概是這樣的:
目標方法是否有注解?如果有PassToken的話就不用執行后面的驗證直接放行,不然全部需要驗證
開始驗證:有沒有token?沒有?那么返回錯誤
從token的audience中獲取簽發對象,查看是否有這個用戶(有可能客戶端造假,有可能這個用戶的賬戶被凍結了),查看用戶的邏輯就是調用Service方法直接比對即可
檢驗Jwt的有效性,如果無效或者過期了就返回錯誤
Jwt有效性檢驗成功:把Jwt的載荷內容獲取到,可以在接下來的controller層中直接使用了(具體使用方法看后面的代碼)
這里設計了兩個接口:登錄和查詢名字,來模擬一個迷你業務,其中后者需要登錄之后才能使用,大致流程如下:
登錄代碼
/** * 用戶登錄:獲取賬號密碼并登錄,如果不對就報錯,對了就返回用戶的登錄信息 * 同時生成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; }
業務代碼
這里列舉一個需要登錄,用來測試用戶名字的接口(其中用戶的名字來源于jwt的載荷部分)
@GetMapping(value = "/username") public String checkName(HttpServletRequest req) { //之前在攔截器里設置好的名字現在可以取出來直接用了 String name = (String) req.getAttribute("userName"); return name; }
關于如何在Springboot項目中實現一個Jwt認證功能就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。