林芝文旅 發表於 2026-1-8 11:06:28

基于Redis实现登录功能思路详解(手机号+验证码)

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">UserServiceImpl实现类</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">sendCode方法</a></li><li><a href="#_lab2_0_1">login方法</a></li><li><a href="#_lab2_0_2">createUserWithPhone方法</a></li></ul><li><a href="#_label1">拦截器</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_3">preHandle方法</a></li></ul><li><a href="#_label2">整体思路</a></li><ul class="second_class_ul"></ul><li><a href="#_label3">优化</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_4">RefreshTokenInterceptor</a></li><li><a href="#_lab2_3_5">LoginInterceptor</a></li><li><a href="#_lab2_3_6">配置类</a></li></ul></ul></div><p>本文使用的是&nbsp;<strong>手机号+验证码</strong> 的登录方式,其中验证码是通过在控制台输出,并没有真的发送到手机上(太麻烦,主要目的还是学习使用Redis)</p>
<p>重点是看思路,而不是具体的代码实现</p>
<p class="maodian"><a name="_label0"></a></p><h2>UserServiceImpl实现类</h2>
<p>整体结构</p>
<div class="jb51code"><pre class="brush:java;">@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl&lt;UserMapper, User&gt; implements IUserService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result sendCode(String phone, HttpSession session) {
      //...
    }
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
      //...
    }
    private User createUserWithPhone(String phone) {
      //...
    }
}</pre></div>
<p class="maodian"><a name="_lab2_0_0"></a></p><h3>sendCode方法</h3>
<p>这个是发送验证码的方法</p>
<div class="jb51code"><pre class="brush:java;">public Result sendCode(String phone, HttpSession session) {
    // 1. 校验手机号
    if (RegexUtils.isPhoneInvalid(phone)) {
      // 2. 如果不符合,返回错误信息
      return Result.fail("手机号格式错误!");
    }
    // 3. 如果符合,生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 4. 保存验证码到redis
    stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY +phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
    // 5. 发送验证码
    log.debug("发送短信验证码成功,验证码:{}", code);
    // 6. 返回结果
    return Result.ok();
}</pre></div>
<p>注:这里的RedisConstants是一个用来存放各种常量的类</p>
<div class="jb51code"><pre class="brush:java;">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;
}</pre></div>
<p class="maodian"><a name="_lab2_0_1"></a></p><h3>login方法</h3>
<p>这里使用了MybatisPlus来操作数据库(User user = query().eq(&quot;phone&quot;, phone).one();),但是这个不是重点</p>
<div class="jb51code"><pre class="brush:java;">public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1. 校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
      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. 保存用户信息到redis
    String token= UUID.randomUUID().toString(true);
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map&lt;String, Object&gt; userMap = BeanUtil.beanToMap(userDTO,new HashMap&lt;&gt;(),
            CopyOptions.create()
                  .setIgnoreNullValue(true)
                  .setFieldValueEditor((fieldName, fieldValue)-&gt;fieldValue.toString()));
    stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
    stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
    return Result.ok(token);
}</pre></div>
<p class="maodian"><a name="_lab2_0_2"></a></p><h3>createUserWithPhone方法</h3>
<p>在login方法中调用了该方法</p>
<p>这里也使用了MybatisPlus来操作数据库(save(user);)</p>
<div class="jb51code"><pre class="brush:java;">private User createUserWithPhone(String phone) {
    // 1. 创建用户
    User user = new User();
    user.setPhone(phone);
    user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    // 2. 保存用户
    save(user);
    return user;
}</pre></div>
<p class="maodian"><a name="_label1"></a></p><h2>拦截器</h2>
<p>整体框架</p>
<p>其实就是实现了HandlerInterceptor的两个方法</p>
<div class="jb51code"><pre class="brush:java;">@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      //...
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      // 移除用户
      UserHolder.removeUser();
    }
}</pre></div>
<p>&nbsp;UserHolder是<strong>ThreadLocal 持有类</strong></p>
<div class="jb51code"><pre class="brush:java;">public class UserHolder {
    private static final ThreadLocal&lt;UserDTO&gt; tl = new ThreadLocal&lt;&gt;();
    public static void saveUser(UserDTO user){
      tl.set(user);
    }
    public static UserDTO getUser(){
      return tl.get();
    }
    public static void removeUser(){
      tl.remove();
    }
}</pre></div>
<p class="maodian"><a name="_lab2_1_3"></a></p><h3>preHandle方法</h3>
<div class="jb51code"><pre class="brush:java;">public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 1.获取请求头中的token
    String token = request.getHeader("authorization");
    if (StrUtil.isBlank(token)) {
      // 不存在,拦截
      response.setStatus(401);
      return false;
    }
    // 2.基于token获取redis中的用户
    String key = RedisConstants.LOGIN_USER_KEY + token;
    Map&lt;Object, Object&gt; userMap = stringRedisTemplate.opsForHash().entries(key);
    // 3.判断用户是否存在
    if (userMap.isEmpty()) {
      // 4.不存在,拦截
      response.setStatus(401);
      return false;
    }
    // 5.将查询到的Hash数据转换为UserDTO对象
    UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
    // 6.存在,保存用户信息到ThreadLocal
    UserHolder.saveUser(userDTO);
    // 7.刷新token有效期
    stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
    // 8.放行
    return true;
}</pre></div>
<p>注:authorization 是前端定义的用来传递token的key</p>
<p>配置类</p>
<div class="jb51code"><pre class="brush:java;">@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}</pre></div>
<p class="maodian"><a name="_label2"></a></p><h2>整体思路</h2>
<div class="jb51code"><pre class="brush:java;">flowchart TD
subgraph A[发送验证码流程]
    A1["前端请求 发送验证码"] --&gt; A2["校验手机号格式"]
    A2 -- 不合法 --&gt; A3["返回错误手机号格式错误"]
    A2 -- 合法 --&gt; A4["生成6位验证码"]
    A4 --&gt; A5["保存验证码到Redis"]
    A5 --&gt; A6["返回成功"]
