SpringBoot基于Redis+Token實(shí)現(xiàn)分布式登錄系統(tǒng)
1. 設(shè)計(jì)思路
在分布式系統(tǒng)或前后端分離的架構(gòu)中,我們需要一種無狀態(tài)的登錄方案。核心設(shè)計(jì)思路如下:
- 憑證機(jī)制:使用 Token(隨機(jī)字符串)作為用戶身份的唯一標(biāo)識,替代傳統(tǒng)的 Cookie。
- 數(shù)據(jù)存儲:將用戶的登錄狀態(tài)(Token 與用戶信息的映射)存儲在 Redis 中,利用其高性能和自動(dòng)過期機(jī)制。
- 狀態(tài)管理:
- Redis:作為服務(wù)端共享的“會話存儲中心”。
- ThreadLocal:作為單次請求內(nèi)的“上下文容器”,方便 Controller 和 Service 層獲取用戶信息。
- 請求攔截:采用 雙攔截器模式,分離“Token 刷新”與“登錄鑒權(quán)”的職責(zé)。
2. 代碼實(shí)現(xiàn)
2.1 步驟一:發(fā)送短信驗(yàn)證碼
驗(yàn)證碼需要有短期的有效期(如 2 分鐘),適合使用 Redis 的 String 結(jié)構(gòu)存儲。
- Key:
login:code:{手機(jī)號} - Value: 驗(yàn)證碼數(shù)字
@Override
public Result sendCode(String phone) {
// 1. 校驗(yàn)手機(jī)號格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機(jī)號格式錯(cuò)誤");
}
// 2. 生成 6 位隨機(jī)驗(yàn)證碼
String code = RandomUtil.randomNumbers(6);
// 3. 保存驗(yàn)證碼到 Redis
// 設(shè)置 2 分鐘過期
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, 2, TimeUnit.MINUTES);
// 4. 模擬發(fā)送短信(實(shí)際項(xiàng)目中調(diào)用短信服務(wù)商 API)
log.debug("發(fā)送驗(yàn)證碼成功,驗(yàn)證碼:{}", code);
return Result.ok();
}
2.2 步驟二:登錄并生成 Token
登錄成功后,我們需要生成 Token,并將用戶對象存儲為 Redis 的 Hash 結(jié)構(gòu)。Hash 結(jié)構(gòu)適合存儲對象,且內(nèi)存占用更優(yōu)。
- Key:
login:token:{隨機(jī)Token} - Value: User 對象的字段映射 (Hash)
@Override
public Result login(LoginFormDTO loginForm) {
String phone = loginForm.getPhone();
// 1. 從 Redis 獲取驗(yàn)證碼并校驗(yàn)
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
return Result.fail("驗(yàn)證碼錯(cuò)誤");
}
// 2. 根據(jù)手機(jī)號查詢用戶(若不存在則注冊,代碼略)
User user = query().eq("phone", phone).one();
if (user == null) {
user = createUserWithPhone(phone);
}
// 3. 生成隨機(jī) Token (作為登錄令牌)
String token = UUID.randomUUID().toString(true);
// 4. 將 User 對象轉(zhuǎn)為 HashMap 存儲
// 核心細(xì)節(jié):StringRedisTemplate 要求 Value 必須是 String,
// 所以必須將 Long 類型的 ID 強(qiáng)轉(zhuǎn)為 String,否則會報(bào) ClassCastException
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 5. 存儲到 Redis
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 6. 設(shè)置有效期(例如 30 分鐘)
stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 7. 返回 Token 給前端
return Result.ok(token);
}
Redis 數(shù)據(jù)庫 │ ├── Key: "login:token:xyz-123" <-- 這是你設(shè)置的 Redis Key (Token) │ │ │ └── Value (Hash結(jié)構(gòu)) <-- 這是一個(gè)大容器 │ ├── Field: "id" --> Value: "101" (必須是 String) │ ├── Field: "nickName" --> Value: "小明" (必須是 String) │ └── Field: "icon" --> Value: "/a.jpg" (必須是 String)
2.3 步驟三:構(gòu)建“雙攔截器”防御體系
為了實(shí)現(xiàn) “用戶只要在使用 App,登錄狀態(tài)就一直有效” 的無感刷新體驗(yàn),我們將攔截器拆分為兩層。
第一層:Token 刷新攔截器 (RefreshTokenInterceptor)
- 攔截路徑:所有路徑 (
/**) - 職責(zé):只要攜帶了 Token,就刷新 Redis 有效期,并保存用戶信息到
ThreadLocal。不負(fù)責(zé)攔截請求。
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
// 構(gòu)造函數(shù)注入 StringRedisTemplate
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 獲取請求頭中的 token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true; // 沒 Token 也放行,交給下一道攔截器
}
// 2. 基于 token 獲取 Redis 中的用戶
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3. 判斷用戶是否存在
if (userMap.isEmpty()) {
return true; // Token 無效或過期,也放行
}
// 4. 將查詢到的 Map 轉(zhuǎn)為 UserDTO 對象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 5. 保存到 ThreadLocal (供后續(xù)業(yè)務(wù)使用)
UserHolder.saveUser(userDTO);
// 6. 【核心動(dòng)作】刷新 Token 有效期
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 7. 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 請求結(jié)束,清理 ThreadLocal,防止內(nèi)存泄漏
UserHolder.removeUser();
}
}
第二層:登錄攔截器 (LoginInterceptor)
- 攔截路徑:敏感業(yè)務(wù)路徑 (排除首頁、登錄接口等)
- 職責(zé):只檢查
ThreadLocal是否有用戶。如果沒有,則攔截并返回 401。
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 判斷 ThreadLocal 中是否有用戶
if (UserHolder.getUser() == null) {
// 2. 沒有用戶,攔截并返回 401 (未授權(quán))
response.setStatus(401);
return false;
}
// 3. 有用戶,放行
return true;
}
}
2.4 步驟四:配置攔截器 (MvcConfig)
在配置類中注冊這兩個(gè)攔截器,并通過 order 控制執(zhí)行順序:先刷新,后攔截。
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 1. 注冊刷新攔截器 (Order = 0, 最先執(zhí)行)
// 攔截所有請求,確保用戶訪問任何頁面都能刷新 Token
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**")
.order(0);
// 2. 注冊登錄攔截器 (Order = 1, 后執(zhí)行)
// 只攔截需要保護(hù)的接口
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/login",
"/user/code",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
).order(1);
}
}
3. 業(yè)務(wù)層使用
在 Controller 或 Service 層,無需再操作 Request 或 Redis,直接從 ThreadLocal 獲取當(dāng)前登錄用戶即可。
@GetMapping("/me")
public Result me(){
// 直接獲取當(dāng)前用戶
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
4. 總結(jié)
這套 Redis + Token + 雙攔截器 的方案具有以下優(yōu)勢:
- 高性能:Redis 的讀寫速度極快,不會拖慢請求響應(yīng)。
- 無感刷新:通過
RefreshTokenInterceptor,解決了用戶在使用過程4中 Token 突然過期的問題。 - 職責(zé)單一:登錄邏輯只負(fù)責(zé)生成 Token,攔截器只負(fù)責(zé)鑒權(quán),業(yè)務(wù)層只負(fù)責(zé)業(yè)務(wù),代碼結(jié)構(gòu)清晰,耦合度低。
到此這篇關(guān)于SpringBoot基于Redis+Token實(shí)現(xiàn)分布式登錄系統(tǒng)的文章就介紹到這了,更多相關(guān)SpringBoot 分布式登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java使用DOM4J對XML文件進(jìn)行增刪改查操作
這篇文章主要為大家詳細(xì)介紹了java使用DOM4J對XML文件進(jìn)行增刪改查操作,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
SpringBoot整合Shiro兩種方式(總結(jié))
這篇文章主要介紹了SpringBoot整合Shiro兩種方式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
java非遞歸實(shí)現(xiàn)之二叉樹的前中后序遍歷詳解
樹的遍歷順序大體分為三種:前序遍歷(先根遍歷、先序遍歷),中序遍歷(中根遍歷),后序遍歷(后根遍歷),本文將給大家詳細(xì)的介紹,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值2021-09-09
Ubuntu環(huán)境下的 RabbitMQ 安裝與配置詳細(xì)指南
本文詳解Ubuntu下RabbitMQ安裝與配置,涵蓋Erlang依賴安裝、服務(wù)部署、管理界面啟用及安全用戶權(quán)限設(shè)置,強(qiáng)調(diào)多協(xié)議支持、高可用性設(shè)計(jì)和分布式場景適配,助力構(gòu)建穩(wěn)定可靠的消息隊(duì)列系統(tǒng),感興趣的朋友跟隨小編一起看看吧2025-09-09
SpringBoot實(shí)現(xiàn)全局異常處理方法總結(jié)
在項(xiàng)目開發(fā)中出現(xiàn)異常時(shí)很平常不過的事情,我們處理異常也有很多種方式。本文將詳細(xì)為大家講解SpringBoot實(shí)現(xiàn)全局異常處理幾種方法,感興趣的可以學(xué)習(xí)一下2022-03-03
springcloud gateway設(shè)置context-path的操作
這篇文章主要介紹了springcloud gateway設(shè)置context-path的操作,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
java常用的定時(shí)任務(wù)框架及相關(guān)代碼應(yīng)用示例
在Java開發(fā)中,定時(shí)任務(wù)是一個(gè)常見的需求,比如定時(shí)備份數(shù)據(jù)、定時(shí)清理日志、定時(shí)發(fā)送郵件等,這篇文章主要介紹了java常用的定時(shí)任務(wù)框架及相關(guān)代碼應(yīng)用的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-11-11

