项目代码导入

1.mysql数据库表准备

执行下面的hmdp.sql文件:
在这里插入图片描述

结果如下:
在这里插入图片描述
在这里插入图片描述

2.项目架构

项目不采用微服务的模式,采用前后端分离的模式(前端部署在nginx服务器上,后端通过idea启动(使用tomcat))。用户请求通过nginx分发,再向服务端tomcat发起请求,服务端查询数据,可能是redis集群,也可能是mysql集群。
在这里插入图片描述

3.导入后端项目

用idea打开下面的项目:
在这里插入图片描述

项目修改前为:
在这里插入图片描述
在这里插入图片描述

记得修改application.yaml中的mysql和redis配置。
验证服务是否能正常启动:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
访问http://localhost:8081/shop-type/list显示如下,则成功。
在这里插入图片描述

4.导入前端项目

复制nginx项目到无中文的路径:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
输入指令:start nginx.exe
在这里插入图片描述打开浏览器搜:
http://localhost:8080
在这里插入图片描述

一、问题解释

以用户登录为例。用户登录时的信息保存在tomcat内存的session中(session都有一个sessionId,用户发送请求时的cookie携带sessionId帮助找到对应的session)。但是,这不适用于集群模式,会存在session共享问题。
session共享问题:多台tomcat并不共享session存储空间,当请求切换到不同tomcat服务器时会导致数据丢失的问题。
需要找到一个替代session的东西,session的替代方案应该满足:
(1)数据共享;
(2)内存存储;
(3)key、value结构。
可以看出,redis符合以上条件。

二、redis作为session集群替代方案

使用session时的流程:
在这里插入图片描述
使用redis时的流程:
在这里插入图片描述
在这里插入图片描述
上图中的token是用户的登录凭证,相当于session中的sessionId。

三、前端代码实现

在这里插入图片描述
上图是前端的登录请求,后台返回的数据data保存在前端session中的token中。
在这里插入图片描述
通过拦截器设置每次发送axios请求都携带token作为请求头authorization。
可以看出,登录凭证保存在前端浏览器。

四、后端代码实现

4.1 使用session

4.1.1 发送手机验证码

在这里插入图片描述
点击“发送验证码”按钮,可以看到请求地址为http://localhost:8080/api/user/code。
找到代码位置:
在这里插入图片描述
修改如下:
在这里插入图片描述

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

在这里插入图片描述

public interface IUserService extends IService<User> {

    Result sendCode(String phone, HttpSession session);
}

在这里插入图片描述

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

        return Result.ok();
    }
}

重启服务,点击前端的“发送验证码”按钮。

4.1.2 短信登录和注册

在这里插入图片描述
在这里插入图片描述

根据http://localhost:8080/api/user/login找到代码位置:
在这里插入图片描述
请求参数LoginFormDTO为自定义类:
在这里插入图片描述
修改controller类:
在这里插入图片描述

    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // TODO 实现登录功能
        return userService.login(loginForm,session);
    }

修改service接口:
在这里插入图片描述

 Result login(LoginFormDTO loginForm, HttpSession session);

修改service实现类:
在这里插入图片描述

@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) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        save(user);
        return user;
    }

启动服务进行测试:
在这里插入图片描述
登录后还是回到这个界面(后面增加登录校验功能即拦截器):
在这里插入图片描述
数据库新增记录:
在这里插入图片描述

4.1.3 拦截器

登录验证请求如下:
在这里插入图片描述
拦截器是在每次请求前进行登录校验功能。
拦截器中获取到的信息保存到ThreadLocal中,便于后面Controller从ThreadLocal取出信息使用。

在这里插入图片描述
在这里插入图片描述

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;
import javax.xml.ws.handler.Handler;

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){
            //4.不存在,拦截
            response.setStatus(401);
            return false;
        }

        //5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((User)user);
        //6.放行
        return true;
    }

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

其中:
在这里插入图片描述

package com.hmdp.utils;

import com.hmdp.dto.UserDTO;

public class UserHolder {
    private static final ThreadLocal<User> tl = new ThreadLocal<>();

    public static void saveUser(User user){
        tl.set(user);
    }

    public static User getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

配置拦截器:
在这里插入图片描述

@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/**"
        );////配置拦截路径

    }
}

再编写通过拦截器校验后的处理:
在这里插入图片描述

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

启动服务进行测试:
在这里插入图片描述
在这里插入图片描述

补充:隐藏用户敏感信息

上图中接口返回的信息过多,只需要id,昵称,图片icon就行。
在这里插入图片描述

package com.hmdp.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * <p>
 * 
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 手机号码
     */
    private String phone;

    /**
     * 密码,加密存储
     */
    private String password;