end
subgraph B[登录流程]
    B1["前端请求 登录"] --&gt; B2["校验手机号格式"]
    B2 -- 不合法 --&gt; B3["返回错误"]
    B2 -- 合法 --&gt; B4["从Redis获取验证码"]
    B4 --&gt; B5{"验证码是否正确"}
    B5 -- 否 --&gt; B6["返回验证码错误"]
    B5 -- 是 --&gt; B7["根据手机号查询用户"]
    B7 --&gt; B8{"用户是否存在"}
    B8 -- 否 --&gt; B9["创建新用户"]
    B8 -- 是 --&gt; B10["使用已有用户"]
    B9 --&gt; B11["生成Token"]
    B10 --&gt; B11
    B11 --&gt; B12["用户信息写入Redis"]
    B12 --&gt; B13["返回Token"]
end
subgraph C[请求拦截流程]
    C1["请求到达拦截器"] --&gt; C2["从请求头获取Token"]
    C2 --&gt; C3{"Token是否存在"}
    C3 -- 否 --&gt; C4["返回401"]
    C3 -- 是 --&gt; C5["从Redis获取用户信息"]
    C5 --&gt; C6{"用户是否存在"}
    C6 -- 否 --&gt; C4
    C6 -- 是 --&gt; C7["保存用户到ThreadLocal"]
    C7 --&gt; C8["刷新Token有效期"]
    C8 --&gt; C9["放行请求"]
end
subgraph D[请求结束]
    D1["请求完成"] --&gt; D2["清理ThreadLocal"]
end
B13 --&gt; C1
C9 --&gt; D1</pre></div>
<p>复制到<a href="https://app.diagrams.net/" rel="external nofollow"   target="_blank" title="未命名绘图 - draw.io">未命名绘图 - draw.io</a>中用mermaid格式文件创建流程图</p>
<p class="maodian"><a name="_label3"></a></p><h2>优化</h2>
<p>目前之后访问被拦截的页面才会刷新有效期,所以这里我们需要优化一下</p>
<p>方式是采用拦截器链,即再加一个拦截器来拦截全部页面,以此来更新有效期</p>
<p class="maodian"><a name="_lab2_3_4"></a></p><h3>RefreshTokenInterceptor</h3>
<div class="jb51code"><pre class="brush:java;">@Slf4j
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      // 1.获取请求头中的token
      String token = request.getHeader("authorization");
      if (StrUtil.isBlank(token)) {
            return true;
      }
      // 2.基于token获取redis中的用户
      String key = RedisConstants.LOGIN_USER_KEY + token;
      Map&lt;Object, Object&gt; userMap = stringRedisTemplate.opsForHash().entries(key);
      // 3.判断用户是否存在
      if (userMap.isEmpty()) {
            return true;
      }
      // 5.将查询到的Hash数据转换为UserDTO对象
      UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
      // 6.存在,保存用户信息到ThreadLocal
      UserHolder.saveUser(userDTO);
      // 7.刷新token有效期
      stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
      // 8.放行
      return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      // 移除用户
      UserHolder.removeUser();
    }
}</pre></div>
<p class="maodian"><a name="_lab2_3_5"></a></p><h3>LoginInterceptor</h3>
<div class="jb51code"><pre class="brush:java;">@Slf4j
@Component
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;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      // 移除用户
      UserHolder.removeUser();
    }
}</pre></div>
<p class="maodian"><a name="_lab2_3_6"></a></p><h3>配置类</h3>
<div class="jb51code"><pre class="brush:java;">@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Autowired
    private RefreshTokenInterceptor refreshTokenInterceptor;
    @Override
    public void addInterceptors(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);
    }
}</pre></div>
<p>注:order方法是用来设置哪一个拦截器在前,哪一个在后;规则:数字小的在前,数字大的在后</p>
頁: [1]
查看完整版本: 基于Redis实现登录功能思路详解(手机号+验证码)