基于Redis實(shí)現(xiàn)共享Session登錄的實(shí)現(xiàn)
背景
session 共享問題:如果后端服務(wù)是集群模式,由于多臺機(jī)器之間并不共享 session 存儲空間,當(dāng)請求切換到不同服務(wù)時會導(dǎo)致數(shù)據(jù)丟失的問題
session 的替代方案應(yīng)該滿足:
1.數(shù)據(jù)共享
2.內(nèi)存存儲
3.key、value 結(jié)構(gòu)
Redis 能夠滿足以上的要求,因此可以采用 Redis 來實(shí)現(xiàn)共享登錄
實(shí)現(xiàn)流程
這里以短信登錄的業(yè)務(wù)作為示例,主要包括三個功能:
1.發(fā)送短信驗(yàn)證碼的接口
2.短信驗(yàn)證碼登錄、注冊接口
3.校驗(yàn)登錄狀態(tài)攔截器
流程圖如下所示:


這里采用的策略是,發(fā)送驗(yàn)證碼時,將對應(yīng)的手機(jī)號作為 key,驗(yàn)證碼作為 value
登錄、注冊時,需要使用手機(jī)號將驗(yàn)證碼取出,并且以隨機(jī) token 作為 key,用戶信息作為 value 保存用戶數(shù)據(jù),這里的用戶數(shù)據(jù)用 hash 類型保存。最后還需要將這個 token 返回給前端
之后在校驗(yàn)登錄狀態(tài)時,前端的每次請求都需要攜帶這個 token 值,以便服務(wù)端能取出相應(yīng)的用戶信息
這里使用隨機(jī) token 而不使用手機(jī)號作為 key 的目的在于,瀏覽器是需要存儲這個 key 的,以便校驗(yàn)登錄狀態(tài),如果使用手機(jī)號會不安全
代碼實(shí)現(xiàn)
實(shí)體類
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主鍵
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 手機(jī)號碼
*/
private String phone;
/**
* 密碼,加密存儲
*/
private String password;
/**
* 昵稱,默認(rèn)是隨機(jī)字符
*/
private String nickName;
/**
* 用戶頭像
*/
private String icon = "";
/**
* 創(chuàng)建時間
*/
private LocalDateTime createTime;
/**
* 更新時間
*/
private LocalDateTime updateTime;
}
dto 類
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
這里單獨(dú)抽取 dto 的原因在于,我們不希望將密碼等敏感字段返回給前端
@Data
public class LoginFormDTO {
private String phone;
private String code;
private String password;
}
結(jié)果返回類
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Boolean success;
private String errorMsg;
private Object data;
private Long total;
public static Result ok(){
return new Result(true, null, null, null);
}
public static Result ok(Object data){
return new Result(true, null, data, null);
}
public static Result ok(List<?> data, Long total){
return new Result(true, null, data, total);
}
public static Result fail(String errorMsg){
return new Result(false, errorMsg, null, null);
}
}
常量類
public class RedisConstants {
public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 2L;
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 30L;
}
工具類
public class ObjectMapUtils {
// 將對象轉(zhuǎn)為 Map
public static Map<String, String> obj2Map(Object obj) throws IllegalAccessException {
Map<String, String> result = new HashMap<>();
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 如果為 static 且 final 則跳過
if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
continue;
}
field.setAccessible(true); // 設(shè)置為可訪問私有字段
Object fieldValue = field.get(obj);
if (fieldValue != null) {
result.put(field.getName(), field.get(obj).toString());
}
}
return result;
}
// 將 Map 轉(zhuǎn)為對象
public static Object map2Obj(Map<Object, Object> map, Class<?> clazz) throws Exception {
Object obj = clazz.getDeclaredConstructor().newInstance();
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object fieldName = entry.getKey();
Object fieldValue = entry.getValue();
Field field = clazz.getDeclaredField(fieldName.toString());
field.setAccessible(true); // 設(shè)置為可訪問私有字段
String fieldValueStr = fieldValue.toString();
// 根據(jù)字段類型進(jìn)行轉(zhuǎn)換
if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {
field.set(obj, Integer.parseInt(fieldValueStr));
} else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
field.set(obj, Boolean.parseBoolean(fieldValueStr));
} else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {
field.set(obj, Double.parseDouble(fieldValueStr));
} else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {
field.set(obj, Long.parseLong(fieldValueStr));
} else if (field.getType().equals(String.class)) {
field.set(obj, fieldValueStr);
} else if(field.getType().equals(LocalDateTime.class)) {
field.set(obj, LocalDateTime.parse(fieldValueStr));
}
}
return obj;
}
}
控制層
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
/**
* 發(fā)送手機(jī)驗(yàn)證碼
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone) {
return userService.sendCode(phone);
}
/**
* 登錄功能
* @param loginForm 登錄參數(shù),包含手機(jī)號、驗(yàn)證碼;或者手機(jī)號、密碼
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm){
return userService.login(loginForm);
}
}
服務(wù)層
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Result sendCode(String phone/*, HttpSession session*/) {
// 校驗(yàn)手機(jī)號
if(RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機(jī)號格式錯誤");
}
// 生成驗(yàn)證碼
String code = RandomUtil.randomNumbers(6);
/*// 保存驗(yàn)證碼到 session
session.setAttribute("code", phone + "-" + code);*/
// 保存驗(yàn)證碼到 redis
redisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code,
RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 發(fā)送驗(yàn)證碼
log.debug("發(fā)送驗(yàn)證碼:" + code + ",手機(jī)號:" + phone);
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm/*, HttpSession session*/) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
/*// 從 session 取出手機(jī)號和驗(yàn)證碼
String[] phoneAndCode = session.getAttribute("code").toString().split("-");
// 校驗(yàn)手機(jī)號和驗(yàn)證碼
if(!phoneAndCode[0].equals(phone) || !phoneAndCode[1].equals(code)) {
return Result.fail("手機(jī)號或驗(yàn)證碼錯誤");
}*/
// 從 redis 中取出驗(yàn)證碼
String realCode = redisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
if(StringUtils.isBlank(realCode) || !realCode.equals(code)) {
return Result.fail("驗(yàn)證碼錯誤");
}
// 根據(jù)手機(jī)號查詢用戶
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone, phone);
User user = this.getOne(queryWrapper);
// 用戶如果不存在,則創(chuàng)建新用戶
if(user == null) {
user = createUserWithPhone(phone);
}
/*// session 保存用戶信息
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));*/
// redis 保存用戶信息
String token = UUID.randomUUID().toString(true);
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
try {
// 將 User 轉(zhuǎn)為 UserDTO 再轉(zhuǎn)為 Map
Map<String, String> userMap = ObjectMapUtils.obj2Map(BeanUtil.copyProperties(user, UserDTO.class));
redisTemplate.opsForHash().putAll(tokenKey, userMap);
redisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
// 將 token 返回
return Result.ok(token);
}
// 根據(jù)手機(jī)號創(chuàng)建新用戶
public User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 保存至數(shù)據(jù)庫
this.save(user);
return user;
}
}
攔截器及其配置類
這里會使用兩個攔截器,一個是攔截一切路徑的刷新攔截器,主要用途就是如果用戶在 token 有效期內(nèi)訪問了系統(tǒng),那么就會刷新超時時間;另一個是攔截部分路徑的登錄校驗(yàn)攔截器,主要就是檢驗(yàn)用戶是否登錄
添加刷新攔截器的原因在于,如果用登錄校驗(yàn)攔截器進(jìn)行刷新工作,由于排除了部分路徑,因此如果用戶一直訪問這些被排除的部分路徑,會導(dǎo)致用戶 token 的有效期不會被刷新。所以需要單獨(dú)添加一個攔截所有路徑的攔截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 刷新攔截器
registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate)).order(10);
// 登錄攔截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns( // 排除的攔截路徑
// 以下根據(jù)業(yè)務(wù)需求來寫
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(20);
}
}
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate redisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 獲取用戶
String token = request.getHeader("authorization");
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
// 用戶不存在,直接放行
if(entries.isEmpty()) {
return true;
}
// Map 轉(zhuǎn)為 UserDTO
UserDTO user = (UserDTO) ObjectMapUtils.map2Obj(entries, UserDTO.class);
// 用戶存在,放入 ThreadLocal
UserHolder.saveUser(user);
// 刷新 token 有效期
redisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 銷毀 ThreadLocal
UserHolder.removeUser();
}
}
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 用戶未登錄,攔截
if(UserHolder.getUser() == null) {
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
return false;
}
return true;
}
}到此這篇關(guān)于基于Redis實(shí)現(xiàn)共享Session登錄的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Redis Session共享登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis優(yōu)化token校驗(yàn)主動失效的實(shí)現(xiàn)方案
在普通的token頒發(fā)和校驗(yàn)中 當(dāng)用戶發(fā)現(xiàn)自己賬號和密碼被暴露了時修改了登錄密碼后舊的token仍然可以通過系統(tǒng)校驗(yàn)直至token到達(dá)失效時間,所以系統(tǒng)需要token主動失效的一種能力,所以本文給大家介紹了Redis優(yōu)化token校驗(yàn)主動失效的實(shí)現(xiàn)方案,需要的朋友可以參考下2024-03-03
淺談Redis?中的過期刪除策略和內(nèi)存淘汰機(jī)制
本文主要介紹了Redis?中的過期刪除策略和內(nèi)存淘汰機(jī)制,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
Redis連接池監(jiān)控(連接池是否已滿)與優(yōu)化方法
本文詳細(xì)講解了如何在Linux系統(tǒng)中監(jiān)控Redis連接池的使用情況,以及如何通過連接池參數(shù)配置、系統(tǒng)資源使用情況、Redis命令監(jiān)控、外部監(jiān)控工具等多種方法進(jìn)行檢測和優(yōu)化,以確保系統(tǒng)在高并發(fā)場景下的性能和穩(wěn)定性,討論了連接池的概念、工作原理、參數(shù)配置,以及優(yōu)化策略等內(nèi)容2024-09-09
Redis數(shù)據(jù)結(jié)構(gòu)之intset整數(shù)集合使用學(xué)習(xí)
這篇文章主要為大家介紹了Redis數(shù)據(jù)結(jié)構(gòu)之整數(shù)集合使用學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性
這篇文章主要介紹了如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性,文中舉了兩個場景例子介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
springboot整合使用云服務(wù)器上的Redis方法
這篇文章主要介紹了springboot整合使用云服務(wù)器上的Redis,整合步驟通過導(dǎo)入依賴,配置yml文件,注入redisTemplate結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),文中給大家分享了可能遇到的坑,感興趣的朋友跟隨小編一起看看吧2022-09-09