    /**
     * 昵称,默认是随机字符
     */
    private String nickName;

    /**
     * 用户头像
     */
    private String icon = "";

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;


}

之前是返回给前端User对象,现改为下面的UserDTO对象。
在这里插入图片描述

import lombok.Data;

@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

修改UserController的登录注册接口,将UserDTO对象存到session:
在这里插入图片描述
在这里插入图片描述
修改用到的UserHolder:
在这里插入图片描述
在这里插入图片描述
启动服务验证:
在这里插入图片描述
在这里插入图片描述

4.2 使用redis

为解决一、问题解释中的session不共享问题,可以使用redis替代session。虽然session具有session拷贝的能力,但浪费空间且有延迟。
注意使用redis替代session实现登录注册的细节:
(1)浏览器有其独立的session,但是redis是共享的,存储时命名需要针对不同手机号来存储不同信息,即redis的key值得唯一性。
使用session时:
在这里插入图片描述
改成使用redis:
在这里插入图片描述
(2)session机制中会将sessionid保存在用户请求得cookies中,但是使用redis需要添加维护sessionid的机制,这里使用token,为每个用户随机生成token,并将token返回给前端,前端的请求需要携带token。
在这里插入图片描述

4.2.1 修改发送短信验证码

在这里插入图片描述
也可以对代码进行优化,将一些字段信息单独存储在一个类中:
在这里插入图片描述

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 = 36000L;

    public static final Long CACHE_NULL_TTL = 2L;

    public static final Long CACHE_SHOP_TTL = 30L;
    public static final String CACHE_SHOP_KEY = "cache:shop:";

    public static final String LOCK_SHOP_KEY = "lock:shop:";
    public static final Long LOCK_SHOP_TTL = 10L;

    public static final String SECKILL_STOCK_KEY = "seckill:stock:";
    public static final String BLOG_LIKED_KEY = "blog:liked:";
    public static final String FEED_KEY = "feed:";
    public static final String SHOP_GEO_KEY = "shop:geo:";
    public static final String USER_SIGN_KEY = "sign:";
}

修改如下:
在这里插入图片描述

4.2.2 修改短信登录和注册

在这里插入图片描述
在这里插入图片描述

     @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2.校验验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
        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));
        //随机生成token,作为登录令牌
        String token= UUID.randomUUID().toString(true);
        //将user对象转为hash存储
        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()));
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //返回token
        return Result.ok(token);
    }

4.2.3 修改拦截器

(除了获取用户信息方式修改外,还需要更新token有效期):
在这里插入图片描述
说明:上图中在MvcConfig.java中添加StringRedisTemplate对象是为了在自定义的拦截器工具类LoginInterceptor.java中实现用户再次访问时更新token有效期。之所以不在LoginInterceptor.java中直接添加StringRedisTemplate对象,是因为MvcConfig.java有@Configuration注解,由spring容器自动创建对象,可以注入StringRedisTemplate对象。

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))////配置拦截器
        .excludePathPatterns(
                "/user/code",
                "/user/login",
                "/blog/hot",
                "/shop/**",
                "/shop-type/**",
                "upload/**",
                "voucher/**"
        );////配置拦截路径

    }
}

再修改拦截器工具类:
在这里插入图片描述
在这里插入图片描述

public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
        this.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)){
            response.setStatus(401);
            return false;
        }
        //2.基于token获取redis中的用户
        Map<Object,Object> userMap=stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);

        //3.判断用户是否存在
        if(userMap.isEmpty()){
            //4.不存在,拦截
            response.setStatus(401);
            return false;
        }
        //将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO= BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
        //5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //刷新token有效期(使用redis新增)
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        //6.放行
        return true;
    }

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

启动服务验证:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

总结:Redis代替session需要考虑的问题

(1)选择合适的数据结构
(2)选择合适的key
(3)选择合适的存储粒度

补充:解决状态登录刷新问题

4.2.3修改拦截器中有进行token有效期刷新的操作,但是存在问题,拦截器不是拦截一切请求(比如首页,商户信息页等不被拦截器拦截),这些请求不会去刷新token,存在问题。
解决方法是在原有拦截器上再新加一个拦截器:
在这里插入图片描述
改为:
在这里插入图片描述
在这里插入图片描述

public class RefreshTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
        this.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中的用户
        Map<Object,Object> userMap=stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);

        //3.判断用户是否存在
        if(userMap.isEmpty()){
            return true;
        }
        //将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO= BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
        //5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //刷新token有效期(使用redis新增)
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        //6.放行
        return true;
    }

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

在这里插入图片描述

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

}

配置两个拦截器:
在这里插入图片描述

@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);////配置拦截路径
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

这里就不展示效果了,可以自行尝试,登录和点击首页都会更新token有效期。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