Redis如何解決使用Session登錄帶來(lái)的共享問(wèn)題
在學(xué)習(xí)項(xiàng)目的過(guò)程中遇到了使用Session實(shí)現(xiàn)登錄功能所帶來(lái)的共享問(wèn)題,此問(wèn)題可以使用Redis來(lái)解決,也即是加上一層來(lái)解決問(wèn)題。
接下來(lái)介紹一些Session的相關(guān)內(nèi)容并且采用Session實(shí)現(xiàn)登錄功能(并附上代碼),進(jìn)行分析其存在的問(wèn)題,并使用Redis來(lái)解決這一問(wèn)題。
一、什么是Session
簡(jiǎn)而言之,session是服務(wù)器為了保存用戶狀態(tài)而創(chuàng)建的一個(gè)特殊的對(duì)象,服務(wù)器會(huì)為每一個(gè)瀏覽器(客戶端)創(chuàng)建一個(gè)唯一的session,session有一個(gè)JSESSIONID,這個(gè)是session的唯一標(biāo)識(shí)。
當(dāng)瀏覽器(客戶端)發(fā)送請(qǐng)求的時(shí)候,會(huì)在Cookie中攜帶JSESSIONID,以便服務(wù)器可以找到對(duì)應(yīng)的Session,服務(wù)器就能知道客戶端的狀態(tài)了。
比如:在登陸一些網(wǎng)站成功之后,你再訪問(wèn)這個(gè)網(wǎng)站的其他內(nèi)容的時(shí)候就不需要重新登陸了,因?yàn)榇藭r(shí)服務(wù)器可以基于瀏覽器Cookie中的JSESSIONID(或者其他校驗(yàn)技術(shù))來(lái)判斷你的狀態(tài)。但是當(dāng)一段時(shí)間未登錄之后,你重新訪問(wèn)網(wǎng)站,往往需要重新登錄,這是因?yàn)闉g覽器訪問(wèn)時(shí)沒(méi)有攜帶JSESSIONID,瀏覽器攜帶的JSESSIONID對(duì)應(yīng)的session不存在(或者失效)的原因。
二、基于Session實(shí)現(xiàn)登錄(短信登錄)
1.流程



