SpringBoot JWT實(shí)現(xiàn)token登錄刷新功能
1. 什么是JWT
Json web token (JWT) 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)。簡(jiǎn)答理解就是一個(gè)身份憑證,用于服務(wù)識(shí)別。
JWT本身是無(wú)狀態(tài)的,這點(diǎn)有別于傳統(tǒng)的session,不在服務(wù)端存儲(chǔ)憑證。這種特性使其在分布式場(chǎng)景,更便于擴(kuò)展使用。
2. JWT組成部分
JWT有三部分組成,頭部(header),載荷(payload),是簽名(signature)。
- 頭部
頭部主要聲明了類型(jwt),以及使用的加密算法( HMAC SHA256)
- 載荷
載荷就是存放有自定義信息的地方,例如用戶標(biāo)識(shí),截止日期等
- 簽名
簽名進(jìn)行對(duì)之前的數(shù)據(jù)添加一層防護(hù),防止被篡改。
簽名生成過(guò)程: base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過(guò)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加密分為兩種對(duì)稱加密和非對(duì)稱加密。
- 對(duì)稱加密
對(duì)稱加密指使用同一秘鑰進(jìn)行加密,解密的操作。加密解密的速度比較快,適合數(shù)據(jù)比較長(zhǎng)時(shí)的使用。常見的算法為DES、3DES等
- 非對(duì)稱加密
非對(duì)稱指通過(guò)公鑰進(jìn)行加密,通過(guò)私鑰進(jìn)行解密。加密和解密花費(fèi)的時(shí)間長(zhǎng)、速度相對(duì)較慢,但安全性更高,只適合對(duì)少量數(shù)據(jù)的使用。常見的算法RSA、ECC等。
兩種加密方法沒(méi)有誰(shuí)更好,只有哪種場(chǎng)景更合適。
4.實(shí)戰(zhàn)
本例采用了spring2.x,jwt使用了nimbus-jose-jwt版本,當(dāng)然其他的jwt版本也都類似,封裝的都是不錯(cuò)的。
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工具類
對(duì)于這里的秘鑰:采用了userId+salt+uuid的方式保證,即使是同一個(gè)用戶每次生成的serect都是不同的
對(duì)于校驗(yàn)token有效性,包含三個(gè)過(guò)程:
- 格式是否合法
- token是否在有效期內(nèi)
- token是否在刷新的有效期內(nèi)
對(duì)于token超過(guò)有效期,但在刷新有效期內(nèi),返回特定的code,前端進(jìn)行識(shí)別,發(fā)起請(qǐng)求刷新token,達(dá)到用戶無(wú)感知的過(guò)程。
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小時(shí) */ private static final long EXPIRE_TIME = 12 * 3600 * 1000; /** * 刷新時(shí)間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對(duì)象 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對(duì)象 JWSObject jwsObject = JWSObject.parse(token); payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO.class); // 校驗(yàn)格式是否合適 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); } // 校驗(yàn)是否過(guò)期,未過(guò)期直接返回原token if (payloadDTO.getExp() >= System.currentTimeMillis()) { return token; } // 校驗(yàn)是否處于刷新時(shí)間內(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驗(yàn)證器 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對(duì)象 JWSObject jwsObject = JWSObject.parse(token); payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO.class); // 校驗(yàn)格式是否合適 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); } // 校驗(yàn)是否過(guò)期 if (payloadDTO.getExp() < System.currentTimeMillis()) { // 校驗(yàn)是否處于刷新時(shí)間內(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)限攔截
本例中采用了自定義注解+切面的方式來(lái)實(shí)現(xiàn)token的校驗(yàn)過(guò)程。
自定義Auth注解提供了是否開啟校驗(yàn)token,sign的選項(xiàng),實(shí)際操作中可以添加更多的功能。
@Target(value = ElementType.METHOD) @Documented @Retention(value = RetentionPolicy.RUNTIME) public @interface Auth { /** * 是否校驗(yàn)token,默認(rèn)開啟 */ boolean token() default true; /** * 是否校驗(yàn)sign,默認(rèn)關(guān)閉 */ boolean sign() default false; }
切面部分指定了對(duì)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)對(duì)象對(duì)應(yīng)的字節(jié)碼對(duì)象 Class<?> targetCls=joinPoint.getTarget().getClass(); //獲取方法簽名信息從而獲取方法名和參數(shù)類型 MethodSignature ms= (MethodSignature) joinPoint.getSignature(); //獲取目標(biāo)方法對(duì)象上注解中的屬性值 Auth auth=ms.getMethod().getAnnotation(Auth.class); // 校驗(yàn)簽名 if (auth.token()) { String token = JwtUtil.getTokenFromHeader(request); JwtUtil.verify(token); } // 校驗(yàn)簽名 if (auth.sign()) { // todo } return joinPoint.proceed(); } }
4.測(cè)試接口
@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ī)校驗(yàn) 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("測(cè)試") public Result<String> test() { return Result.success("測(cè)試成功"); } }
5.總結(jié)
許多同學(xué)使用jwt經(jīng)常將獲取到的token放在redis中,在服務(wù)器端控制其有效性。這是一種處理token的方式,但這種方式跟jwt的思路是背道而去的,jwt本身就提供了過(guò)期的信息,將token的生命周期放入服務(wù)器中,又何必采用jwt的方式呢?直接來(lái)個(gè)uuid不香么。
最后來(lái)個(gè)項(xiàng)目地址。
到此這篇關(guān)于SpringBoot JWT實(shí)現(xiàn)登錄刷新token的文章就介紹到這了,更多相關(guān)SpringBoot JWT實(shí)現(xiàn)token登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Security 中如何讓上級(jí)擁有下級(jí)的所有權(quán)限(案例分析)
這篇文章主要介紹了Spring Security 中如何讓上級(jí)擁有下級(jí)的所有權(quán)限,本文通過(guò)案例分析給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09spring boot @ResponseBody轉(zhuǎn)換JSON 時(shí) Date 類型處理方法【兩種方法】
這篇文章主要介紹了spring boot @ResponseBody轉(zhuǎn)換JSON 時(shí) Date 類型處理方法,主要給大家介紹Jackson和FastJson兩種方式,每一種方法給大家介紹的都非常詳細(xì),需要的朋友可以參考下2018-08-08SpringBoot實(shí)現(xiàn)發(fā)送郵件、發(fā)送微信公眾號(hào)推送功能
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)發(fā)送郵件、發(fā)送微信公眾號(hào)推送功能,這里對(duì)成員變量JavaMailSender使用了@Resource注解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03Springboot中的Validation參數(shù)校驗(yàn)詳解
這篇文章主要介紹了Springboot中的Validation參數(shù)校驗(yàn)詳解,Springboot參數(shù)校驗(yàn)是一種常用的驗(yàn)證機(jī)制,在傳遞參數(shù)時(shí)進(jìn)行校驗(yàn),以確保參數(shù)的有效性和正確性,該機(jī)制可以幫助開發(fā)者在代碼實(shí)現(xiàn)前就避免一些常見的錯(cuò)誤,需要的朋友可以參考下2023-10-10數(shù)組重排序(如何將所有奇數(shù)都放在所有偶數(shù)前面)的深入分析
本篇文章是對(duì)數(shù)組重排序(如何將所有奇數(shù)都放在所有偶數(shù)前面)的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06