扩展阅读推荐:
黑马程序员Redis入门到实战教程_哔哩哔哩_bilibili
一、项目介绍及其初始化
学习Redis的过程,我们还将遇到各种实际问题,例如缓存击穿、雪崩、热Key等问题,只有在实际的项目实践中解决这些问题,才能更好的掌握和理解Redis的企业开发思维。
以下是本次【黑马点评】项目的主要内容:
 
 
  
 
 1.1 数据库连接配置
在yml配置文件中,配置好自己的数据库连接信息

 
 
 1.2 工程启动演示
想要成功启动该项目,需要以下步骤:
1. 打开VM虚拟机,激活Linux系统中事先配置好的Redis数据库
2. 启动nignx服务器(注意nignx服务器必须放在无中文的目录下)
3. 启动后端程序(观察端口,访问初始工程)
4. 访问请求地址,验证工程启动正确http://localhost:8081/shop-type/list
 
 
  
 
 二、短信登录功能实现
2.1 基于传统Session实现的短信登录及其校验
2.1.1 基于Session登录校验的流程设计

2.1.2 实现短信验证码发送功能
| 请求接口 | /user/code | 
| 请求类型 | post | 
| 请求参数 | phone | 
| 返回值 | 无 | 
    /**
     * 发送手机验证码
     */
    @PostMapping("/code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        log.info("发送验证码, 手机号:{}", phone);
        return userService.sendCode(phone, session);
    }
    /**
     * 发送验证码
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1. 校验手机号码
        if(RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号码格式错误!");
        }
        // 2. 生成验证码
        String code = RandomUtil.randomNumbers(6);
        // 3. 将验证码保存到Session中
        session.setAttribute("code", code);
        //TODO 4. 调用阿里云 将短信信息发送到指定手机
        log.info("发送短信验证码成功,验证码:{}", code);
        return Result.ok();
    }
2.1.3 实现登录、注册功能
| 请求接口 | /user/login | 
| 请求类型 | post | 
| 请求参数 | LoginForm---> phone,code,[password] | 
| 返回值 | 无 | 
    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        log.info("用户登录, 参数:{}", loginForm);
        return userService.login(loginForm, session);
    }
/**
     * 登录功能
     * @param loginForm
     * @param session
     * @return
     */
    @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)){
            return Result.fail("验证码错误!");
        }
        // 3. 根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        //    if 0 :创建新用户,保存数据库,将用户信息存储到Session
        //
        if(user == null){
            user = createUserWithPhone(phone);
        }
        //else: 登录成功,将用户信息存储到Session
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        return Result.ok();
    }
/**
     * 根据手机号创建用户
     * @param phone
     * @return
     */
    private User createUserWithPhone(String phone) {
        //创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        //保存用户
        save(user);
        // 返回
        return user;
    }2.1.4 实现登录状态校验拦截器
由于日后项目功能会越来越多,需要登录才能进行访问的界面也会越来越多,我们必须想办法将登录状态校验抽离出来形成一个前置校验的条件,再放行到后续逻辑。

1. 封装TreadLocal工具类
将用户信息保存到 TreadLocal中 并封装TreadLocal工具类用于 保存用户、获取用户、移除用户
在 urils / UserHolder
/**
 * TreadLocal工具类
 */
public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
    // 保存用户
    public static void saveUser(UserDTO user){
        tl.set(user);
    }
    // 获取ThreadLocal中的用户
    public static UserDTO getUser(){
        return tl.get();
    }
    // 清空ThreadLocal
    public static void removeUser(){
        tl.remove();
    }
}
2. 创建登录拦截器
在 urils / LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 前置拦截器
     */
    @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){
            response.setStatus(401);
            return false;
        }
        // 4. 如果存在,用户信息保存到 ThreadLocal 并放行
        UserHolder.saveUser((UserDTO) user);
        return true;
    }
    /**
     * 后置拦截器(移除用户)
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}
3. 添加配置,生效拦截器,并配置放行路径
在 config/ MvcConfig
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    /**
     * 添加拦截器
     * @param registry
     */
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/host",
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**"
                );
    }
}2.1.5 实现获取用户请求
前端点击我的,发送请求到后端,获取当前登录状态,方能进入个人中心
    /**
     * 获取当前登录的用户
     * @return
     */
    @GetMapping("/me")
    public Result me(){
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }2.1.6 (附加)用户信息脱敏处理
为防止出现以下这种情况(将用户隐私信息暴露过多),我们采用UserDTO对象对用户信息脱敏处理:

@Data public class UserDTO { private Long id; private String nickName; private String icon; }并借助拷贝工具 进行对象拷贝
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
2.2 传统Session在集群环境下的弊端
Session共享问题
多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
解决策略
1. 让Session可以共享
Tomcat提供了Session拷贝功能,但是这会增加服务器的额外内存开销,并且带来数据一致性问题
2. 【推荐】使用Redis进行替代
数据共享、内存存储(快)、key-value结构
2.3 基于Redis实现短信登录功能
2.3.1 基于Redis实现短信登录流程设计
 
 
  
 
 对于验证码,使用 手机号码作为KEY,确保了正确的手机对应着正确的短信验证码。
对于用户信息唯一标识,使用 UUID生成的Token作为 KEY,而不使用手机号码,从而提高了用户数据安全性。
2.3.2 修改发送短信验证码功能
只需要在Session的基础上,将第三步保存到Redis中
格式:
| key | value | TTL | 
| login:code:[手机号码] | [验证码] | 120S | 
//        // 3. 将验证码保存到Session中
//        session.setAttribute("code", code);
        // 3. 将验证码保存到Redis中
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
2.3.3 修改登录、注册功能
1. 手机号校验
2. 从Redis中取出验证码进行校验
3.查询用户信息
4. 将用户信息存储到Redis ---> 需要以Hash结构进行存储 ----> 需要将user对象转成 Map对象
5. 将token返回给客户端 ,充当Session的标识作用
/**
     * 登录功能
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号码格式错误!");
        }
        // 2. 校验验证码 REDIS
//        Object cacheCode = session.getAttribute("code");
        // 2.1 从Redis中获取验证码
        String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        // 2.2 校验验证码
        String code = loginForm.getCode();
        if(redisCode==null || !redisCode.equals(code)){
            return Result.fail("验证码错误!");
        }
        // 3. 根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        //    if 0 :创建新用户,保存数据库,将用户信息存储到Session
        if(user == null){
            user = createUserWithPhone(phone);
        }
//        //else: 登录成功,将用户信息存储到Session
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        // 4. 将用户信息存储到Redis中
         // 1. 随机生成token,作为登录令牌 ---> UUID导入工具包中的方法,不要导入java自带的
        String token = UUID.randomUUID().toString(true);
         // 2. 以hash结构进行存储
        UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
        //TODO 这里报错了,因为UserDTO中有个id属性,不是字符串,在Redis序列化下报错
//        Map<String,Object> userMap = BeanUtil.beanToMap(userDTO);
        Map<String,Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));
        // 3. 存储到Redis中
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token,userMap);
        // 给token设置有效期
        // 超过30分钟不访问任何界面就会剔除,所以还需要设置在访问过程中不断更新token的有效期
        // 实现方式: 在登录拦截器中进行处理
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 5. 返回token到客户端,客户端保存到浏览器中
        return Result.ok(token);
    }2.3.4 修改登录校验拦截器逻辑
首先,由于需要在自定义的拦截器中使用StringRedisTemplate对象



















