黑马点评笔记(一)

此文章为总结黑马点评项目中的Java后端所用技术。视频链接:黑马点评项目。

1.基于Reids实现登录流程

该项目是使用短信发送验证码的方式登录,而验证码我们会将其保存到redis中并设置一个过期时间,用户发送验证码后在一定时间内提供验证码进行登录

Controller代码:(发送验证码)

@PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // TODO 发送短信验证码并保存验证码
        Result result = userService.sendCode(phone, session);
        return result;
    }

Service代码:(发送验证码)

/**
     * 发送验证码Redis版本
     * @param phone
     * @param session
     * @return 
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //0.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机格式有误");
        }
        //1.获取验证码
        String code = RandomUtil.randomNumbers(6);
        //2.将验证码保存到Redis中
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,3, TimeUnit.MINUTES);
        //3.发送验证码
        log.debug("验证码为:"+code);
        return Result.ok();
    }

这里用打日志的方式来模拟验证码发送。使用事先定义好的工具类RandomUtil来生成六位数的验证码。再使用springboot提供的Redis客户端StrigRedisTemplate来将验证码保存到redis数据库中。


Comtroller代码:(登录注册)

/**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // TODO 实现登录功能
        Result result = userService.login(loginForm, session);
        return result;
    }

Service代码:(登录注册)

/**
     * 登录注册Redis版本
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.获取前端上送的手机号和验证码
        String code = loginForm.getCode();
        String phone = loginForm.getPhone();
        //2.确定手机号的格式是否正确
        if(RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机格式错误");
        }
        //3.判断验证码是否正确
        String code1 = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if(code1 == null || !code1.equals(code)){
            return Result.fail("验证码错误");
        }
        //4.根据手机号查询是否有用户
        User user = query().eq("phone", phone).one();
        //5.若没有用户,则新增用户
        if(null == user) {
            //不存在,创建新用户并保存
            user = createUserWithPhone(phone);
            boolean save = save(user);
            if (!save) {
                return Result.fail("注册失败");
            }
        }
        //6将用户信息保存到Redis
        //6.1生成随机token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //6.2将user对象转换为HashMap
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).
                        setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        //6.3存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        // 6.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
        //7.返回结果
        return Result.ok(token);
    }
    /**
     * 创建新用户
     * @param phone
     * @return
     */
    public User createUserWithPhone(String phone){
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        return user;
    }

LoginFormDTO类代码:

import lombok.Data;

@Data
public class LoginFormDTO {
    private String phone;
    private String code;
    private String password;
}

以上代码使用UUID来创建随机token,再使用BeanUtil工具类来进行类型转换,最后再将用户对象使用HashMap类型存到Redis中。

2. 登录拦截

我们的项目有许多页面需要用户进行登录后才可以进行访问,因此要利用Spring的拦截器来进行判断哪些请求是没有用户登录也可以访问的哪些请求是需要用户登录才可以进行访问。同时,由于我们给用户登录的token设置了过期时间,因此需要做好刷新token的动作。代码如下:

  1. 利用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();
    }
}
  1. 创建第一层拦截
public class RefreshTokenInterceptor implements HandlerInterceptor {
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // TODO 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        //2.基于Token获取redis中的用户
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3.判断用户是否存在
        if (userMap.isEmpty()) {
           return true;
        }
        //4.将查询到的Hash数据转换为UserDto对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //5.存在,保存用户信息到Threadlocal
        UserHolder.saveUser(userDTO);
        //6.刷新token的有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        //7.放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}
  1. 创建第二层拦截
public class LoginInterceptor implements HandlerInterceptor {


    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private StringRedisTemplate stringRedisTemplate;
    /**
     * 使用Redis方法
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @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;
    }
}
  1. 设置拦截类
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate stringRedisTemplatel;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //登录拦截器
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplatel)).excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
        ).order(1);
        //token刷新拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplatel)).addPathPatterns("/**").order(0);
    }
}

这里利用实现HandlerInterceptor接口并重写其preHandle方法。再到config下配置使用拦截器,使用excludePathPatternsaddPathPatterns方法来写不需要和需要拦截的路径,再使用order方法来配置拦截器的先后顺序。