Redis實現(xiàn)短信登錄的企業(yè)實戰(zhàn)
一、導入黑馬點評項目
黑馬點評項目主要包括以下功能:

這一章主要介紹短信登錄功能,短信登錄功能是基于Redis的共享session實現(xiàn)的
1. 導入SQL
需要項目資料的私信我
其中的表有:
- tb_user:用戶表
- tb_user_info:用戶詳情表
- tb_shop:商戶信息表
- tb_shop_type:商戶類型表
- tb_blog:用戶日記表(達人探店日記)
- tb_follow:用戶關注表
- tb_voucher:優(yōu)惠券表
- tb_voucher_order:優(yōu)惠券的訂單表
注意:Mysql的版本采用5.7及以上版本

2. 前后端分離

3. 導入后端項目
3.1 將后端項目導入到 Idea 中

3.2 注意:修改application.yaml文件中的mysql、redis地址信息 將mysql、redis地址信息修改為自己的信息

3.3 啟動項目 啟動項目后,在瀏覽器訪問:http://localhost:8081/shop-type/list ,如果可以看到數(shù)據(jù)則證明運行沒有問題

4. 導入前端項目
4.1 導入nginx文件夾 將nginx文件夾復制到任意目錄,要確保該目錄不包含中文、特殊字符和空格,例如:

4.2 運行前端項目 在nginx所在目錄下打開一個CMD窗口,輸入命令啟動nginx:
start nginx.exe

打開chrome瀏覽器,在空白頁面點擊鼠標右鍵,選擇檢查,即可打開開發(fā)者工具:

然后訪問: http://127.0.0.1:8080 ,即可看到頁面:

二、基于Session實現(xiàn)登錄流程

- 后端將生成的驗證碼和用戶信息保存到session中,并將sessionId返回給前端保存到cookie中
- 用戶登錄時,會攜帶cookie向后端發(fā)起請求,后端進行校驗時,從cookie中獲取sessionId,通過sessionId可以從session中獲取用戶信息并保存到ThreadLocal中
- 后續(xù)每個線程都有一份ThreadLocal中的用戶副本信息,不同線程拿到用戶信息后可以實現(xiàn)不同的操作,從而起到線程隔離作用
1. 發(fā)送短信驗證碼

