@
用户登录 sql 脚本
DROP TABLE IF EXISTS `seckill_user`;
CREATE TABLE `seckill_user`
(
    `id`              BIGINT(20)   NOT NULL COMMENT '用户 ID, 设为主键, 唯一 手机号',
    `nickname`        VARCHAR(255) NOT NULL DEFAULT '',
    `password`        VARCHAR(32)  NOT NULL DEFAULT '' COMMENT 'MD5(MD5(pass 明 文 + 固 定
salt)+salt)',
    `slat`            VARCHAR(10)  NOT NULL DEFAULT '',
    `head`            VARCHAR(128) NOT NULL DEFAULT '' COMMENT '头像',
    `register_date`   DATETIME              DEFAULT NULL COMMENT '注册时间',
    `last_login_date` DATETIME              DEFAULT NULL COMMENT '最后一次登录时间',
    `login_count`     INT(11)               DEFAULT '0' COMMENT '登录次数',
    PRIMARY KEY (`id`)
) ENGINE = INNODB
  DEFAULT CHARSET = utf8mb4;
MD5 的加密的依赖包:
这里我们解读一下密码的设计!!:
登录为例讲解:
传统方式:
客户端——> password 明文——>后端(md5(password 明文)) == db 中存放的 password 是否一致) :这种传统方式存在的问题:

传统方式改进的方式 1:客户端——> md5(password 明文)——>后端(md5(md5(password 明文)) ) == db 中存放的 password 是否一致) 。这样就算黑客拦截到了我们前端发送的信息,也是被加密的,所以无妨。
我们可以在传统方式的基础上,在 进行一个加盐上的处理。让密码更加安全一些。
传统方式改进的方式 2:客户端——> md5(password 明文+salt1(固定的盐))——>后端(md5(md5(password 明文+salt1(固定的盐)+salt2(从数据库当中获取的盐,不同用户对应的盐也不同))) ) == db 中存放的 password 是否一致) 。
注意:是对称加密的。
Md5 加密所需的相关依赖 。
      
        
            commons-codec 
            commons-codec 
            1.15 
         
        
            org.apache.commons 
            commons-lang3 
            3.11 
         
Junit Jupiter: Junit Jupiter 提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心。内部 包含了一个测试引擎,用于在 Junit Platform 上运行
        
            org.apache.commons 
            commons-lang3 
            3.11 
         
        
        
            org.junit.jupiter 
            junit-jupiter-api 
            5.7.2 
            compile 
         
加密密码工具类编写
package com.rainbowsea.seckill.utill;
import org.apache.commons.codec.digest.DigestUtils;
/**
 * MD5 加密工具类,根据前面密码设计方案提供相应的方法
 */
public class MD5Util {
    /**
     * 第一次加密所需的盐。
     */
    private static final String SALT = "UCmP7xHA";
    /**
     * MD5 加密
     *
     * @param src 要加密的字符串
     * @return String
     */
    public static String md5(String src) {
        return DigestUtils.md5Hex(src);
    }
    /**
     * 加密加盐,完成的是 md5(pass+salt1)
     *
     * @param inputPass 输入的密码
     * @return String
     */
    public static String inputPassToMidPass(String inputPass) {
        String str = "" + SALT.charAt(0) + inputPass + SALT.charAt(6);
        return md5(str);
    }
    /**
     * 这个盐随机生成,成的是 md5( md5(pass+salt1)+salt2)
     *
     * @param midPass 加密的密码
     * @param salt    从数据库获取到不同用户加密的盐
     * @return String
     */
    public static String midPassToDBPass(String midPass, String salt) {
        String str = salt.charAt(1) + midPass + salt.charAt(5);
        return md5(str);
    }
    /**
     * 进行两次加密加盐 最后存到数据库的 md5( md5(pass+salt1)+salt2)
     * salt1是前端进行的salt2 是后端进行的随机生成
     */
    public static String inputPassToDBPass(String inputPass, String salt) {
        String midPass = inputPassToMidPass(inputPass);
        String dbPass = midPassToDBPass(midPass, salt);
        return dbPass;
    }
}

