SpringBoot JWT實現(xiàn)token登錄刷新功能
1. 什么是JWT
Json web token (JWT) 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)。簡答理解就是一個身份憑證,用于服務(wù)識別。
JWT本身是無狀態(tài)的,這點有別于傳統(tǒng)的session,不在服務(wù)端存儲憑證。這種特性使其在分布式場景,更便于擴展使用。
2. JWT組成部分
JWT有三部分組成,頭部(header),載荷(payload),是簽名(signature)。
- 頭部
頭部主要聲明了類型(jwt),以及使用的加密算法( HMAC SHA256)
- 載荷
載荷就是存放有自定義信息的地方,例如用戶標(biāo)識,截止日期等
- 簽名
簽名進(jìn)行對之前的數(shù)據(jù)添加一層防護,防止被篡改。
簽名生成過程: base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進(jìn)行加鹽secret組合加密。
// base64加密后的header和base64加密后的payload使用.連接組成的字符串 String str=base64(header).base64(payload); // 加鹽secret進(jìn)行加密 String sign=HMACSHA256(encodedString, 'secret');
3. JWT加密方式
jwt加密分為兩種對稱加密和非對稱加密。
- 對稱加密
對稱加密指使用同一秘鑰進(jìn)行加密,解密的操作。加密解密的速度比較快,適合數(shù)據(jù)比較長時的使用。常見的算法為DES、3DES等
- 非對稱加密
非對稱指通過公鑰進(jìn)行加密,通過私鑰進(jìn)行解密。加密和解密花費的時間長、速度相對較慢,但安全性更高,只適合對少量數(shù)據(jù)的使用。常見的算法RSA、ECC等。
兩種加密方法沒有誰更好,只有哪種場景更合適。
4.實戰(zhàn)
本例采用了spring2.x,jwt使用了nimbus-jose-jwt版本,當(dāng)然其他的jwt版本也都類似,封裝的都是不錯的。
1.maven關(guān)鍵配置如下
<dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.12.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.72</version> </dependency>
2.jwt工具類
對于這里的秘鑰:采用了userId+salt+uuid的方式保證,即使是同一個用戶每次生成的serect都是不同的
對于校驗token有效性,包含三個過程:
- 格式是否合法
- token是否在有效期內(nèi)
- token是否在刷新的有效期內(nèi)
對于token超過有效期,但在刷新有效期內(nèi),返回特定的code,前端進(jìn)行識別,發(fā)起請求刷新token,達(dá)到用戶無感知的過程。
public class JwtUtil { private static final Logger log = LoggerFactory.getLogger(JwtUtil.class); private static final String BEARER_TYPE = "Bearer"; private static final String PARAM_TOKEN = "token"; /** * 秘鑰 */ private static final String SECRET = "dfg#fh!Fdh3443"; /** * 有效期12小時 */ private static final long EXPIRE_TIME = 12 * 3600 * 1000; /** * 刷新時間7天 */ private static final long REFRESH_TIME = 7 * 24 * 3600 * 1000; public static String generate(PayloadDTO payloadDTO) { //創(chuàng)建JWS頭,設(shè)置簽名算法和類型 JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256) .type(JOSEObjectType.JWT) .build(); //將負(fù)載信息封裝到Payload中 Payload payload = new Payload(JSON.toJSONString(payloadDTO)); //創(chuàng)建JWS對象 JWSObject jwsObject = new JWSObject(jwsHeader, payload); try { //創(chuàng)建HMAC簽名器 JWSSigner jwsSigner = new MACSigner(payloadDTO.getUserId() + SECRET+payloadDTO.getJti()); //簽名 jwsObject.sign(jwsSigner); return jwsObject.serialize(); } catch (JOSEException e) { log.error("jwt生成器異常",e); throw new BizException(TOKEN_SIGNER); } } public static String freshToken(String token) { PayloadDTO payloadDTO; try { //從token中解析JWS對象 JWSObject jwsObject = JWSObject.parse(token); payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO.class); // 校驗格式是否合適 verifyFormat(payloadDTO, jwsObject); }catch (ParseException e) { log.error("jwt解析異常",e); throw new BizException(TOKEN_PARSE); } catch (JOSEException e) { log.error("jwt生成器異常",e); throw new BizException(TOKEN_SIGNER); } // 校驗是否過期,未過期直接返回原token if (payloadDTO.getExp() >= System.currentTimeMillis()) { return token; } // 校驗是否處于刷新時間內(nèi),重新生成token if (payloadDTO.getRef() >= System.currentTimeMillis()) { getRefreshPayload(payloadDTO); return generate(payloadDTO); } throw new BizException(TOKEN_EXP); } private static void verifyFormat(PayloadDTO payloadDTO, JWSObject jwsObject) throws JOSEException { //創(chuàng)建HMAC驗證器 JWSVerifier jwsVerifier = new MACVerifier(payloadDTO.getUserId() + SECRET+payloadDTO.getJti()); if (!jwsObject.verify(jwsVerifier)) { throw new BizException(TOKEN_ERROR); } } public static String getTokenFromHeader(HttpServletRequest request) { // 先從header取值 String value = request.getHeader("Authorization"); if (!StringUtils.hasText(value)) { // header不存在從參數(shù)中獲取 value = request.getParameter(PARAM_TOKEN); if (!StringUtils.hasText(value)) { throw new BizException(TOKEN_MUST); } } if (value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase())) { return value.substring(BEARER_TYPE.length()).trim(); } return value; } public static PayloadDTO verify(String token) { PayloadDTO payloadDTO; try { //從token中解析JWS對象 JWSObject jwsObject = JWSObject.parse(token); payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO.class); // 校驗格式是否合適 verifyFormat(payloadDTO, jwsObject); }catch (ParseException e) { log.error("jwt解析異常",e); throw new BizException(TOKEN_PARSE); } catch (JOSEException e) { log.error("jwt生成器異常",e); throw new BizException(TOKEN_SIGNER); } // 校驗是否過期 if (payloadDTO.getExp() < System.currentTimeMillis()) { // 校驗是否處于刷新時間內(nèi) if (payloadDTO.getRef() >= System.currentTimeMillis()) { throw new BizException(TOKEN_REFRESH); } throw new BizException(TOKEN_EXP); } return payloadDTO; } public static PayloadDTO getDefaultPayload(Long userId) { long currentTimeMillis = System.currentTimeMillis(); PayloadDTO payloadDTO = new PayloadDTO(); payloadDTO.setJti(UUID.randomUUID().toString()); payloadDTO.setExp(currentTimeMillis + EXPIRE_TIME); payloadDTO.setRef(currentTimeMillis + REFRESH_TIME); payloadDTO.setUserId(userId); return payloadDTO; } public static void getRefreshPayload(PayloadDTO payload) { long currentTimeMillis = System.currentTimeMillis(); payload.setJti(UUID.randomUUID().toString()); payload.setExp(currentTimeMillis + EXPIRE_TIME); payload.setRef(currentTimeMillis + REFRESH_TIME); } }
3.權(quán)限攔截
本例中采用了自定義注解+切面的方式來實現(xiàn)token的校驗過程。
自定義Auth注解提供了是否開啟校驗token,sign的選項,實際操作中可以添加更多的功能。
@Target(value = ElementType.METHOD) @Documented @Retention(value = RetentionPolicy.RUNTIME) public @interface Auth { /** * 是否校驗token,默認(rèn)開啟 */ boolean token() default true; /** * 是否校驗sign,默認(rèn)關(guān)閉 */ boolean sign() default false; }
切面部分指定了對Auth進(jìn)行切面,這種方法比采用攔截器方式更加靈活些。
@Component @Aspect public class AuthAspect { @Autowired private HttpServletRequest request; @Pointcut("@annotation(com.rain.jwt.config.Auth)") private void authPointcut(){} @Around("authPointcut()") public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable { //獲取目標(biāo)對象對應(yīng)的字節(jié)碼對象 Class<?> targetCls=joinPoint.getTarget().getClass(); //獲取方法簽名信息從而獲取方法名和參數(shù)類型 MethodSignature ms= (MethodSignature) joinPoint.getSignature(); //獲取目標(biāo)方法對象上注解中的屬性值 Auth auth=ms.getMethod().getAnnotation(Auth.class); // 校驗簽名 if (auth.token()) { String token = JwtUtil.getTokenFromHeader(request); JwtUtil.verify(token); } // 校驗簽名 if (auth.sign()) { // todo } return joinPoint.proceed(); } }
4.測試接口
@RestController @RequestMapping(value="/user") @Api(tags = "用戶") public class UserController { @PostMapping(value = "/login") @Auth(token = false) @ApiOperation("登錄") public Result<String> login(String username,String password) { // 用戶常規(guī)校驗 Long userId = 100L; // 用戶信息存入緩存 // 生成token String token = JwtUtil.generate(JwtUtil.getDefaultPayload(userId)); return Result.success(token); } @GetMapping(value = "refreshToken") @Auth @ApiOperation("刷新token") public Result<String> refreshToken(String token) { String freshToken = JwtUtil.freshToken(token); return Result.success(freshToken); } @GetMapping(value = "test") @Auth @ApiOperation("測試") public Result<String> test() { return Result.success("測試成功"); } }
5.總結(jié)
許多同學(xué)使用jwt經(jīng)常將獲取到的token放在redis中,在服務(wù)器端控制其有效性。這是一種處理token的方式,但這種方式跟jwt的思路是背道而去的,jwt本身就提供了過期的信息,將token的生命周期放入服務(wù)器中,又何必采用jwt的方式呢?直接來個uuid不香么。
最后來個項目地址。
到此這篇關(guān)于SpringBoot JWT實現(xiàn)登錄刷新token的文章就介紹到這了,更多相關(guān)SpringBoot JWT實現(xiàn)token登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Security 中如何讓上級擁有下級的所有權(quán)限(案例分析)
這篇文章主要介紹了Spring Security 中如何讓上級擁有下級的所有權(quán)限,本文通過案例分析給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09spring boot @ResponseBody轉(zhuǎn)換JSON 時 Date 類型處理方法【兩種方法】
這篇文章主要介紹了spring boot @ResponseBody轉(zhuǎn)換JSON 時 Date 類型處理方法,主要給大家介紹Jackson和FastJson兩種方式,每一種方法給大家介紹的都非常詳細(xì),需要的朋友可以參考下2018-08-08SpringBoot實現(xiàn)發(fā)送郵件、發(fā)送微信公眾號推送功能
這篇文章主要介紹了SpringBoot實現(xiàn)發(fā)送郵件、發(fā)送微信公眾號推送功能,這里對成員變量JavaMailSender使用了@Resource注解,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03Springboot中的Validation參數(shù)校驗詳解
這篇文章主要介紹了Springboot中的Validation參數(shù)校驗詳解,Springboot參數(shù)校驗是一種常用的驗證機制,在傳遞參數(shù)時進(jìn)行校驗,以確保參數(shù)的有效性和正確性,該機制可以幫助開發(fā)者在代碼實現(xiàn)前就避免一些常見的錯誤,需要的朋友可以參考下2023-10-10數(shù)組重排序(如何將所有奇數(shù)都放在所有偶數(shù)前面)的深入分析
本篇文章是對數(shù)組重排序(如何將所有奇數(shù)都放在所有偶數(shù)前面)的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06