2.代碼實(shí)現(xiàn)
(1)Controller層:(接收請(qǐng)求)
/**
* 發(fā)送手機(jī)驗(yàn)證碼
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
//發(fā)送短信驗(yàn)證碼并保存驗(yàn)證碼
return userService.sendCode(phone, session);
}
/**
* 登錄功能
* @param loginForm 登錄參數(shù),包含手機(jī)號(hào)、驗(yàn)證碼;或者手機(jī)號(hào)、密碼
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 實(shí)現(xiàn)登錄功能
return userService.login(loginForm, session);
}(2)Service層:(處理業(yè)務(wù)邏輯)
/**
* 發(fā)送短信驗(yàn)證碼并保存驗(yàn)證碼
* @param phone
* @param session
* @return
*/
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校驗(yàn)手機(jī)號(hào)是否合法
boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
//2.若不合法,返回錯(cuò)誤信息
if(!phoneInvalid){
return Result.fail("手機(jī)號(hào)不合法");
}
//3.生成驗(yàn)證碼 6位
String code = RandomUtil.randomNumbers(6);
//4.保存驗(yàn)證碼到session中
session.setAttribute("code",code);
//5.發(fā)送驗(yàn)證碼 (這里可以結(jié)合阿里云提供的第三方服務(wù)來(lái)做,但是為了方便,輸出一下就可以)
log.debug("發(fā)送短信驗(yàn)證碼成功,驗(yàn)證碼:{}",code);
//6.返回ok
return Result.ok();
} /**
* 登錄功能
* @param loginForm
* @param session
* @return
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
//1.校驗(yàn)手機(jī)號(hào)是否合法
if(!RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手機(jī)號(hào)不合法");
}
//2.校驗(yàn)驗(yàn)證碼是否一致
String sessionCode = (String) session.getAttribute("code");
String userCode = loginForm.getCode();
//3.驗(yàn)證碼不一致返回錯(cuò)誤信息
if(sessionCode == null ||!sessionCode.equals(userCode)){
return Result.fail("驗(yàn)證碼錯(cuò)誤");
}
//4.根據(jù)手機(jī)號(hào)到數(shù)據(jù)庫(kù)查詢用戶
User user = query().eq("phone", phone).one();
//5.若用戶不存在則創(chuàng)建用戶并保存到數(shù)據(jù)庫(kù)
if(user == null){
user = createUserByPhone(phone);
}
//6.保存用戶信息到session中
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
session.setAttribute("user",userDTO);
//7.返回ok
return Result.ok();
}(3)LoginInterceptor:(攔截請(qǐng)求,進(jìn)行登錄校驗(yàn))
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.獲取session
HttpSession session = request.getSession();
//2.獲取session里面的user
User user = (User) session.getAttribute("user");
//3.用戶不存在,則攔截
if(user == null){
//4.攔截,返回401
response.setStatus(401);
return false;
}
//5.存在,保存到ThreadLocal中
UserDTO userDTO = new UserDTO();
userDTO.setId(user.getId());
userDTO.setIcon(user.getIcon());
userDTO.setNickName(user.getNickName());
UserHolder.saveUser(userDTO);
//6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//在頁(yè)面渲染完成返回給用戶前,刪除用戶信息,避免內(nèi)存泄漏
UserHolder.removeUser();
}
}(4)MVCconfig:(用于注冊(cè)攔截器,使得攔截器生效)
@Configuration//由Spring創(chuàng)建的對(duì)象 可以使用 Autowired
public class MVCconfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/user/code",
"/user/login",
"/shop/**",
"/shop-type/**",
"/blog/hot",
"/upload/**",
"/voucher/**"
);
}
}三、基于Session實(shí)現(xiàn)登錄存在的共享問(wèn)題
1.問(wèn)題分析

session共享問(wèn)題:多臺(tái)Tomcat并不共享session存儲(chǔ)空間,當(dāng)請(qǐng)求切換到不同tomcat服務(wù)時(shí)導(dǎo)致數(shù)據(jù)丟失的問(wèn)題。
也就是說(shuō)一臺(tái)瀏覽器可能會(huì)被負(fù)載均衡到不同的服務(wù)器上,但是服務(wù)器的Session是不共享的,那當(dāng)瀏覽器先被負(fù)載均衡到服務(wù)器A,后負(fù)載均衡到服務(wù)器B時(shí),瀏覽器所攜帶的JSESSIONID只在服務(wù)器A生效,而服務(wù)器B找不到其對(duì)應(yīng)的Session,也就無(wú)法獲知用戶的狀態(tài)。這就是基于Session實(shí)現(xiàn)登錄存在的共享問(wèn)題。
2.解決方案:
session的替代方案應(yīng)該滿足以下條件:
(1)數(shù)據(jù)共享:
首先必須滿足數(shù)據(jù)可共享,否則還是會(huì)引發(fā)共享問(wèn)題。這個(gè)時(shí)候就應(yīng)該想到常用的思路“加一層”,加上一層中間件,上層都到這個(gè)中間件來(lái)存取數(shù)據(jù),這樣不就實(shí)現(xiàn)了數(shù)據(jù)共享嗎。
(2)內(nèi)存存儲(chǔ)
由于每次請(qǐng)求的時(shí)候,服務(wù)器都需要對(duì)請(qǐng)求進(jìn)行校驗(yàn),這個(gè)操作是十分頻繁的,如果可以將數(shù)據(jù)存儲(chǔ)在內(nèi)存中,就可以極大地提高服務(wù)器處理請(qǐng)求的效率。
(3)key、value結(jié)構(gòu)
服務(wù)器應(yīng)該根據(jù)服務(wù)器所攜帶key去找到其對(duì)應(yīng)的value
通過(guò)以上分析,使用Redis來(lái)解決這個(gè)問(wèn)題是再合適不過(guò)的了。
使用Redis實(shí)現(xiàn)登錄功能總思路:
1.用戶提交手機(jī)號(hào),后端收到手機(jī)號(hào)之后生成驗(yàn)證碼并以手機(jī)號(hào)為key,驗(yàn)證碼為value,并設(shè)置過(guò)期時(shí)間,存儲(chǔ)到redis中。存儲(chǔ)完成之后發(fā)送驗(yàn)證碼給用戶。
2.用戶填寫驗(yàn)證碼,瀏覽器發(fā)送協(xié)帶用戶填寫的手機(jī)號(hào)和驗(yàn)證碼的請(qǐng)求。后端接收請(qǐng)求,根據(jù)用戶的手機(jī)號(hào)到redis中查找對(duì)應(yīng)的驗(yàn)證碼值,查找后將redis中的驗(yàn)證碼和用戶提交的驗(yàn)證碼進(jìn)行比較。
3.驗(yàn)證成功,則生成一個(gè)token,將token作為key,用戶信息作為value(這里使用redis的hash結(jié)構(gòu)存儲(chǔ)),設(shè)置過(guò)期時(shí)間,存入redis中,并返回一個(gè)token。
4.此后,前端發(fā)送請(qǐng)求的時(shí)候,就攜帶這個(gè)token,服務(wù)器就可以根據(jù)這個(gè)token到redis中去獲取用戶的信息。
5.由于redis是單獨(dú)的一層,所有瀏覽器拿到token之后都是去這個(gè)redis中查找數(shù)據(jù),這就解決了用戶被負(fù)載均衡到不同服務(wù)器時(shí),session引發(fā)的共享問(wèn)題。
四、基于Redis實(shí)現(xiàn)登錄
1.流程


簡(jiǎn)單來(lái)說(shuō)就是把對(duì)session存取操作改為對(duì)redis的存取操作。
2.代碼實(shí)現(xiàn)
(1)Controller層
/**
* 發(fā)送手機(jī)驗(yàn)證碼
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
//發(fā)送短信驗(yàn)證碼并保存驗(yàn)證碼
return userService.sendCode(phone, session);
}
/**
* 登錄功能
* @param loginForm 登錄參數(shù),包含手機(jī)號(hào)、驗(yàn)證碼;或者手機(jī)號(hào)、密碼
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 實(shí)現(xiàn)登錄功能
return userService.login(loginForm, session);
}
@GetMapping("/me")
public Result me(){
//獲取當(dāng)前登錄的用戶并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
(2)Service層:(處理業(yè)務(wù)邏輯)
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 發(fā)送短信驗(yàn)證碼并保存驗(yàn)證碼
* @param phone
* @param session
* @return
*/
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校驗(yàn)手機(jī)號(hào)是否合法
boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
//2.若不合法,返回錯(cuò)誤信息
if(!phoneInvalid){
return Result.fail("手機(jī)號(hào)不合法");
}
//3.生成驗(yàn)證碼 6位
String code = RandomUtil.randomNumbers(6);
//4.保存驗(yàn)證碼到redis中
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY+phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5.發(fā)送驗(yàn)證碼 (這里可以結(jié)合阿里云提供的第三方服務(wù)來(lái)做,但是為了方便,輸出一下就可以)
log.debug("發(fā)送短信驗(yàn)證碼成功,驗(yàn)證碼:{}",code);
//6.返回ok
return Result.ok();
}
/**
* 登錄功能
* @param loginForm
* @param session
* @return
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
//1.校驗(yàn)手機(jī)號(hào)是否合法
if(!RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手機(jī)號(hào)不合法");
}
//2.TODO 從Redis獲取驗(yàn)證碼并校驗(yàn)驗(yàn)證碼是否一致
String reidsCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY+phone);
String userCode = loginForm.getCode();
//3.驗(yàn)證碼不一致返回錯(cuò)誤信息
if(reidsCode == null ||!reidsCode.equals(userCode)){
return Result.fail("驗(yàn)證碼錯(cuò)誤");
}
//4.根據(jù)手機(jī)號(hào)到數(shù)據(jù)庫(kù)查詢用戶
User user = query().eq("phone", phone).one();
//5.若用戶不存在則創(chuàng)建用戶并保存到數(shù)據(jù)庫(kù)
if(user == null){
user = createUserByPhone(phone);
}
//6.保存用戶信息到redis中
//6.1 生成隨機(jī)token 作為登錄令牌
String token = UUID.randomUUID().toString(true);
//6.2 將UserDTO轉(zhuǎn)為 HashMap
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
Map<String, Object>usertMap = new HashMap<>();
usertMap.put("id", userDTO.getId().toString());
usertMap.put("nickName", userDTO.getNickName());
usertMap.put("icon", userDTO.getIcon());
//6.3 存儲(chǔ)到redis中
String tokenKey = RedisConstants.LOGIN_USER_KEY+token;
stringRedisTemplate.opsForHash().putAll(tokenKey,usertMap);
//6.4 設(shè)置有效期
stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);
//7.返回token
return Result.ok(token);
}
/**
* 根據(jù)手機(jī)號(hào)創(chuàng)建用戶
* @param phone
* @return
*/
private User createUserByPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(6));
save(user);
return user;
}(3)LoginInterceptor:(攔截請(qǐng)求,進(jìn)行登錄校驗(yàn))
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.獲取請(qǐng)求頭中的token
String token = request.getHeader("authorization");
if(StringUtils.isEmpty(token)){
//token 為空 攔截,返回401
response.setStatus(401);
return false;
}
//2.通過(guò)token獲取redis中的用戶信息
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(tokenKey);
//3.用戶不存在,則攔截
if(userMap.isEmpty()){
//4.攔截,返回401
response.setStatus(401);
return false;
}
//5. 將HashMap轉(zhuǎn)為UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.存在,保存到ThreadLocal中
UserHolder.saveUser(userDTO);
//7.重置token有效期
stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES)
//8.放行
return true;
}(4)MVCconfig:(用于注冊(cè)攔截器,使得攔截器生效)
@Configuration//由Spring創(chuàng)建的對(duì)象 可以使用 Autowired
public class MVCconfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/user/code",
"/user/login",
"/shop/**",
"/shop-type/**",
"/blog/hot",
"/upload/**",
"/voucher/**"
);
}
}總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- 使用Redis快速實(shí)現(xiàn)共享Session登錄的詳細(xì)步驟
- 基于Redis實(shí)現(xiàn)共享Session登錄的實(shí)現(xiàn)
- 微服務(wù)Redis-Session共享登錄狀態(tài)的過(guò)程詳解
- Redis的共享session應(yīng)用實(shí)現(xiàn)短信登錄
- Redis實(shí)現(xiàn)Session共享與單點(diǎn)登錄
- SpringBoot+SpringSession+Redis實(shí)現(xiàn)session共享及唯一登錄示例
- 基于SpringBoot+Redis的Session共享與單點(diǎn)登錄詳解
相關(guān)文章
Feign遠(yuǎn)程調(diào)用丟失請(qǐng)求頭問(wèn)題
本文介紹了在服務(wù)端項(xiàng)目中如何解決資源訪問(wèn)限制問(wèn)題,首先介紹了問(wèn)題的產(chǎn)生,然后詳細(xì)解析了源碼,最后提出了解決方案,解決方案包括同步和異步兩種,同步時(shí)直接向Spring容器注入RequestInterceptor攔截器2024-09-09
在本地用idea連接虛擬機(jī)上的hbase集群的實(shí)現(xiàn)代碼
這篇文章主要介紹了在本地用idea連接虛擬機(jī)上的hbase集群的實(shí)現(xiàn)代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10
Java執(zhí)行Python代碼的五種場(chǎng)景與示例方法
python可以搭建后端讓Java調(diào)用接口,但某些時(shí)候我們用到的python代碼可能并不多也許只有一個(gè)算法,此時(shí)就需要其他方法了,下面小編就來(lái)和大家詳細(xì)介紹一下吧2025-07-07
SpringBoot集成kafka全面實(shí)戰(zhàn)記錄
在實(shí)際開(kāi)發(fā)中,我們可能有這樣的需求,應(yīng)用A從TopicA獲取到消息,經(jīng)過(guò)處理后轉(zhuǎn)發(fā)到TopicB,再由應(yīng)用B監(jiān)聽(tīng)處理消息,即一個(gè)應(yīng)用處理完成后將該消息轉(zhuǎn)發(fā)至其他應(yīng)用,完成消息的轉(zhuǎn)發(fā),這篇文章主要介紹了SpringBoot集成kafka全面實(shí)戰(zhàn),需要的朋友可以參考下2021-11-11
靈活控制任務(wù)執(zhí)行時(shí)間的Cron表達(dá)式范例
這篇文章主要為大家介紹了靈活控制任務(wù)執(zhí)行時(shí)間的Cron表達(dá)式范例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
maven多profile 打包下 -P參和-D參數(shù)的實(shí)現(xiàn)
這篇文章主要介紹了maven多profile 打包下 -P參和-D參數(shù)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
詳解IDEA中SpringBoot整合Servlet三大組件的過(guò)程
這篇文章主要介紹了詳解IDEA中SpringBoot整合Servlet三大組件的過(guò)程,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
Android 資源 id詳解及的動(dòng)態(tài)獲取
這篇文章主要介紹了Android 資源 id詳解及的動(dòng)態(tài)獲取的相關(guān)資料,需要的朋友可以參考下2016-12-12

