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)。簡答理解就是一個(gè)身份憑證,用于服務(wù)識(shí)別。
JWT本身是無狀態(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ù),防止被篡改。
簽名生成過程: 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加密分為兩種對(duì)稱加密和非對(duì)稱加密。
- 對(duì)稱加密
對(duì)稱加密指使用同一秘鑰進(jìn)行加密,解密的操作。加密解密的速度比較快,適合數(shù)據(jù)比較長時(shí)的使用。常見的算法為DES、3DES等
- 非對(duì)稱加密
非對(duì)稱指通過公鑰進(jìn)行加密,通過私鑰進(jìn)行解密。加密和解密花費(fèi)的時(shí)間長、速度相對(duì)較慢,但安全性更高,只適合對(duì)少量數(shù)據(jù)的使用。常見的算法RSA、ECC等。
兩種加密方法沒有誰更好,只有哪種場(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è)過程:
- 格式是否合法
- token是否在有效期內(nèi)
- token是否在刷新的有效期內(nèi)
對(duì)于token超過有效期,但在刷新有效期內(nèi),返回特定的code,前端進(jìn)行識(shí)別,發(fā)起請(qǐng)求刷新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小時(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)是否過期,未過期直接返回原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)是否過期
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)限攔截
本例中采用了自定義注解+切面的方式來實(shí)現(xiàn)token的校驗(yàn)過程。
自定義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本身就提供了過期的信息,將token的生命周期放入服務(wù)器中,又何必采用jwt的方式呢?直接來個(gè)uuid不香么。
最后來個(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)文章
Docker 部署 SpringBoot 項(xiàng)目整合 Redis 鏡像做訪問計(jì)數(shù)示例代碼
這篇文章主要介紹了Docker 部署 SpringBoot 項(xiàng)目整合 Redis 鏡像做訪問計(jì)數(shù)Demo,本文給大家介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01
淺談Java回收對(duì)象的標(biāo)記和對(duì)象的二次標(biāo)記過程
這篇文章主要介紹了淺談Java回收對(duì)象的標(biāo)記和對(duì)象的二次標(biāo)記過程的相關(guān)內(nèi)容,小編覺得還是挺不錯(cuò)的,這里給大家分享一下,需要的朋友可以參考。2017-10-10
Java訪問Hadoop分布式文件系統(tǒng)HDFS的配置說明
Hadoop的能提供高吞吐量的數(shù)據(jù)訪問,是集群式服務(wù)器的上的數(shù)據(jù)操作利器,這里就來為大家分享Java訪問Hadoop分布式文件系統(tǒng)HDFS的配置說明:2016-06-06
Springboot中配置Mail和普通mail的實(shí)現(xiàn)方式
這篇文章主要介紹了Springboot中配置Mail和普通mail的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03

