Redis實現(xiàn)短信登錄的示例代碼
一、基于Session實現(xiàn)登錄

---------------------------------------------------Controller
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
//發(fā)送短信驗證碼并保存驗證碼
return userService.sendCode(phone,session);
}
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
//實現(xiàn)登錄功能 loginForm 登錄參數(shù),包含手機(jī)號、驗證碼;或者手機(jī)號、密碼
return userService.login(loginForm,session);
}
@GetMapping("/me")
public Result me(){
// 獲取當(dāng)前登錄的用戶并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
---------------------------------------------------Service
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Result sendCode(String phone, HttpSession session) {
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手機(jī)號格式錯誤");
}
String code = RandomUtil.randomNumbers(6);
session.setAttribute("code",code);
log.debug("短信驗證碼" + code);
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
if(RegexUtils.isPhoneInvalid(loginForm.getPhone())){
return Result.fail("手機(jī)號格式錯誤");
}
Object catchCode = session.getAttribute("code");
String code = loginForm.getCode();
if(catchCode == null || !catchCode.equals(code)){
Result.fail("驗證碼錯誤");
}
User user = query().eq("phone", loginForm.getPhone()).one();
if (user == null) {
user = createUserWithPhone(loginForm.getPhone());
}
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
return Result.ok();
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(3));
save(user);
return user;
}
}
---------------------------------------------------自定義攔截器
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
if (user == null) {
response.setStatus(401);
return false;
}
UserHolder.saveUser((UserDTO) user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
---------------------------------------------------添加攔截器并指定攔截的請求和不攔截的請求
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//實現(xiàn)WebMvcConfigurer接口中的addInterceptors方法把自定義的攔截器類添加進(jìn)來即可
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/voucher/**"
);
}
}總結(jié): 短信驗證碼用隨機(jī)工具生成6位數(shù),保存了session中,在用戶使用手機(jī)號登錄時,獲取session中的驗證碼和請求參數(shù)中的驗證碼比對,一致則去庫里查該手機(jī)號的用戶是否存在,不存在則新建用戶,并把該用戶對象存在在session中。校驗登錄狀態(tài)是使用HandlerInterceptor攔截器實現(xiàn)的,在此之前需要配置攔截哪些請求,不攔截哪些請求,從客戶端的請求中獲取session信息,為空返回401狀態(tài),不為空則把用戶信息存儲在ThreadLocal中,在請求處理完之后銷毀用戶信息。
問題: 集群的session共享問題。多臺Tomcat并不共享session存儲空間,當(dāng)請求切換到不同Tomcat服務(wù)時導(dǎo)致數(shù)據(jù)丟失的問題。早期的Tomcat提供session拷貝功能,但是并不能解決問題,問題有1.多臺Tomcat保存相同的數(shù)據(jù)信息,內(nèi)存空間浪費(fèi);2.拷貝需要時間,在延遲之內(nèi)有用戶訪問,多臺Tomcat依然存在數(shù)據(jù)不一致。
二、基于Redis實現(xiàn)共享Session實現(xiàn)登錄


-------------------只記錄有變化的--------------------------------Service
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機(jī)號格式錯誤");
}
String code = RandomUtil.randomNumbers(6);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
log.debug("短信驗證碼" + code);
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機(jī)號格式錯誤");
}
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
return Result.fail("驗證碼錯誤");
}
User user = query().eq("phone", phone).one();
if (user == null) {
user = createUserWithPhone(phone);
}
String token = UUID.randomUUID().toString(true);
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()));
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, userMap);
stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok(token);
}
}
---------------------------------------------------添加攔截器并指定攔截的請求和不攔截的請求
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/voucher/**"
).order(1);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
---------------------------------------------------自定義攔截器
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判斷ThreadLocal中是否有用戶
if(UserHolder.getUser() == null){
response.setStatus(401);
return false;
}
return true;
}
}
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
if (userMap.isEmpty()) {
return true;
}
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//保存在ThreadLocal中
UserHolder.saveUser(userDTO);
//刷新token有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
}總結(jié): 短信驗證碼修改為以手機(jī)號為key驗證碼為value保存在redis中,在用戶使用手機(jī)號登錄時,獲取redis中的驗證碼和請求參數(shù)中的驗證碼比對,一致則去庫里查該手機(jī)號的用戶是否存在,不存在則新建用戶,并把該用戶對象存在在redis中。校驗登錄狀態(tài)是使用HandlerInterceptor攔截器實現(xiàn)的,在此之前需要配置攔截哪些請求,不攔截哪些請求,從客戶端的請求頭中獲取token信息,并從redis中獲取用戶信息, 為空返回401狀態(tài),不為空則把用戶信息存儲在ThreadLocal中,還把驗證碼和用戶信息設(shè)置有效時間,時間一過,則退出用戶登錄。在請求處理完之后銷毀用戶信息。
改造的點:
- 發(fā)送短信驗證碼時,key使用手機(jī)號來確保唯一存儲在redis中,在用戶登錄時可以根據(jù)key來取出驗證碼校對。
- 短信驗證碼登錄時,key使用UUID來確保唯一存儲在redis中,為確保將來前端能把token發(fā)送過來進(jìn)行校驗,在請求結(jié)束前把token返回給客戶端。
問題:
1.攔截器能否真正的實現(xiàn)只要用戶一直在訪問,token就不會過期?
不行,因為攔截器只攔截需要登錄的路徑,如果用戶在有效期內(nèi)一直訪問不需要登錄的路徑,那么redis中的token就會過期。問題解決如下圖。

到此這篇關(guān)于Redis實現(xiàn)短信登錄的示例代碼的文章就介紹到這了,更多相關(guān)Redis 短信登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis+自定義注解+AOP實現(xiàn)聲明式注解緩存查詢的示例
實際項目中,會遇到很多查詢數(shù)據(jù)的場景,這些數(shù)據(jù)更新頻率也不是很高,一般我們在業(yè)務(wù)處理時,會對這些數(shù)據(jù)進(jìn)行緩存,本文主要介紹了Redis+自定義注解+AOP實現(xiàn)聲明式注解緩存查詢的示例,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-04-04
如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性
這篇文章主要介紹了如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性,文中舉了兩個場景例子介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
詳解使用Redis SETNX 命令實現(xiàn)分布式鎖
本篇文章主要介紹了詳解使用Redis SETNX 命令實現(xiàn)分布式鎖,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01
大白話講解調(diào)用Redis的increment失敗原因及推薦使用詳解
本文主要介紹了調(diào)用Redis的increment失敗原因及推薦使用,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11
Redis超詳細(xì)講解高可用主從復(fù)制基礎(chǔ)與哨兵模式方案
Redis因為其高性能和易用性在我們后端的服務(wù)中發(fā)揮了巨大的作用,并且很多重要功能的實現(xiàn)都會依賴redis,本篇我們來了解Redis高可用主從復(fù)制與哨兵模式2022-04-04