主要代碼:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
/**
* 發(fā)送手機驗證碼
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 發(fā)送短信驗證碼并保存驗證碼
return userService.sendCode(phone, session);
}
}
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.使用工具類校驗手機號
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回錯誤信息
return Result.fail("手機號格式錯誤!");
}
// 3.符合,生成驗證碼
String code = RandomUtil.randomNumbers(6);
// 4.保存驗證碼到 session
session.setAttribute("code",code);
// 5.模擬發(fā)送驗證碼
log.debug("發(fā)送短信驗證碼成功,驗證碼:{}", code);
// 返回ok
return Result.ok();
}
}
2. 短信驗證碼登錄、注冊

主要代碼:
UserController
/**
* 登錄功能
* @param loginForm 登錄參數(shù),包含手機號、驗證碼;或者手機號、密碼
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 實現(xiàn)登錄功能
return userService.login(loginForm, session);
}
UserServiceImpl
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校驗手機號
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果不符合,返回錯誤信息
return Result.fail("手機號格式錯誤!");
}
// 2.校驗驗證碼
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code)) {
// 3.驗證碼不一致,則報錯
return Result.fail("驗證碼錯誤");
}
// 4.驗證碼一致,根據(jù)手機號查詢用戶
User user = query().eq("phone", phone).one();
// 5.判斷用戶是否存在
if (user == null) {
// 6.用戶不存在,則創(chuàng)建用戶并保存
user = createUserWithPhone(phone);
}
// 7.保存用戶信息到session中,UserDTO只包含簡單的用戶信息,
// 而不是完整的User,這樣可以隱藏用戶的敏感信息(例如:密碼等),還能減少內(nèi)存使用
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
// 8.返回ok
return Result.ok();
}
private User createUserWithPhone(String phone) {
// 1.創(chuàng)建用戶
User user = new User();
user.setPhone(phone);
// 隨機設置昵稱 user_mrkuw05lok
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 2.保存用戶
save(user);
return user;
}
3. 登錄驗證功能
用戶請求登錄時,會攜帶cookie,cookie中包含JSEESIONID

為了避免用戶請求每個controller時,每次都去校驗用戶信息,所以可以加攔截器
攔截器只需在用戶請求訪問時,校驗一次后將用戶信息保存到ThreadLocal中,供后續(xù)線程使用

主要代碼:
在工具類中編寫ThreadLocal
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
在工具類中編寫登錄攔截器
public class LoginInterceptor implements HandlerInterceptor {
/**
* 前置攔截
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.獲取session
HttpSession session = request.getSession();
// 2.獲取session中的用戶
Object user = session.getAttribute("user");
// 3.判斷用戶是否存在
if(user == null){
// 4.不存在,攔截,返回401狀態(tài)碼
response.setStatus(401);
return false;
}
// 5.存在,保存用戶信息到ThreadLocal
UserHolder.saveUser((User)user);
// 6.放行
return true;
}
/**
* 后置攔截器
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 請求結(jié)束后移除用戶,防止ThreadLocal造成內(nèi)存泄漏
UserHolder.removeUser();
}
}
在配置類中添加攔截器配置類
@Configuration
public class MvcConfig implements WebMvcConfigurer {
/**
* 添加攔截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登錄攔截器
registry.addInterceptor(new LoginInterceptor())
// 排除不需要攔截的路徑
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
UserController
@GetMapping("/me")
public Result me(){
// 獲取當前登錄的用戶并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
三、集群的session共享問題

四、基于Redis實現(xiàn)共享session的登錄功能
1. 選擇合適的數(shù)據(jù)結(jié)構(gòu)存入Redis
- 手機號作為key,String類型的驗證碼作為value
- 用戶登錄時正好會提交手機號,方便通過Redis進行校驗驗證碼

token作為key,Hash類型的用戶信息作為value
后端校驗成功后,會返回token給前端,前端會將token保存到sessionStorage中(這是瀏覽器的存儲方式),以后前端每次請求都會攜帶token,方便后端通過Redis校驗用戶信息

前端代碼:將后端返回的token保存到sessionStorage中

前端每次請求時,都會通過攔截器將token設置到請求頭中,賦值給變量authorization,后端通過authorization獲取前端攜帶的token進行校驗

2. 發(fā)送短信驗證碼
修改之前代碼,將驗證碼存入Redis
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.使用工具類校驗手機號
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回錯誤信息
return Result.fail("手機號格式錯誤!");
}
// 3.符合,生成驗證碼
String code = RandomUtil.randomNumbers(6);
// 4.保存驗證碼到 session
// session.setAttribute("code",code);
// 4.保存驗證碼到 redis
// "login:code:"是業(yè)務前綴,以"login:code:" + 手機號為key,過期時間2分鐘
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5.模擬發(fā)送驗證碼
log.debug("發(fā)送短信驗證碼成功,驗證碼:{}", code);
// 返回ok
return Result.ok();
}
}
3. 短信驗證碼登錄、注冊
- 修改之前代碼,從Redis獲取驗證碼并校驗
- 隨機生成token,保存用戶信息到redis中,返回token
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校驗手機號
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果不符合,返回錯誤信息
return Result.fail("手機號格式錯誤!");
}
// // 2.校驗驗證碼
// Object cacheCode = session.getAttribute("code");
// String code = loginForm.getCode();
// if (cacheCode == null || !cacheCode.toString().equals(code)) {
// // 3.驗證碼不一致,則報錯
// return Result.fail("驗證碼錯誤");
// }
// 2.從Redis獲取驗證碼并校驗
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 3.驗證碼不一致,則報錯
return Result.fail("驗證碼錯誤");
}
// 4.驗證碼一致,根據(jù)手機號查詢用戶
User user = query().eq("phone", phone).one();
// 5.判斷用戶是否存在
if (user == null) {
// 6.用戶不存在,則創(chuàng)建用戶并保存
user = createUserWithPhone(phone);
}
// // 7.保存用戶信息到session中,UserDTO只包含簡單的用戶信息,而不是完整的User,這樣可以隱藏用戶的敏感信息(例如:密碼等),還能減少內(nèi)存使用
// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
// 7.保存用戶信息到redis中
// 7.1隨機生成token,作為登錄令牌
// 使用hutool工具中的UUID,true表示不帶“-”符號的UUID
String token = UUID.randomUUID().toString(true);
// 7.2將User對象轉(zhuǎn)為Hash類型進行存儲
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 由于使用的是stringRedisTemplate,所以存入的value中的值必須都是String類型的
// 但是UserDTO中的id是Long類型的,所以進行對象屬性拷貝時,需要自定義實現(xiàn)轉(zhuǎn)換規(guī)則
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3存入redis, "login:token:"是業(yè)務前綴,以 "login:token:" + token作為key
stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
// 7.4設置token有效期,有效期為30分鐘
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}
private User createUserWithPhone(String phone) {
// 1.創(chuàng)建用戶
User user = new User();
user.setPhone(phone);
// 隨機設置昵稱 user_mrkuw05lok
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 2.保存用戶
save(user);
return user;
}
4. 解決token刷新問題
- token刷新問題是指,用戶長時間不進行界面操作時,到了過期時間,token自動失效;但是,用戶一旦進行操作,就需要給token續(xù)期,即更新token過期時間
- 為了解決token刷新問題,需要加2個攔截器
- 第一個攔截器可以攔截所有請求,只要用戶有請求就刷新token,并保存用戶信息到ThreadLocal中
- 第二個攔截器只對登錄請求進行攔截,從ThreadLocal中獲取用戶信息進行校驗

刷新token的攔截器代碼:
public class RefreshTokenInterceptor implements HandlerInterceptor {
// 因為LoginInterceptor不是通過Spring進行管理的Bean,所以不能再LoginInterceptor中進行注入StringRedisTemplate
// 可以通過構(gòu)造方法傳入StringRedisTemplate
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 前置攔截
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// // 1.獲取session
// HttpSession session = request.getSession();
// // 2.獲取session中的用戶
// Object user = session.getAttribute("user");
// // 3.判斷用戶是否存在
// if(user == null){
// // 4.不存在,攔截,返回401狀態(tài)碼
// response.setStatus(401);
// return false;
// }
// // 5.存在,保存用戶信息到ThreadLocal
// UserHolder.saveUser((UserDTO)user);
// // 6.放行
// return true;
// 1.獲取請求頭中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// 不存在,則攔截,返回401狀態(tài)碼
response.setStatus(401);
return false;
}
// 2.通過token獲取redis中的用戶
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(RedisConstants.LOGIN_USER_KEY + token);
// 3.判斷用戶是否存在
if (userMap.isEmpty()) {
// 4.用戶不存在,則攔截,返回401狀態(tài)碼
response.setStatus(401);
return false;
}
// 5.將redis中Hash類型數(shù)據(jù)轉(zhuǎn)換成UserDTO對象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.用戶存在,保存用戶信息到ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
/**
* 后置攔截器
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 請求結(jié)束后移除用戶,防止ThreadLocal造成內(nèi)存泄漏
UserHolder.removeUser();
}
}
登錄攔截器的代碼:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.判斷是否需要攔截(ThreadLocal中是否有用戶)
if (UserHolder.getUser() == null) {
// 沒有,需要攔截,設置狀態(tài)碼
response.setStatus(401);
// 攔截
return false;
}
// 有用戶,則放行
return true;
}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 添加攔截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登錄攔截器
registry.addInterceptor(new LoginInterceptor())
// 排除不需要攔截的路徑
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// token刷新的攔截器,order越小,執(zhí)行優(yōu)先級越高,所以token刷新的攔截器先執(zhí)行
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**")
.excludePathPatterns(
// RefreshTokenInterceptor攔截器也需要放行"/user/code","/user/login",不然token過期后再重新登錄就會一直被攔截
"/user/code",
"/user/login")
.order(0);
}
}
到此這篇關于Redis實現(xiàn)短信登錄的企業(yè)實戰(zhàn)的文章就介紹到這了,更多相關Redis 短信登錄 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
redis中Could not get a resource from
這篇文章主要介紹了redis中Could not get a resource from the pool異常及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12
Redis Desktop Manager(Redis可視化工具)安裝及使用圖文教程
這篇文章主要介紹了Redis Desktop Manager(Redis可視化工具)安裝及使用圖文教程,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04
Redis遍歷海量數(shù)據(jù)集的幾種實現(xiàn)方法
Redis作為一個高性能的鍵值存儲數(shù)據(jù)庫,廣泛應用于各種場景,包括緩存、消息隊列、排行榜,本文主要介紹了Redis遍歷海量數(shù)據(jù)集的幾種實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,需要的朋友們下面隨著小編來一起學習學習吧2024-02-02

