黑马点评项目笔记

目录

短信登录

基于session实现短信登录

实现发送短信验证码功能

UserServiceImpl.java代码:

添加sendCode方法

java 复制代码
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1、校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //2、如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }

        //3、符合、生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4、保存验证码到session
        session.setAttribute("code",code);
        //5、发送验证码
        log.debug("发送短信验证码成功,验证码:{}",code);
        //返回ok
        return Result.ok();
    }
}

实现短信验证码登录和注册功能

UserServiceImpl.java代码:

添加login和createUserWithPhone方法

java 复制代码
@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1、校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //2、如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2、校验验证码
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.toString().equals(code)){
            //3、不一致,报错
            return Result.fail("验证码错误");
        }

        //4、一致、根据手机号查询用户
        User user = query().eq("phone",phone).one();
        //5、判断用户是否存在
        if(user == null){
            //6、不存在、创建新用户并保存
            user = createUserWithPhone(phone);
        }
        //7、保存用户信息到session中
        session.setAttribute("user",user);
        return Result.ok();
    }
    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;
    }

实现登录校验拦截器

1、创建拦截器loginInterceptor.java

java 复制代码
package com.hmdp.utils;

import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户
        UserHolder.removeUser();
    }

    @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) {
            //4、不存在,拦截并返回 401 状态码
            response.setStatus(401);
            return false;
        }
        //5、存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((User) user);
        //6、放行
        return true;
    }
}

2、配置拦截器MvcConfig.java

java 复制代码
package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器并排除需要拦截的路径
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}

3、实现/user/me接口

java 复制代码
@GetMapping("/me")
    public Result me(){
        // 获取当前登录的用户并返回
        User user = UserHolder.getUser();
        return Result.ok(user);
    }

登录成功点击我的调用/user/me接口会获取用户信息数据

隐藏用户敏感信息

登录时,不将user的完整信息存入session,而是实现一个UserDto封装信息,将userDto存入

java 复制代码
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

UserServiceImpl.java/login方法

java 复制代码
 @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1、校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //2、如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2、校验验证码
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.toString().equals(code)){
            //3、不一致,报错
            return Result.fail("验证码错误");
        }

        //4、一致、根据手机号查询用户
        User user = query().eq("phone",phone).one();
        //5、判断用户是否存在
        if(user == null){
            //6、不存在、创建新用户并保存
            user = createUserWithPhone(phone);
        }
        //7、保存用户信息到session中
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        return Result.ok();
    }

将拦截器中取出的user转换为userDto

java 复制代码
UserHolder.saveUser((UserDTO) user);

将/user/me接口取出的用户信息转换为userDTO

java 复制代码
@GetMapping("/me")
    public Result me(){
        // 获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

重新登录一下,调用/user/me接口得到用户的敏感信息已经隐藏

session共享的问题分析

session共享问题: 多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

session的替代方案应该满足

  • 数据共享
  • 内存存储
  • key、value结构

所以我们可以使用redis来解决。

redis代替session的业务流程

短信验证码使用手机作为key,用户信息使用token作为key。

实现登录注册时,需要将生成的token返回给前端,在校验时,拿到前端传递的token。

基于redis实现短信登录

UserServiceImpl.java

java 复制代码
/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1、校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //2、如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }

        //3、符合、生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4、保存验证码到 redis(可以加上业务前缀:login:code:以作区分),时间有效期2分钟
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //5、发送验证码
        log.debug("发送短信验证码成功,验证码:{}",code);
        //返回ok
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1、校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //2、如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2、从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(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中
        //7.1 随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString();
        //7.2 将user对象转为Hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //转成map时,自定义hashMap的值,将其中的参数转为string
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
        //7.3 存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        //7.4 设置token有效期
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //8、返回token
        return Result.ok(token);
    }
    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;
    }
}

LoginInterceptor.java

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户
        UserHolder.removeUser();
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、获取请求头中的token
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
            //不存在,拦截并返回 401 状态码
            response.setStatus(401);
            return false;
        }
        //2、基于token获取redis中的用户,使用entries取出所有hashMap
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
                .entries(key);
        //3、判断用户是否存在
        if (userMap.isEmpty()) {
            //4、不存在,拦截并返回 401 状态码
            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;
    }
}

注册拦截器时注入StringRedisTemplate

解决状态登录刷新的问题

所存在的问题: 目前拦截器是拦截需要登录的路径,若用户一直访问不需要登录的页面,则刷新token有效期则不会生效,导致用户会丢失登录态。

解决方案: 在此拦截器前加一个新的拦截器,拦截一切路径,并将刷新token有效期的代码逻辑放在这个拦截器。

新建刷新token拦截器RefreshTokenInterceptor.java

java 复制代码
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class RefreshTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户
        UserHolder.removeUser();
    }

    @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中的用户,使用entries取出所有hashMap
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> 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;
    }
}

修改登录拦截器LoginInterceptor.java

java 复制代码
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import io.netty.util.internal.StringUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class LoginInterceptor implements HandlerInterceptor {

    @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;
    }
}

注册拦截器MvcConfig.java

java 复制代码
package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器并排除需要拦截的路径(登录拦截器)
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                ).order(1);
        //token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

原有效时间

刷新首页后的有效时间被重置,说明代码没问题

商户查询缓存

添加商户缓存

shop/{id}获取店铺信息接口中修改

java 复制代码
/**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        return shopService.queryById(id);
    }

ShopServiceImpl.java

java 复制代码
/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryById(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        //1、从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3、存在、直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4、不存在,根据Id查询数据库
        Shop shop = getById(id);
        //5、不存在,返回错误
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        //6、存在,写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
        //7、返回
        return Result.ok(shop);
    }
}