return Result.fail("验证码错误");
    }
    //判断用户是否存在
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
            .eq(User::getPhone, phone);
    User user = userMapper.selectOne(wrapper);
    if (user == null) {
        user = createUser(phone);
    }
    //优化:减少Tomcat内存的使用并隐藏用户的敏感信息
    session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
    return Result.ok();
}
/\*\*
* 创建用户
 *
 * @param phone
 * @return
 */
 private User createUser(String phone) {
 User user = new User();
 user.setPhone(phone);
 user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(8));
 userMapper.insert(user);
 return user;
 }
 @Data
 public class UserDTO {
 private Long id;
 private String nickName;
 private String icon;
 }
---


### 3、实现登录校验拦截器功能


**思路分析:**


* 为了避免在多个controller层中实现登录校验功能,可以使用拦截器,在访问controller之前,进行登录校验;
* 在后续的业务中,因为要用到用户信息,所以要把拦截器中的用户信息传到controller中,并且,为了确保线程安全问题,可以使用ThreadLocal类。我们可以把拦截到的用户信息保存到ThreadLocal对象中。
* [Java中的ThreadLocal详解]()




---
public class UserHolder {
 private static final ThreadLocal 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 {
 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 HttpSession session = request.getSession();
 UserDTO user = (UserDTO) session.getAttribute(“user”);
 if (user == null) {
 response.setStatus(401);
 return false;
 }
 UserHolder.saveUser(user);
 return true;
 }
 @Override
 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
 UserHolder.removeUser();
 }
 }
 @Configuration
 public class MyConfiguration implements WebMvcConfigurer {
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
 registry.addInterceptor(new LoginInterceptor())
 .excludePathPatterns(
 “/shop/**”,
 “/voucher/**”,
 “/shop-type/**”,
 “/upload/**”,
 “/blog/hot”,
 “/user/code”,
 “/user/login”
 );
 }
 }
---


**controller层代码:**
@GetMapping("/me")
public Result me() {
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}
---


**最后来看看基于session登录的完整流程:**


![在这里插入图片描述]()




---


## 二、session共享的问题分析



> 
> 为什么使用Redis实现登录功能,而不使用基于Session实现登录功能?考虑到多台Tomcat并不共享session存储空间(虽然多台Tomcat可以对数据进行拷贝,但是不仅会造成内存空间的浪费,而且还会因为存在数据拷贝时间上的延迟,如果在延迟时间内有使用者来访问,依然会出现数据不一致的情况!),当请求切换到不同tomcat服务时会导致数据丢失!所以,我们的解决方案应该满足:数据共享、内存存储、key-value结构。
> 
> 
> 




---


## 三、基于Redis实现短信登录


### 1、实现发送验证码功能


**思路分析:**


* 跟基于session发送验证码不同的是:我们把验证码放到redis数据库中,并设置验证码过期时间;
* 使用字符串数据结构:以手机号为key,验证码为value。


**代码实现:**
@PostMapping("/code")
public Result sendCode(@RequestParam("phone") String phone) {
    return userService.generateCode(phone);
}
@Override
public Result generateCode(String phone) {
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式错误");
    }
    String code = RandomUtil.randomNumbers(6);
    //设置验证码过期时间为2minutes
    redisTemplate.opsForValue().set(LOGIN\_CODE\_KEY + phone, code, LOGIN\_CODE\_TTL, TimeUnit.MINUTES);
    log.info("生成的验证码是:{}", code);
    return Result.ok();
}
---


### 2、实现用户登录和注册功能


**思路分析:**


* 跟基于session实现用户登录和注册不同的是:我们把用户对象放到redis数据库中并设置有效期;
* 对象存储使用哈希结构,随机生成token,作为登录令牌,并以token作为对象存储的key,对象值为value。




---


**代码实现:**
@Override
public Result login(LoginFormDTO loginForm) {
    String phone = loginForm.getPhone();
    if (!redisTemplate.hasKey(LOGIN\_CODE\_KEY + phone)) {
        return Result.fail("手机号改变,请重新获取验证码");
    }
    String cacheCode = redisTemplate.opsForValue().get(LOGIN\_CODE\_KEY + phone);
    String code = loginForm.getCode();
    if (code == null || !code.equals(cacheCode)) {
        return Result.fail("验证码错误");
    }
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
            .eq(User::getPhone, phone);
    User user = userMapper.selectOne(wrapper);
    if (user == null) {
        user = createUser(phone);
    }
    //保存用户信息到redis中
    String token = UUID.randomUUID().toString(true);
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    HashMap<String, String> hashMap = getHashMap(userDTO);
    token = LOGIN\_USER\_KEY + token;
    redisTemplate.opsForHash().putAll(token, hashMap);
    //设置有效期为30分钟
    redisTemplate.expire(token, LOGIN\_USER\_TTL, TimeUnit.SECONDS);
    return Result.ok(token);
}

/\*\*
* 用hashmap存储userDTO对象
 * @param userDTO
 * @return
 */
 private HashMap<String, String> getHashMap(UserDTO userDTO) {
 HashMap<String, String> hashMap = new HashMap<>();
 hashMap.put(“id”, userDTO.getId().toString());
 hashMap.put(“nickName”, userDTO.getNickName());
 hashMap.put(“icon”, userDTO.getIcon());
 return hashMap;
 }
---


### 3、用拦截器实现用户有效期更新


* 我们在service层设置了用户在不做任何操作的情况下,保存用户信息的有效期为30分钟,但是如果用户进行了某种操作,比如点赞、发布博客等等需要登录后才能完成的操作,就需要重新设置用户信息有效期为30分钟。




---


**代码实现:**
public class LoginInterceptor implements HandlerInterceptor {
 private StringRedisTemplate redisTemplate;
 //构造器注入
 public LoginInterceptor(StringRedisTemplate redisTemplate) {
 this.redisTemplate = redisTemplate;
 }
 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 // 获取请求头中的token
 String token = request.getHeader(“authorization”);
 if (StrUtil.isBlank(token)) {
 response.setStatus(401);
 return false;
 }
 // 基于token获取redis中的用户
 Map<Object, Object> userMap = redisTemplate.opsForHash().entries(token);
 // 判断用户是否存在
 if (userMap.isEmpty()) {
 response.setStatus(401);
 return false;
 }
 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
 UserHolder.saveUser(userDTO);
 // 刷新token有效期
 redisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);
 return true;
 }
 @Override
 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
 UserHolder.removeUser();
 }
 }
---


### 4、拦截器功能的优化


**思路分析:**


* 前面设计的拦截器还是存在一些问题:如果用户在30分钟的时间内,操作的是拦截器不拦截的操作(比如浏览商铺、查看他人博客),那么就会导致用户的有效期时间不会刷新;
* 为了解决这个问题,我们可以再加一个拦截器,把它放在第一个位置,用来拦截所有的请求。




---


**代码实现:**