您好,登錄后才能下訂單哦!
這篇“Redis實現短信登錄的企業實例分析”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Redis實現短信登錄的企業實例分析”文章吧。
黑馬點評項目主要包括以下功能:
需要項目資料的私信我
其中的表有:
tb_user:用戶表
tb_user_info:用戶詳情表
tb_shop:商戶信息表
tb_shop_type:商戶類型表
tb_blog:用戶日記表(達人探店日記)
tb_follow:用戶關注表
tb_voucher:優惠券表
tb_voucher_order:優惠券的訂單表
注意:Mysql的版本采用5.7及以上版本
3.1 將后端項目導入到 Idea 中
3.2 注意:修改application.yaml文件中的mysql、redis地址信息 將mysql、redis地址信息修改為自己的信息
3.3 啟動項目 啟動項目后,在瀏覽器訪問:http://localhost:8081/shop-type/list ,如果可以看到數據則證明運行沒有問題
4.1 導入nginx文件夾 將nginx文件夾復制到任意目錄,要確保該目錄不包含中文、特殊字符和空格,例如:
4.2 運行前端項目 在nginx所在目錄下打開一個CMD窗口,輸入命令啟動nginx:
start nginx.exe
打開chrome瀏覽器,在空白頁面點擊鼠標右鍵,選擇檢查,即可打開開發者工具:
然后訪問: http://127.0.0.1:8080 ,即可看到頁面:
后端將生成的驗證碼和用戶信息保存到session中,并將sessionId返回給前端保存到cookie中
用戶登錄時,會攜帶cookie向后端發起請求,后端進行校驗時,從cookie中獲取sessionId,通過sessionId可以從session中獲取用戶信息并保存到ThreadLocal中
后續每個線程都有一份ThreadLocal中的用戶副本信息,不同線程拿到用戶信息后可以實現不同的操作,從而起到線程隔離作用
主要代碼:
@Slf4j @RestController @RequestMapping("/user") public class UserController { @Resource private IUserService userService; /** * 發送手機驗證碼 */ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { // 發送短信驗證碼并保存驗證碼 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.模擬發送驗證碼 log.debug("發送短信驗證碼成功,驗證碼:{}", code); // 返回ok return Result.ok(); } }
主要代碼:
UserController
/** * 登錄功能 * @param loginForm 登錄參數,包含手機號、驗證碼;或者手機號、密碼 */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ // 實現登錄功能 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.驗證碼一致,根據手機號查詢用戶 User user = query().eq("phone", phone).one(); // 5.判斷用戶是否存在 if (user == null) { // 6.用戶不存在,則創建用戶并保存 user = createUserWithPhone(phone); } // 7.保存用戶信息到session中,UserDTO只包含簡單的用戶信息, // 而不是完整的User,這樣可以隱藏用戶的敏感信息(例如:密碼等),還能減少內存使用 session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); // 8.返回ok return Result.ok(); } private User createUserWithPhone(String phone) { // 1.創建用戶 User user = new User(); user.setPhone(phone); // 隨機設置昵稱 user_mrkuw05lok user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2.保存用戶 save(user); return user; }
用戶請求登錄時,會攜帶cookie,cookie中包含JSEESIONID
為了避免用戶請求每個controller時,每次都去校驗用戶信息,所以可以加攔截器
攔截器只需在用戶請求訪問時,校驗一次后將用戶信息保存到ThreadLocal中,供后續線程使用
主要代碼:
在工具類中編寫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狀態碼 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 { // 請求結束后移除用戶,防止ThreadLocal造成內存泄漏 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); }
手機號作為key,String類型的驗證碼作為value
用戶登錄時正好會提交手機號,方便通過Redis進行校驗驗證碼
token作為key,Hash類型的用戶信息作為value
后端校驗成功后,會返回token給前端,前端會將token保存到sessionStorage中(這是瀏覽器的存儲方式),以后前端每次請求都會攜帶token,方便后端通過Redis校驗用戶信息
前端代碼:將后端返回的token保存到sessionStorage中
前端每次請求時,都會通過攔截器將token設置到請求頭中,賦值給變量authorization,后端通過authorization獲取前端攜帶的token進行校驗
修改之前代碼,將驗證碼存入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:"是業務前綴,以"login:code:" + 手機號為key,過期時間2分鐘 stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES); // 5.模擬發送驗證碼 log.debug("發送短信驗證碼成功,驗證碼:{}", code); // 返回ok return Result.ok(); } }
修改之前代碼,從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.驗證碼一致,根據手機號查詢用戶 User user = query().eq("phone", phone).one(); // 5.判斷用戶是否存在 if (user == null) { // 6.用戶不存在,則創建用戶并保存 user = createUserWithPhone(phone); } // // 7.保存用戶信息到session中,UserDTO只包含簡單的用戶信息,而不是完整的User,這樣可以隱藏用戶的敏感信息(例如:密碼等),還能減少內存使用 // 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對象轉為Hash類型進行存儲 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); // 由于使用的是stringRedisTemplate,所以存入的value中的值必須都是String類型的 // 但是UserDTO中的id是Long類型的,所以進行對象屬性拷貝時,需要自定義實現轉換規則 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:"是業務前綴,以 "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.創建用戶 User user = new User(); user.setPhone(phone); // 隨機設置昵稱 user_mrkuw05lok user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2.保存用戶 save(user); return user; }
token刷新問題是指,用戶長時間不進行界面操作時,到了過期時間,token自動失效;但是,用戶一旦進行操作,就需要給token續期,即更新token過期時間
為了解決token刷新問題,需要加2個攔截器
第一個攔截器可以攔截所有請求,只要用戶有請求就刷新token,并保存用戶信息到ThreadLocal中
第二個攔截器只對登錄請求進行攔截,從ThreadLocal中獲取用戶信息進行校驗
刷新token的攔截器代碼:
public class RefreshTokenInterceptor implements HandlerInterceptor { // 因為LoginInterceptor不是通過Spring進行管理的Bean,所以不能再LoginInterceptor中進行注入StringRedisTemplate // 可以通過構造方法傳入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狀態碼 // 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狀態碼 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狀態碼 response.setStatus(401); return false; } // 5.將redis中Hash類型數據轉換成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 { // 請求結束后移除用戶,防止ThreadLocal造成內存泄漏 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) { // 沒有,需要攔截,設置狀態碼 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越小,執行優先級越高,所以token刷新的攔截器先執行 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**") .excludePathPatterns( // RefreshTokenInterceptor攔截器也需要放行"/user/code","/user/login",不然token過期后再重新登錄就會一直被攔截 "/user/code", "/user/login") .order(0); } }
以上就是關于“Redis實現短信登錄的企業實例分析”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。