新聞中心
審校 | 梁策 孫淑娟

灤平ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場(chǎng)景,ssl證書(shū)未來(lái)市場(chǎng)廣闊!成為成都創(chuàng)新互聯(lián)的ssl證書(shū)銷售渠道,可以享受市場(chǎng)價(jià)格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:18980820575(備注:SSL證書(shū)合作)期待與您的合作!
為了安全起見(jiàn),使用無(wú)狀態(tài)JWT令牌時(shí)可以使用短時(shí)限TTL(1分鐘)策略,然后這些令牌會(huì)在其生存時(shí)間內(nèi)及時(shí)刷新。如果服務(wù)器不知道用戶何時(shí)注銷,那么可以繼續(xù)刷新已注銷用戶的令牌。本文將提供針對(duì)這個(gè)問(wèn)題的一種解決方案,使之在保持水平擴(kuò)展性的同時(shí)確保安全性能不受影響。
架構(gòu)設(shè)計(jì)
從圖中展示的體系架構(gòu)可見(jiàn),每個(gè)微服務(wù)都有自己的數(shù)據(jù)庫(kù)。被撤銷的令牌和用戶都需要單一(身份)信息源(Single Source of Truth,簡(jiǎn)稱“SSOT”)。數(shù)據(jù)庫(kù)需要具有高可用性,包括多主機(jī)、熱備份及數(shù)據(jù)庫(kù)的其他功能。其中,撤銷的令牌數(shù)據(jù)庫(kù)只需要兩個(gè)表:一個(gè)用于用戶注銷時(shí)來(lái)緩存撤銷的令牌,此令牌由負(fù)責(zé)緩存撤銷令牌表中內(nèi)容的微服務(wù)每90秒調(diào)用一次;另一個(gè)用于用戶登錄。每次注銷后,微服務(wù)都會(huì)在定義的行生存時(shí)間內(nèi)更新撤銷的令牌表,并且登錄是有速度限制的。因此,上述體系結(jié)構(gòu)減少了吊銷令牌數(shù)據(jù)庫(kù)的負(fù)載,使其能夠擴(kuò)展到更大的部署。被撤銷令牌的單一(身份)信息源要求是必要的,因?yàn)槊總€(gè)用戶請(qǐng)求都可以在任何微服務(wù)上處理,并且需要在那里檢查撤銷的令牌。需要通過(guò)用戶表來(lái)支持微服務(wù)登錄用戶。這樣一來(lái)就可以將安全檢查的負(fù)載分散到各個(gè)微服務(wù)。該架構(gòu)中,JWT令牌是在微服務(wù)的內(nèi)存中進(jìn)行檢查的,而且只增加了一點(diǎn)CPU損耗,不要求使用IO負(fù)載。
實(shí)現(xiàn)代碼分析
為了驗(yàn)證上述結(jié)論,我開(kāi)發(fā)了一個(gè)MovieManager項(xiàng)目來(lái)實(shí)現(xiàn)撤銷令牌的處理。首先,我們來(lái)看登錄部分實(shí)現(xiàn)代碼。
登錄操作
為了支持已撤銷的令牌,登錄時(shí)首先要檢查用戶當(dāng)前已撤銷令牌的數(shù)量,并降低登錄速度,以限制用戶可以生成的已撤銷令牌的數(shù)量。這一部分功能是在UserDetailsMgmt服務(wù)中完成的,其中關(guān)鍵部分代碼如下:
private UserDto loginHelp(OptionalentityOpt, String passwd) {
UserDto user = new UserDto();
OptionalmyRole = entityOpt.stream()
.flatMap(myUser -> Arrays.stream(Role.values())
.filter(role1 -> Role.USERS.equals(role1))
.filter(role1 ->
role1.name().equals(myUser.getRoles()))).findAny();
if (myRole.isPresent() && entityOpt.get().isEnabled()
&& this.passwordEncoder.matches(passwd, entityOpt.get().getPassword())) {
CallablecallableTask = () -> this.jwtTokenService
.createToken(entityOpt.get()
.getUsername(), Arrays.asList(myRole.get()), Optional.empty());
try {
String jwtToken = executorService
.schedule(callableTask, 3, TimeUnit.SECONDS).get();
user = this.jwtTokenService
.userNameLogouts(entityOpt.get().getUsername()) > 2 ?
user : this.userMapper.convert(entityOpt.get(),
jwtToken, 0L);
} catch (InterruptedException | ExecutionException e) {
LOG.error("Login failed.", e);
}
}
return user;
}
在上述代碼中,首先篩選出用戶實(shí)體User的可選角色,然后檢查用戶實(shí)體User是否存在以及其是否具有Users角色,是否已啟用,以及密碼是否匹配。
然后,創(chuàng)建一個(gè)Callable來(lái)為用戶創(chuàng)建JWT令牌。該令牌中包含有Username和UUID,用于在注銷時(shí)標(biāo)識(shí)每個(gè)令牌。在不同的線程池上以3秒的延遲執(zhí)行Callable,以限制用戶在更新撤銷的令牌緩存之間可以進(jìn)行的注銷次數(shù)。
接下來(lái),檢查是否為用戶緩存了超過(guò)2個(gè)已撤銷的令牌。如果為真,則拒絕登錄。
總之,上述兩項(xiàng)檢查確??梢韵拗朴脩艨缮傻囊殉蜂N令牌數(shù)量以及登錄時(shí)的負(fù)載。
為了實(shí)現(xiàn)水平可擴(kuò)展性,必須將數(shù)據(jù)表移動(dòng)到RevokedToken數(shù)據(jù)庫(kù)中。
注銷操作
注銷操作是在UserDetailsMgmt服務(wù)中實(shí)現(xiàn),代碼如下:
public Boolean logout(String bearerStr) {
if (!this.jwtTokenService.validateToken(
this.jwtTokenService.resolveToken(bearerStr).orElse(""))) {
throw new AuthenticationException("Invalid token.");
}
String username = this.jwtTokenService.getUsername(
this.jwtTokenService
.resolveToken(bearerStr).orElseThrow(() ->
new AuthenticationException("Invalid bearer string.")));
String uuid = this.jwtTokenService
.getUuid(this.jwtTokenService.resolveToken(bearerStr)
.orElseThrow(() ->
new AuthenticationException("Invalid bearer string.")));
this.userRepository.findByUsername(username).orElseThrow(() ->
new ResourceNotFoundException("Username not found: " + username));
long revokedTokensForUuid = this.revokedTokenRepository.findAll().stream()
.filter(myRevokedToken -> myRevokedToken.getUuid().equals(uuid)
&& myRevokedToken.getName().equalsIgnoreCase(username)).count();
if (revokedTokensForUuid == 0) {
this.revokedTokenRepository.save(new RevokedToken(username, uuid,
LocalDateTime.now()));
} else {
LOG.warn("Duplicate logout for user {}", username);
}
return Boolean.TRUE;
}上述代碼中,首先檢查JWT令牌是否有效。接下來(lái),從JWT令牌中讀取用戶名和UUID。然后,使用令牌中的用戶名檢查用戶表Users中的用戶數(shù)據(jù)。接下來(lái),使用相同的UUID和UserID調(diào)用revokedTokens以進(jìn)行記錄檢查。如果找到對(duì)應(yīng)的記錄,將記錄一條關(guān)于重復(fù)注銷嘗試的警告。如果是第一次注銷JWT令牌,則會(huì)在吊銷的令牌表中創(chuàng)建一個(gè)新的包含當(dāng)前用戶名、UUID及當(dāng)前時(shí)間等數(shù)據(jù)的RevokedToken實(shí)體。
為了實(shí)現(xiàn)水平擴(kuò)展性,也必須將數(shù)據(jù)表移動(dòng)到RevokedToken數(shù)據(jù)庫(kù)。
撤銷令牌緩存更新問(wèn)題
撤銷的令牌緩存任務(wù)是使用CronJobs組件進(jìn)行更新的:
@Scheduled(fixedRate = 90000)
public void updateLoggedOutUsers() {
LOG.info("Update logged out users.");
this.userService.updateLoggedOutUsers();
}
每隔90秒從表中讀取一次數(shù)據(jù)。更新操作在UserDetailsMgmt服務(wù)中處理:
public void updateLoggedOutUsers() {
final List revokedTokens =
new ArrayList(this.revokedTokenRepository.findAll());
this.jwtTokenService.updateLoggedOutUsers(
revokedTokens.stream().filter(myRevokedToken ->
myRevokedToken.getLastLogout() == null ||
!myRevokedToken.getLastLogout()
.isBefore(LocalDateTime.now()
.minusSeconds(LOGOUT_TIMEOUT))).toList());
this.revokedTokenRepository.deleteAll(
revokedTokens.stream().filter(myRevokedToken ->
myRevokedToken.getLastLogout() != null && myRevokedToken
.getLastLogout().isBefore(LocalDateTime.now()
.minusSeconds(LOGOUT_TIMEOUT))).toList());
} 上述代碼中,首先從列表中讀取所有被撤銷的令牌。然后,刪除早于LOGOUT_TIMEOUT(185秒)的記錄;其他的則緩存在JwtTokenService中。
JwtTokenService負(fù)責(zé)管理撤銷令牌的緩存:
public record UserNameUuid(String userName, String uuid) {}
private final List loggedOutUsers =
new CopyOnWriteArrayList<>();
public void updateLoggedOutUsers(List revokedTokens) {
this.loggedOutUsers.clear();
this.loggedOutUsers.addAll(revokedTokens.stream()
.map(myRevokedToken -> new UserNameUuid(myRevokedToken.getName(),
myRevokedToken.getUuid())).toList());
} 上述代碼中,UserNameUuid記錄數(shù)據(jù)中包含具有標(biāo)識(shí)令牌的值。loggedOutUsers列表中提供了已注銷用戶或者已撤銷令牌的用戶名UUID。其中,CopyOnWriteArrayList是線程安全的。
接下來(lái),UpdateLogeDoutUsers方法獲取已撤銷令牌的當(dāng)前列表,清除并更新loggedOutUsers列表——該列表用于令牌驗(yàn)證。
JWT令牌驗(yàn)證
JWT令牌中包含了一個(gè)用戶名和哈希值,這些都需要進(jìn)行驗(yàn)證?,F(xiàn)在,JWT令牌也會(huì)根據(jù)loggedOutUsers列表進(jìn)行檢查,以檢查注銷情況。這部分任務(wù)是在下面的JWT令牌過(guò)濾器部分完成的:
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain filterChain) throws IOException, ServletException {
String token = jwtTokenProvider.resolveToken((HttpServletRequest) req);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = token != null ?
jwtTokenProvider.getAuthentication(token) : null;
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(req, res);
}
上述代碼中,在處理請(qǐng)求前先調(diào)用JwtTokenFilter。首先,從HTTP頭部數(shù)據(jù)中讀出令牌。然后,檢查令牌是否已找到且有效(validateToken(…)。最后,在SecurityContextHolder中創(chuàng)建并設(shè)置身份認(rèn)證。
實(shí)現(xiàn)令牌驗(yàn)證的Java代碼如下所示:
public boolean validateToken(String token) {
try {
Jws claimsJws = Jwts.parserBuilder()
.setSigningKey(this.jwtTokenKey).build().parseClaimsJws(token);
String subject = Optional.ofNullable(
claimsJws.getBody().getSubject()).orElseThrow(() ->
new AuthenticationException("Invalid JWT token"));
String uuid = Optional.ofNullable(claimsJws.getBody()
.get(JwtUtils.UUID, String.class)).orElseThrow(() ->
new AuthenticationException("Invalid JWT token"));
return this.loggedOutUsers.stream().noneMatch(myUserName ->
subject.equalsIgnoreCase(myUserName.userName) &&
uuid.equals(myUserName.uuid));
} catch (JwtException | IllegalArgumentException e) {
throw new AuthenticationException("Expired or invalid JWT token",e);
}
} 在上述代碼中,首先解析令牌,檢查簽名密鑰,讀取聲明:否則,拋出異常。然后,讀取主題(userName)和UUID。接下來(lái),根據(jù)loggedOutUsers檢查令牌。如果所有檢查都正常,則令牌有效,請(qǐng)求得到處理。
小結(jié)
在上述方案中,令牌的生存時(shí)間為60秒。注銷令牌寫(xiě)入撤銷令牌表后,緩存每90秒更新一次。被撤銷的令牌在表中保留185秒。這意味著,每個(gè)令牌在所有緩存中都需要刷新。然后,刷新將失敗,令牌從而不再有效。登錄的速度限制確保用戶可以在撤銷的令牌表中創(chuàng)建的條目數(shù)量有限。所有這些都限制了RevokedToken數(shù)據(jù)庫(kù)上的負(fù)載,從而增加它可以處理的微服務(wù)的數(shù)量。
因此,有了這樣的體系結(jié)構(gòu)便可降低令牌丟失的風(fēng)險(xiǎn)。同時(shí),基于JWT令牌身份驗(yàn)證的分布式安全檢查可讓大部分可拓展性優(yōu)勢(shì)得以保持。
作為補(bǔ)充,對(duì)于微服務(wù)中的同步時(shí)鐘,可以使用NTP技術(shù)實(shí)現(xiàn)。文章??《Ubuntu中的同步技術(shù)》??提供了有關(guān)實(shí)現(xiàn)此技術(shù)的操作指南。另外,文章??《基于Spring+Angular的JWT自刷新解決方案》??中也展示了一個(gè)基于Angular前端如何處理令牌的例子。
譯者介紹
朱先忠,社區(qū)編輯,專家博客、講師,濰坊一所高校計(jì)算機(jī)教師,自由編程界老兵一枚。早期專注各種微軟技術(shù)(編著成ASP.NET AJX、Cocos 2d-X相關(guān)三本技術(shù)圖書(shū)),近十多年投身于開(kāi)源世界(熟悉流行全棧Web開(kāi)發(fā)技術(shù)),了解基于OneNet/AliOS+Arduino/ESP32/樹(shù)莓派等物聯(lián)網(wǎng)開(kāi)發(fā)技術(shù)與Scala+Hadoop+Spark+Flink等大數(shù)據(jù)開(kāi)發(fā)技術(shù)。
原文標(biāo)題:??Scalable JWT Token Revokation in Spring Boot??,作者:Sven Loesekann
文章題目:詳解SpringBoot中的JWT令牌管理策略
文章地址:http://m.fisionsoft.com.cn/article/cojjjso.html


咨詢
建站咨詢