validation参数校验
        
        
            org.springframework.boot 
            spring-boot-starter-validation 
            2.4.5 
         

用户登录逻辑编写:
package com.rainbowsea.seckill.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.seckill.mapper.UserMapper;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.MD5Util;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * @author huo
 * @description 针对表【seckill_user】的数据库操作Service实现
 * @createDate 2025-04-24 15:38:01
 */
@Service
public class UserServiceImpl extends ServiceImpl
        implements UserService {
    @Resource
    private UserMapper userMapper;
    /**
     * 登录校验
     *
     * @param loginVo  登录时发送的信息
     * @param request  request
     * @param response response
     * @return RespBean
     */
    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
        String mobile = loginVo.getMobile();
        String password = loginVo.getPassword();
        // 判断手机号/id,和密码是否为空
        if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        // 判断手机号是否合格
        if (!ValidatorUtil.isMobile(mobile)) {
            return RespBean.error(RespBeanEnum.MOBILE_ERROR);
        }
        // 查询DB,判断用户是否存在
        User user = userMapper.selectById(mobile);
        if (null == user) {
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        // 如果用户存在,则对比密码!
        // 注意:我们从 LoginVo 取出的密码是中间密码(即客户端经过一次加密加盐处理的密码)
        if (!MD5Util.midPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        // 登录成功
        return RespBean.success(user);
    }
}
 
package com.rainbowsea.seckill.controller;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
@Slf4j
@Controller
@RequestMapping("/login")
public class LoginController {
    @Resource
    private UserService userService;
    /**
     * 用户登录
     *
     * @return 返回登录页面
     */
    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login"; // 到templates/login.html
    }
    /**
     * 登录功能
     */
    @RequestMapping("/doLogin")
    @ResponseBody
    public RespBean doLogin
    (@Valid LoginVo loginVo, HttpServletRequest request,
     HttpServletResponse response) {
        log.info("{}", loginVo);
        return userService.doLogin(loginVo, request, response);
    }
}

自定义校验器:

package com.rainbowsea.seckill.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
 * 开发一个自定义的注解:替换如下,登录校验时的代码
 * 
 * 
 * // 判断手机号/id,和密码是否为空
 * if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
 * return RespBean.error(RespBeanEnum.LOGIN_ERROR);
 * }
 * 
 * // 判断手机号是否合格
 * if (!ValidatorUtil.isMobile(mobile)) {
 * return RespBean.error(RespBeanEnum.MOBILE_ERROR);
 * }
 */
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER,
        TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {
    String message() default "手机号码格式错误";
    boolean required() default true;
    Class>[] groups() default {}; // 默认参数
    Class extends Payload>[] payload() default {}; //默认参数
}
package com.rainbowsea.seckill.validator;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
 * 我们自拟定注解 IsMobile(手机号是否正确) 的校验规则
 */
public class IsMobileValidator implements ConstraintValidator {
    private boolean required = false;
    @Override
    public void initialize(IsMobile constraintAnnotation) {
        // 初始化
        required = constraintAnnotation.required();
    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        //必填
        if (required) {
            return ValidatorUtil.isMobile(value);
        } else {//非必填
            if (!StringUtils.hasText(value)) {
                return true;
            } else {
                return ValidatorUtil.isMobile(value);
            }
        }
    }
}
 
