基于Redis实现登录功能思路详解

张开发
2026/4/17 9:19:29 15 分钟阅读

分享文章

基于Redis实现登录功能思路详解
本文使用的是手机号验证码的登录方式其中验证码是通过在控制台输出并没有真的发送到手机上太麻烦主要目的还是学习使用Redis重点是看思路而不是具体的代码实现UserServiceImpl实现类整体结构1234567891011121314151617Slf4jServicepublicclassUserServiceImplextendsServiceImplUserMapper, UserimplementsIUserService {AutowiredprivateStringRedisTemplate stringRedisTemplate;OverridepublicResult sendCode(String phone, HttpSession session) {//...}OverridepublicResult login(LoginFormDTO loginForm, HttpSession session) {//...}privateUser createUserWithPhone(String phone) {//...}}sendCode方法这个是发送验证码的方法123456789101112131415publicResult sendCode(String phone, HttpSession session) {// 1. 校验手机号if(RegexUtils.isPhoneInvalid(phone)) {// 2. 如果不符合返回错误信息returnResult.fail(手机号格式错误);}// 3. 如果符合生成验证码String code RandomUtil.randomNumbers(6);// 4. 保存验证码到redisstringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);// 5. 发送验证码log.debug(发送短信验证码成功验证码{}, code);// 6. 返回结果returnResult.ok();}注这里的RedisConstants是一个用来存放各种常量的类123456publicclassRedisConstants {publicstaticfinalString LOGIN_CODE_KEY login:code:;publicstaticfinalLong LOGIN_CODE_TTL 2L;publicstaticfinalString LOGIN_USER_KEY login:token:;publicstaticfinalLong LOGIN_USER_TTL 30L;}login方法这里使用了MybatisPlus来操作数据库User user query().eq(phone, phone).one();但是这个不是重点12345678910111213141516171819202122232425262728293031publicResult login(LoginFormDTO loginForm, HttpSession session) {// 1. 校验手机号String phone loginForm.getPhone();if(RegexUtils.isPhoneInvalid(phone)) {returnResult.fail(手机号格式错误);}// 2. 从redis获取验证码并校验String cacheCode stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY phone);String code loginForm.getCode();if(cacheCode null|| !cacheCode.equals(code)) {// 3. 不一致报错returnResult.fail(验证码错误);}// 4. 一致根据手机号查询用户User user query().eq(phone, phone).one();// 5. 判断用户是否存在if(user null) {// 6. 不存在创建新用户并保存user createUserWithPhone(phone);}// 7. 保存用户信息到redisString token UUID.randomUUID().toString(true);UserDTO userDTO BeanUtil.copyProperties(user, UserDTO.class);MapString, Object userMap BeanUtil.beanToMap(userDTO,newHashMap(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue)-fieldValue.toString()));stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY token, userMap);stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);returnResult.ok(token);}createUserWithPhone方法在login方法中调用了该方法这里也使用了MybatisPlus来操作数据库save(user);123456789privateUser createUserWithPhone(String phone) {// 1. 创建用户User user newUser();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX RandomUtil.randomString(10));// 2. 保存用户save(user);returnuser;}拦截器整体框架其实就是实现了HandlerInterceptor的两个方法123456789101112131415Slf4jComponentpublicclassLoginInterceptorimplementsHandlerInterceptor {AutowiredprivateStringRedisTemplate stringRedisTemplate;OverridepublicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException {//...}OverridepublicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throwsException {// 移除用户UserHolder.removeUser();}}UserHolder是ThreadLocal 持有类123456789101112publicclassUserHolder {privatestaticfinalThreadLocalUserDTO tl newThreadLocal();publicstaticvoidsaveUser(UserDTO user){tl.set(user);}publicstaticUserDTO getUser(){returntl.get();}publicstaticvoidremoveUser(){tl.remove();}}preHandle方法1234567891011121314151617181920212223242526publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException {// 1.获取请求头中的tokenString token request.getHeader(authorization);if(StrUtil.isBlank(token)) {// 不存在拦截response.setStatus(401);returnfalse;}// 2.基于token获取redis中的用户String key RedisConstants.LOGIN_USER_KEY token;MapObject, Object userMap stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if(userMap.isEmpty()) {// 4.不存在拦截response.setStatus(401);returnfalse;}// 5.将查询到的Hash数据转换为UserDTO对象UserDTO userDTO BeanUtil.fillBeanWithMap(userMap,newUserDTO(),false);// 6.存在保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行returntrue;}注authorization 是前端定义的用来传递token的key配置类12345678910111213141516171819ConfigurationpublicclassMvcConfigimplementsWebMvcConfigurer {AutowiredprivateLoginInterceptor loginInterceptor;OverridepublicvoidaddInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns(/**).excludePathPatterns(/user/code,/user/login,/blog/hot,/shop/**,/shop-type/**,/upload/**,/voucher/**);}}整体思路123456789101112131415161718192021222324252627282930313233343536373839flowchart TDsubgraph A[发送验证码流程]A1[前端请求 发送验证码] -- A2[校验手机号格式]A2 -- 不合法 -- A3[返回错误 手机号格式错误]A2 -- 合法 -- A4[生成6位验证码]A4 -- A5[保存验证码到Redis]A5 -- A6[返回成功]endsubgraph B[登录流程]B1[前端请求 登录] -- B2[校验手机号格式]B2 -- 不合法 -- B3[返回错误]B2 -- 合法 -- B4[从Redis获取验证码]B4 -- B5{验证码是否正确}B5 -- 否 -- B6[返回验证码错误]B5 -- 是 -- B7[根据手机号查询用户]B7 -- B8{用户是否存在}B8 -- 否 -- B9[创建新用户]B8 -- 是 -- B10[使用已有用户]B9 -- B11[生成Token]B10 -- B11B11 -- B12[用户信息写入Redis]B12 -- B13[返回Token]endsubgraph C[请求拦截流程]C1[请求到达拦截器] -- C2[从请求头获取Token]C2 -- C3{Token是否存在}C3 -- 否 -- C4[返回401]C3 -- 是 -- C5[从Redis获取用户信息]C5 -- C6{用户是否存在}C6 -- 否 -- C4C6 -- 是 -- C7[保存用户到ThreadLocal]C7 -- C8[刷新Token有效期]C8 -- C9[放行请求]endsubgraph D[请求结束]D1[请求完成] -- D2[清理ThreadLocal]endB13 -- C1C9 -- D1复制到未命名绘图 - draw.io中用mermaid格式文件创建流程图优化目前之后访问被拦截的页面才会刷新有效期所以这里我们需要优化一下方式是采用拦截器链即再加一个拦截器来拦截全部页面以此来更新有效期RefreshTokenInterceptor12345678910111213141516171819202122232425262728293031323334Slf4jComponentpublicclassRefreshTokenInterceptorimplementsHandlerInterceptor {AutowiredprivateStringRedisTemplate stringRedisTemplate;OverridepublicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException {// 1.获取请求头中的tokenString token request.getHeader(authorization);if(StrUtil.isBlank(token)) {returntrue;}// 2.基于token获取redis中的用户String key RedisConstants.LOGIN_USER_KEY token;MapObject, Object userMap stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if(userMap.isEmpty()) {returntrue;}// 5.将查询到的Hash数据转换为UserDTO对象UserDTO userDTO BeanUtil.fillBeanWithMap(userMap,newUserDTO(),false);// 6.存在保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行returntrue;}OverridepublicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throwsException {// 移除用户UserHolder.removeUser();}}LoginInterceptor12345678910111213141516171819Slf4jComponentpublicclassLoginInterceptorimplementsHandlerInterceptor {OverridepublicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException {// 判断是否需要拦截ThreadLocal中是否有用户if(UserHolder.getUser() null) {response.setStatus(401);returnfalse;}// 有用户则放行returntrue;}OverridepublicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throwsException {// 移除用户UserHolder.removeUser();}}配置类12345678910111213141516171819202122232425ConfigurationpublicclassMvcConfigimplementsWebMvcConfigurer {AutowiredprivateLoginInterceptor loginInterceptor;AutowiredprivateRefreshTokenInterceptor refreshTokenInterceptor;OverridepublicvoidaddInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(loginInterceptor).addPathPatterns(/**).excludePathPatterns(/user/code,/user/login,/blog/hot,/shop/**,/shop-type/**,/upload/**,/voucher/**).order(1);// 刷新token拦截器registry.addInterceptor(refreshTokenInterceptor).addPathPatterns(/**).order(0);}}注order方法是用来设置哪一个拦截器在前哪一个在后规则数字小的在前数字大的在后

更多文章