SpringBoot JWT實現token登入重新整理功能
目錄
- 1. 什麼是JWT
- 2. JWT組成部分
- 3. JWT加密方式
- 4.實戰
- 5.總結
1. 什麼是JWT
on web token (JWT) 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準。簡答理解就是一個身份憑證,用於服務識別。
JWT本身是無狀態的,這點有別於傳統的session,不在服務端儲存憑證。這種特性使其在分散式場景,更便於擴充套件使用。
2. JWT組成部分
JWT有三部分組成,頭部(header),載荷(payload),是簽名(signature)。
- 頭部
頭部主要聲明瞭型別(jwt),以及使用的加密演算法( HMAC SHA256)
- 載荷
載荷就是存放有自定義資訊的地方,例如使用者標識,截止日期等
- 簽名
簽名進行對之前的資料新增一層防護,防止被篡改。
簽名生成過程: base64加密後的header和base64加密後的payload使用.連線組成的字串,然後通過header中宣告的加密方式進行加鹽secret組合加密。
// base64加密後的header和base64加密後的payload使用.連線組成的字串 String str=base64(header).base64(payload); // 加鹽secret進行加密 String sign=HMACSHA256(encodedString,'secret');
3. JWT加密方式
jwt加密分為兩種對稱加密和非對稱加密。
- 對稱加密
對稱加密指使用同一祕鑰進行加密,解密的操作。加密解密的速度比較快,適合資料比較長時的使用。常見的演算法為DES、3DES等
- 非對稱加密
非對稱指通過公鑰進行加密,通過私鑰進行解密。加密和解密花費的時間長、速度相對較慢,但安全性更高,只適合對少量資料的使用。常見的演算法RSA、ECC等。
兩種加密方法沒有誰更好,只有哪種場景更合適。
4.實戰
本例採用了spring2.x,jwt使用了nimbus-jose-jwt版本,當然其他的jwt版本也都類似,封裝的都是不錯的。
1.maven關鍵配置如下
<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是否在有效期內
- token是否在重新整理的有效期內
對於token超過有效期,但在重新整理有效期內,返回特定的code,前端進行識別,發起請求重新整理token,達到使用者無感知的過程。
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) { //建立JWS頭,設定簽名演算法和型別 JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256) .type(JOSEObjectType.JWT) .build(); //將負載資訊封裝到Payload中 Payload payload = new Payload(JSON.toJSONString(payloadDTO)); //建立JWS物件 JWSObject jwsObject = new JWSObject(jwsHeader,payload); try { //建立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; } // 校驗是否處於重新整理時間內,重新生成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 { //建立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不存在從引數中獲取 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(),e); throw new BizException(TOKEN_SIGNER); } // 校驗是否過期 if (payloadDTO.getExp() < System.currentTimeMillis()) { // 校驗是否處於重新整理時間內 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.許可權攔截
本例中採用了自定義註解+切面的方式來實現token的校驗過程。
自定義Auth註解提供了是否開啟校驗token,sign的選項,實際操作中可以新增更多的功能。
@Target(value = ElementType.METHOD) @Documented @Retention(value = RetentionPolicy.RUNTIME) public @interface Auth { /** * 是否校驗token,預設開啟 */ boolean token() default true; /** * 是否校驗sign,預設關閉 */ boolean sign() default false; }
切面部分指定了對Auth進行切面,這種方法比採用攔截器方式更加靈活些。
@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 { //獲取目標物件對應的位元組碼物件 Class<?> targetCls=joinPoint.getTarget().getClass(); //獲取方法簽名信息從而獲取方法名和引數型別 MethodSignature ms= (MethodSignature) joinPoint.getSignature(); //獲取目標方法物件上註解中的屬性值 Auth auth=ms.getMethod().getAnnotation(Auth.class); // 校驗簽名 if (auth.token()) { String token = JwtUtil.getTokenFromHeader(request); JwtUtil.verify(token); www.cppcns.com } // 校驗簽名 if (auth.sign()) { // todo } return joinPoint.proceed(); } }
4.測試介面
@RestController @RequestMapping(value="/user") @Api(tags = "使用者") public class UserController { @PostMapping(value = "/login") @Auth(token = false) @ApiOperation("登入http://www.cppcns.com") public Result<String> login(String username,String password) { // 使用者常規校驗 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.freshTohttp://www.cppcns.comken(token); return Result.success(freshToken); } @GetMapping(value = "test") @Auth @ApiOperation("測試") public Result<String> test() { return Result.success("測試成功"); } }
5.總結
許多同學使用jwt經常將獲取到的token放在redis中,在伺服器端控制其有效性。這是一種處理token的方式,但這種方式跟jwt的思路是背道而去的,jwt本身就提供了過期的資訊,將token的生命週期放入伺服器中,又何必採用jwt的方式呢?直接來個uuid不香麼。
最後來個專案地址。
到此這篇關於SpringBoot JWT實現登入重新整理token的文章就介紹到這了,更多相關SpringBoot JWT實現token登入內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!