package com.rainbowsea.seckill.vo;
import com.rainbowsea.seckill.validator.IsMobile;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
/**
 * 接收用户登录时,发送的信息(mobile,password)
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginVo {
    // 添加 validation 组件后使用
    @NotNull
    @IsMobile  //自拟定注解
    private String mobile;
    @Length(min = 32)
    @NotNull
    private String password;
}



package com.rainbowsea.seckill.exception;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * 全局异常处理类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {
    private RespBeanEnum respBeanEnum;
}

package com.rainbowsea.seckill.exception;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.validation.BindException;
/**
 * 全局异常处理定义
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 处理所有的异常
     *
     * @param e 异常对象
     * @return RespBean
     */
    @ExceptionHandler(Exception.class)
    public RespBean ExceptionHandler(Exception e) {
        //如果是全局异常,正常处理
        if (e instanceof GlobalException) {
            GlobalException ex = (GlobalException) e;
            return RespBean.error(ex.getRespBeanEnum());
        } else if (e instanceof BindException) {  // BindException 绑定异常
            // 如果是绑定异常 :由于我们自定义的注解只会在控制台打印错误信息,想让改信息传给前端。
            // 需要获取改异常 BindException,进行打印
            BindException ex = (BindException) e;
            RespBean respBean = RespBean.error(RespBeanEnum.BING_ERROR);
            respBean.setMessage(" 参 数 校 验 异 常 ~ : " +
                    ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
            return respBean;
        }
        return RespBean.error(RespBeanEnum.ERROR);
    }
}

package com.rainbowsea.seckill.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.seckill.exception.GlobalException;
import com.rainbowsea.seckill.mapper.UserMapper;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.MD5Util;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * @author huo
 * @description 针对表【seckill_user】的数据库操作Service实现
 * @createDate 2025-04-24 15:38:01
 */
@Service
public class UserServiceImpl extends ServiceImpl
        implements UserService {
    @Resource
    private UserMapper userMapper;
    /**
     * 登录校验
     *
     * @param loginVo  登录时发送的信息
     * @param request  request
     * @param response response
     * @return RespBean
     */
    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
        String mobile = loginVo.getMobile();
        String password = loginVo.getPassword();
        // 判断手机号/id,和密码是否为空
        //if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
        //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        //}
        // 判断手机号是否合格
        //if (!ValidatorUtil.isMobile(mobile)) {
        //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        //}
        // 查询DB,判断用户是否存在
        User user = userMapper.selectById(mobile);
        if (null == user) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        // 如果用户存在,则对比密码!
        // 注意:我们从 LoginVo 取出的密码是中间密码(即客户端经过一次加密加盐处理的密码)
        if (!MD5Util.midPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        // 登录成功
        return RespBean.success(user);
    }
}
 完成测试 , 运行项目,访问 http://localhost:8080/login/toLogin



编写工具类:
第一个工具类:用于生成唯一的 UUID ,作为一个唯一的 userTicket


package com.rainbowsea.seckill.utill;
import java.util.UUID;
/**
 * 用户生产唯一的 UUID ,作为 session
 */
