SpringBoot JWT實現(xiàn)token登錄刷新功能
1. 什么是JWT
Json web token (JWT) 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標準。簡答理解就是一個身份憑證,用于服務(wù)識別。
JWT本身是無狀態(tài)的,這點有別于傳統(tǒng)的session,不在服務(wù)端存儲憑證。這種特性使其在分布式場景,更便于擴展使用。
2. JWT組成部分
JWT有三部分組成,頭部(header),載荷(payload),是簽名(signature)。
- 頭部
頭部主要聲明了類型(jwt),以及使用的加密算法( HMAC SHA256)
- 載荷
載荷就是存放有自定義信息的地方,例如用戶標識,截止日期等
- 簽名
簽名進行對之前的數(shù)據(jù)添加一層防護,防止被篡改。
簽名生成過程: 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加密分為兩種對稱加密和非對稱加密。
- 對稱加密
對稱加密指使用同一秘鑰進行加密,解密的操作。加密解密的速度比較快,適合數(shù)據(jù)比較長時的使用。常見的算法為DES、3DES等
- 非對稱加密
非對稱指通過公鑰進行加密,通過私鑰進行解密。加密和解密花費的時間長、速度相對較慢,但安全性更高,只適合對少量數(shù)據(jù)的使用。常見的算法RSA、ECC等。
兩種加密方法沒有誰更好,只有哪種場景更合適。
4.實戰(zhàn)
本例采用了spring2.x,jwt使用了nimbus-jose-jwt版本,當然其他的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,前端進行識別,發(fā)起請求刷新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) {
//創(chuàng)建JWS頭,設(shè)置簽名算法和類型
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
.type(JOSEObjectType.JWT)
.build();
//將負載信息封裝到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,默認開啟
*/
boolean token() default true;
/**
* 是否校驗sign,默認關(guān)閉
*/
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 {
//獲取目標對象對應(yīng)的字節(jié)碼對象
Class<?> targetCls=joinPoint.getTarget().getClass();
//獲取方法簽名信息從而獲取方法名和參數(shù)類型
MethodSignature ms= (MethodSignature) joinPoint.getSignature();
//獲取目標方法對象上注解中的屬性值
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é)
許多同學使用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)文章希望大家以后多多支持腳本之家!
- SpringBoot集成Spring Security用JWT令牌實現(xiàn)登錄和鑒權(quán)的方法
- spring boot如何基于JWT實現(xiàn)單點登錄詳解
- SpringBoot+Vue+JWT的前后端分離登錄認證詳細步驟
- SpringBoot使用JWT實現(xiàn)登錄驗證的方法示例
- springboot+jwt+微信小程序授權(quán)登錄獲取token的方法實例
- SpringBoot結(jié)合JWT實現(xiàn)用戶登錄、注冊、鑒權(quán)
- SpringBoot實現(xiàn)登錄校驗(JWT令牌)
- SpringBoot+JWT實現(xiàn)單點登錄完美解決方案
- Spring?Boot?JWT登錄授權(quán)使用(無感刷新)
相關(guān)文章
使用dubbo+zookeeper+spring boot構(gòu)建服務(wù)的方法詳解
這篇文章主要給大家介紹了關(guān)于如何使用dubbo+zookeeper+spring boot構(gòu)建服務(wù)的相關(guān)資料,文中通過示例代碼及圖片介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學習學習吧2018-05-05
hadoop運行java程序(jar包)并運行時動態(tài)指定參數(shù)
這篇文章主要介紹了hadoop如何運行java程序(jar包)并運行時動態(tài)指定參數(shù),使用hadoop 運行 java jar包,Main函數(shù)一定要加上全限定類名,需要的朋友可以參考下2021-06-06
Java刷題之最小k個數(shù)的思路及具體實現(xiàn)
這篇文章主要介紹了Java刷題之最小k個數(shù)的思路及具體實現(xiàn),最小K個數(shù)是一個經(jīng)典的top-K問題,可以通過整體排序、建立小根堆或大根堆的方式解決,排序方式時間復(fù)雜度較高,適合數(shù)據(jù)量小的場景,小根堆適合k較小的情況,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-10-10