public class UUIDUtil {
    public static String uuid() {
        // 默认下: 生成的字符串形式 xxxx-yyyy-zzz-ddd
        // 把 UUID中的-替换掉,所以使用 replace("-", "")
        return UUID.randomUUID().toString().replace("-", "");
    }
}
这是一个工具类, 直接使用即可. (该工具了,可以让我们更方便的操作 cookie , 比如编码处理等等
package com.rainbowsea.seckill.utill;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
 * 这是一个工具类, 直接使用即可. (该工具了,可以让我们更方便的操作 cookie , 比如编码
 * 处理等等.
 */
public class CookieUtil {
    /**
     * 得到Cookie的值, 不编码
     *
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String
            cookieName) {
        return getCookieValue(request, cookieName, false);
    }
    /**
     * 得到Cookie的值,
     *
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String
            cookieName, boolean isDecoder) {
        Cookie[] cookieList = request.getCookies();
        if (cookieList == null || cookieName == null) {
            return null;
        }
        String retValue = null;
        try {
            for (int i = 0; i  0) {
                cookie.setMaxAge(cookieMaxage);
            }
//            if (null != request) {// 设置域名的cookie
//                String domainName = getDomainName(request);
//                if (!"localhost".equals(domainName)) {
//                    cookie.setDomain(domainName);
//                }
//            }
            cookie.setPath("/");
            response.addCookie(cookie);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 设置Cookie的值,并使其在指定时间内生效
     *
     * @param cookieMaxage cookie生效的最大秒数
     */
    private static final void doSetCookie(HttpServletRequest request,
                                          HttpServletResponse response,
                                          String cookieName, String cookieValue,
                                          int cookieMaxage, String encodeString) {
        try {
            if (cookieValue == null) {
                cookieValue = "";
            } else {
                cookieValue = URLEncoder.encode(cookieValue, encodeString);
            }
            Cookie cookie = new Cookie(cookieName, cookieValue);
            if (cookieMaxage > 0) {
                cookie.setMaxAge(cookieMaxage);
            }
            if (null != request) {// 设置域名的cookie
                String domainName = getDomainName(request);
                System.out.println(domainName);
                if (!"localhost".equals(domainName)) {
                    cookie.setDomain(domainName);
                }
            }
            cookie.setPath("/");
            response.addCookie(cookie);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 得到cookie的域名
     */
    private static final String getDomainName(HttpServletRequest request) {
        String domainName = null;
        // 通过request对象获取访问的url地址
        String serverName = request.getRequestURL().toString();
        if ("".equals(serverName)) {
            domainName = "";
        } else {
            // 将url地下转换为小写
            serverName = serverName.toLowerCase();
            // 如果url地址是以http://开头 将http://截取
            if (serverName.startsWith("http://")) {
                serverName = serverName.substring(7);
            }
            int end = serverName.length();
            // 判断url地址是否包含"/"
            if (serverName.contains("/")) {
                //得到第一个"/"出现的位置
                end = serverName.indexOf("/");
            }
            // 截取
            serverName = serverName.substring(0, end);
            // 根据"."进行分割
            final String[] domains = serverName.split("\.");
            int len = domains.length;
            if (len > 3) {
                // www.abc.com.cn
                domainName = domains[len - 3] + "." + domains[len - 2] + "." +
                        domains[len - 1];
            } else if (len > 1) {
                // abc.com or abc.cn
                domainName = domains[len - 2] + "." + domains[len - 1];
            } else {
                domainName = serverName;
            }
        }
        if (domainName.indexOf(":") > 0) {
            String[] ary = domainName.split("\:");
            domainName = ary[0];
        }
        return domainName;
    }
}
注意:将 ticket 保存到 cookie,cookieName 不可以随便写,必须时 "userTicket"。





上图分析-分布式存在的 Session 共享问题:
解决方案:
什么是 session 绑定/粘滞/黏滞

Session 绑定/粘滞/黏滞 :服务器会把用户的请求急,交给 tomcat 集群中的一个节点,以后此节点就复杂该用户的 Session。
Hash(ip_hast)算法实现。优点:不占用服务端内存
缺点:
增加新机器,会重新 Hash,导致重新登录,前面存储的就 Session 信息丢失。
应用重启,也是需要重新登录。
某台服务器宕机,该机器上的 Session 也就不存在了,用户请求切换到其他机器后,因为没有 Session 而无法完成业务处理,这种发案不符合系统高可用需求,使用较少。
Session 复制:

Session 复制:是小型架构使用较多的一种服务器集群 Session 管理机制。
优点:无需修改代码,修改 Tomcat 配置即可。
缺点:
Session 同步传输占用内网带宽。
多台 Tomcat 同步性能指数级下降。
Session 占用内存,无法有效水平扩展。
前端存储
优点:不占用服务器内存
缺点:
存在安全风险。
数据大小受到 Cookie 本身容量的限制。
占用外网带宽
后端集中存储:使用第三方的缓存数据库存储,比如:Redis 存储我们的 Session 信息。
优点:安全,容易水平扩展
缺点:增加复杂度,需要修改代码
一句话:将用户 Session 不再存放到各自登录的 Tomcat 服务器,而是统一存在 Redis,从而解决 Session 分布式问题

需求说明: 用户登录,将用户 Session 统一存放到指定 Redis ,而不是分布式存放到不同
的 Tomcat 所在服务器

安装配置 Redis:大家可以参考移步至:✏️✏️✏️ 二. Redis 超详细的安装教程((七步)一步一步指导,步步附有截屏操作步骤)_truenas安装redis-CSDN博客
安装 redis-desktop-manager
一句话:这个是 Redis 可视化操作工具
安装过程非常简单,直接下一步即可
启动我们虚拟机当中的 Redis 服务器 :

[root@localhost ~]# redis-server /etc/redis.conf
[root@localhost ~]# ps -aux | grep redis
root       3690  0.2  0.2 162516  9956 ?        Ssl  09:08   0:00 redis-server *:6379
root       3696  0.0  0.0 112812   980 pts/1    S+   09:08   0:00 grep --color=auto redis
#打开端口
firewall-cmd --zone=public --add-port=6379/tcp --permanent
#重启防火墙, 才能生效
firewall-cmd --reload
#查看端口是否打开
firewall-cmd --list-ports


protected-mode no

重启 Redis 生效
通过 telnet 来连接 Linux Redis , 看看是否 OK,如果连接不上,检查前面的配置是否
正确, 特别注意: 需要保证 Redis Desktop 所在机器, 允许访问 6379
运行 Redis Desktop, 连接到 Linux 的 Redis


在 pom.xml 文件当中加入相关的 Redis 依赖。
 
        
            org.springframework.boot 
            spring-boot-starter-data-redis 
            2.4.5 
         
        
        
            org.apache.commons 
            commons-pool2 
            2.9.0 
         
        
        
            org.springframework.session 
            spring-session-data-redis 
         
在项目的 application.yml, 配置 Redis 信息

spring:
    #  配置Redis
  redis:
    host: 192.168.198.135
    port: 6379
    password: rainbowsea
    database: 0
    timeout: 10000ms
    lettuce:
      pool:
        #最大连接数,默认是8
        max-active: 8
        #最大连接等待/阻塞时间,默认-1
        max-wait: 10000ms
        #最大空闲连接
        max-idle: 200
        #最小空闲连接,默认0
        min-idle: 5
#mybatis-plus配置
mybatis-plus:
  #配置mapper.xml映射文件
  mapper-locations: classpath*:/mapper/*Mapper.xml
  #配置mybatis数据返回类型别名
  type-aliases-package: com.rainbowsea.seckill.pojo
#mybatis sql 打印
logging:
  level:
    com.rainbowsea.seckill.mapper: debug
server:
  port: 8080
完成测试,启动项目,用户登录
浏览器输入 http://localhost:8080/login/toLogin


如下优化将:保存到 Redis 当中的数据
一句话:前面将 Session 统一存放指定 Redis, 是以原生的形式存放, 在操作时, 还需要反序列化,不方便,我们可以直接将登录用户信息统一存放到 Redis, 利于操作
如图-将登录用户信息统一存放到 Redis, 方便操作


我们进行改进: 直接将登录用户信息统一存放到 Redis, 利于操作
这里,我们既然要使用自己配置的 Session ,将信息直接存储到 Redis 的话,我们必须要将在 pom.xml 当中 org.springframework.session提供的 Session 会话处理的 jar 报依赖,注释掉,防止冲突。

创建:RedisConfig.java 一个关于 Redis 的一个配置类。
把 session 信息提取出来存到 redis 中,主要实现序列化, 这里是以常规操作

package com.rainbowsea.seckill.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
 * 把session信息提取出来存到redis中
 * 主要实现序列化, 这里是以常规操作
 * @author Rainbowsea
 * @version 1.0
 */
@Configuration
public class RedisConfig {
    /**
     * 自定义 RedisTemplate对象, 注入到容器
     * 后面我们操作Redis时,就使用自定义的 RedisTemplate对象
     * @param redisConnectionFactory
     * @return RedisTemplate
     */
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置相应key的序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //value序列化
        //redis默认是jdk的序列化是二进制,这里使用的是通用的json数据,不用传具体的序列化的对象
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //设置相应的hash序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        //注入连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        System.out.println("测试--> redisTemplate" + redisTemplate.hashCode());
        return redisTemplate;
    }
    /**
     * 增加执行脚本
     * @return DefaultRedisScript
     */
    @Bean
    public DefaultRedisScript script() {
        DefaultRedisScript redisScript = new DefaultRedisScript();
        //设置要执行的lua脚本位置, 把lock.lua文件放在resources
        redisScript.setLocation(new ClassPathResource("lock.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}
      
package com.rainbowsea.seckill.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.seckill.exception.GlobalException;
import com.rainbowsea.seckill.mapper.UserMapper;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.CookieUtil;
import com.rainbowsea.seckill.utill.MD5Util;
import com.rainbowsea.seckill.utill.UUIDUtil;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * @author huo
 * @description 针对表【seckill_user】的数据库操作Service实现
 * @createDate 2025-04-24 15:38:01
 */
@Service
public class UserServiceImpl extends ServiceImpl
        implements UserService {
    @Resource
    private UserMapper userMapper;
    @Resource
    private RedisTemplate redisTemplate;
    /**
     * 登录校验
     *
     * @param loginVo  登录时发送的信息
     * @param request  request
     * @param response response
     * @return RespBean
     */
    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
        String mobile = loginVo.getMobile();
        String password = loginVo.getPassword();
        // 判断手机号/id,和密码是否为空
        //if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
        //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        //}
        // 判断手机号是否合格
        //if (!ValidatorUtil.isMobile(mobile)) {
        //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        //}
        // 查询DB,判断用户是否存在
        User user = userMapper.selectById(mobile);
        if (null == user) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        // 如果用户存在,则对比密码!
        // 注意:我们从 LoginVo 取出的密码是中间密码(即客户端经过一次加密加盐处理的密码)
        if (!MD5Util.midPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        // 登录成功
        // 给每个用户生成 ticket 唯一
        String ticket = UUIDUtil.uuid();
        // 为实现分布式 Session ,把登录用户信息存放到 Redis 当中
        System.out.println("使用 redisTemplate->" + redisTemplate.hashCode());
        redisTemplate.opsForValue().set("user:" + ticket, user);
        // 将登录成功的用户保存到 session
        //request.getSession().setAttribute(ticket, user);
        // 将 ticket 保存到 cookie,cookieName 不可以随便写,必须时 "userTicket"
        CookieUtil.setCookie(request, response, "userTicket", ticket);
        return RespBean.success();
    }
}
 
测试:运行查看,我们在 Redis 保存的信息是否,符合我们的预期:
完成测试,启动项目,用户登录
浏览器输入 http://localhost:8080/login/toLogin

我们还还需要修改,登录成功,进入商品的处理,因为我们登录成功了,需要改为从 Redis 当中获取 Session 信息了。如果 Redis 当中没有该登录的用户的信息,那么就说明该用户没有登录过,需要登录,才能访问,我们的商品列表信息页面。

package com.rainbowsea.seckill.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * @author huo
 * @description 针对表【seckill_user】的数据库操作Service
 * @createDate 2025-04-24 15:38:01
 */
public interface UserService extends IService {
    /**
     * 根据 Cookie 当中的 userTicket 获取判断,存储到 Redis 当中的用户信息
     * @param userTicket  Cookie 当中的 userTicket
     * @param request
     * @param response
     * @return 存储到 Redis 当中的 User 对象信息
     */
    User getUserByCookieByRedis(String userTicket,
                                HttpServletRequest request,
                                HttpServletResponse response);
}
 
package com.rainbowsea.seckill.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.seckill.exception.GlobalException;
import com.rainbowsea.seckill.mapper.UserMapper;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.CookieUtil;
import com.rainbowsea.seckill.utill.MD5Util;
import com.rainbowsea.seckill.utill.UUIDUtil;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * @author huo
 * @description 针对表【seckill_user】的数据库操作Service实现
 * @createDate 2025-04-24 15:38:01
 */
@Service
public class UserServiceImpl extends ServiceImpl
        implements UserService {
    @Resource
    private UserMapper userMapper;
    @Resource
    private RedisTemplate redisTemplate;
    /**
     * 登录校验
     *
     * @param loginVo  登录时发送的信息
     * @param request  request
     * @param response response
     * @return RespBean
     */
    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
        String mobile = loginVo.getMobile();
        String password = loginVo.getPassword();
        // 判断手机号/id,和密码是否为空
        //if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
        //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        //}
        // 判断手机号是否合格
        //if (!ValidatorUtil.isMobile(mobile)) {
        //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        //}
        // 查询DB,判断用户是否存在
        User user = userMapper.selectById(mobile);
        if (null == user) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        // 如果用户存在,则对比密码!
        // 注意:我们从 LoginVo 取出的密码是中间密码(即客户端经过一次加密加盐处理的密码)
        if (!MD5Util.midPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        // 登录成功
        // 给每个用户生成 ticket 唯一
        String ticket = UUIDUtil.uuid();
        // 为实现分布式 Session ,把登录用户信息存放到 Redis 当中
        System.out.println("使用 redisTemplate->" + redisTemplate.hashCode());
        redisTemplate.opsForValue().set("user:" + ticket, user);
        // 将登录成功的用户保存到 session
        //request.getSession().setAttribute(ticket, user);
        // 将 ticket 保存到 cookie,cookieName 不可以随便写,必须时 "userTicket"
        CookieUtil.setCookie(request, response, "userTicket", ticket);
        return RespBean.success();
    }
    /**
     * 根据 Cookie 当中的 userTicket 获取判断,存储到 Redis 当中的用户信息
     * @param userTicket  Cookie 当中的 userTicket
     * @param request
     * @param response
     * @return 存储到 Redis 当中的 User 对象信息
     */
    @Override
    public User getUserByCookieByRedis(String userTicket, HttpServletRequest request, HttpServletResponse response) {
        if(!StringUtils.hasText(userTicket)) {
            return null;
        }
        // 根据 Cookie 当中的 userTicket 获取判断,存储到 Redis 当中的用户信息
        // 注意:这里我们在 Redis 存储的 Key是:"user:+userTicket"
        User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);
        // 如果用户不为 null,就重新设置 cookie,刷新,防止cookie超时了,
        if(user != null) {
            // cookieName 不可以随便写,必须是 "userTicket"
            CookieUtil.setCookie(request,response,"userTicket",userTicket);
        }
        return user;
    }
}
 
package com.rainbowsea.seckill.controller;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
 * 商品列表处理
 */
@Controller
@RequestMapping("/goods")
public class GoodsController {
    @Resource
    private UserService userService;
    // 跳转到商品列表页
    //@RequestMapping(value = "/toList")
    //public String toList(HttpSession session,
    //                     Model model,
    //                     @CookieValue("userTicket") String ticket,
    //                     ) {
    @RequestMapping(value = "/toList")
    public String toList(Model model,
                         @CookieValue("userTicket") String ticket,
                         HttpServletRequest request,
                         HttpServletResponse response
    ) {
        //  @CookieValue("userTicket") String ticket 注解可以直接获取到,对应 "userTicket" 名称
        // 的cookievalue 信息
        if (!StringUtils.hasText(ticket)) {
            return "login";
        }
        // 通过 cookieVale 当中的 ticket 获取 session 中存放的 user
        //User user = (User) session.getAttribute(ticket);
        // 改为从 Redis 当中获取
        User user = userService.getUserByCookieByRedis(ticket, request, response);
        if (null == user) { // 用户没有成功登录
            return "login";
        }
        // 将 user 放入到 model,携带该下一个模板使用
        model.addAttribute("user", user);
        return "goodsList";
    }
}
测试:运行查看,我们在 Redis 保存的信息是否,符合我们的预期:
完成测试,启动项目,用户登录
浏览器输入 http://localhost:8080/login/toLogin



“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
 登录查看全部
登录查看全部
                参与评论
手机查看
返回顶部