▶【SecKill】U2 实现登录功能


?【SecKill】U2 实现登录功能

一、数据库设计

1、新建数据库表miaosha_user`

CREATE TABLE `miaosha_user` (
  `id` bigint(20) NOT NULL COMMENT '用户ID、手机号码',
  `nickname` varchar(255) NOT NULL,
  `password` varchar(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt)+salt)',
  `salt` varchar(10) DEFAULT NULL,
  `head` varchar(128) DEFAULT NULL COMMENT '头像、云存储ID',
  `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;

【注】字符集采用的是utf8mb4(most bytes 4)。简单来说,utf8mb4是utf8的超集,能够用4个字节存储更多的字符。标准UTF-8字符集编码可以用1~4个字节取编码21位字符,但是在MySQL中,utf8最多使用3个字节,像一些表情emoji和不常用的字符如“墅”需要用4个字节才能表示出来。用utf8mb4能解决以上问题。数据库中存储了"动态"salt值

二、明文密码两次MD5处理:安全

1、为什么密码要用两次MD5?

第一次MD5,是针对传输安全做的MD5加密,因为http是明文传递,如果不进行加密的话,密码就直接被劫持了。

(Password1 = MD5(inputPassword,固定的salt值),salt为字符串)

第二次MD5,是针对数据库安全做的MD5加密,保证数据库的防盗安全。若不进行二次加密,MD5值经数据库获取,可直接被

MD5转换器直接转换为用户密码,不安全。

(Password2 = MD5(Password1,随机的salt值))

2、在pom.xml添加MD5依赖



    commons-codec
    commons-codec


    org.apache.commons
    commons-lang3
    3.6

3、新建com.kirin.miaosha.util包,作为MD5工具类

4、新建MD5Util工具类

(1)(测试)做一次MD5

package com.kirin.miaosha.util;

public class MD5Util {
    
    //做一次MD5
    public static String md5(String src) {
        return DigestUtils.md5Hex(src); //调用DigestUtils,实现md5处理
    }
    
    //做第一次MD5
    //静态的salt,用于第一次MD5,在输入的密码后进行拼接
    private static final String salt = "1a2b3c4d";
    
    //把用户输入的密码转换成Form表单
    public static String inputPassToFormPass(String inputPass) {
        //拼接字符时没有添加"",出现了登录验证失败的问题
        String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4); 
        System.out.println(str);
        return md5(str); //做一次MD5
    }
    
    //编写主类进行测试
    public static void main(String[] args) {
        System.out.println(inputPassToFormPass("123456"));
    }

}

(2)★(完整)做2次MD5

package com.kirin.miaosha.util;

public class MD5Util {
    
    //做一次MD5
    public static String md5(String src) {
        return DigestUtils.md5Hex(src); //调用DigestUtils,实现md5处理
    }
    
    //1.做第一次MD5
    //静态的salt,用于第一次MD5,在输入的密码后进行拼接
    private static final String salt = "1a2b3c4d";
    
    //1.做第一次MD5:把用户输入的明文密码转换成Form表单格式
    public static String inputPassToFormPass(String inputPass) {
        //拼接字符时没有添加"",出现了登录验证失败的问题
        String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4); 
        System.out.println(str);
        return md5(str); //做一次MD5
    }
    
    //2.做第二次MD5:把Form表单格式的密码转换成DB格式的密码
    public static String formPassToDBPass(String formPass, String salt) { //动态的salt,取随机值
        String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4);
        return md5(str); //做一次MD5
    }
    
    //3.合并第一和第二次转换(MD5):把用户输入的明文密码转换成DB格式的密码
    public static String inputPassToDbPass(String inputPass, String saltDB) {
        String formPass = inputPassToFormPass(inputPass);
        String dbPass = formPassToDBPass(formPass, saltDB);
        return dbPass;
    }
    
    //编写主类进行测试
    public static void main(String[] args) {
        //1.做第一次MD5
        //System.out.println(inputPassToFormPass("123456")); //输出:d3b1294a61a07da9b49b6e22b2cbd7f9 ——> 破解后12123456c3
        //2.做第二次MD5
        //System.out.println(formPassToDBPass(inputPassToFormPass("123456"), "1a2b3c4d")); //设定动态salt的值为1a2b3c4d
        System.out.println(inputPassToDbPass("123456", "1a2b3c4d")); //b7797cce01b4b131b433b6acf4add449
    }

}

(3)做2次MD5后,得到加密密码b7797cce01b4b131b433b6acf4add449,将其输入数据库表中作为一条记录

5、新建LoginController类,直接复制DemoController.java

6、新建登录页面

(1)在 src/main/resources/templates 中新建login.html

(2)导入静态CSS模板

(3)login.html:





    登录
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    



    

用户登录

7、新建com.kirin.miaosha.vo包:接收参数

新建LoginVo.java:接收参数

LoginController.java:

LoginController.java:参数校验

CodeMsg.java:设置登录模块的报错弹窗

//登录模块 5002XX
public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效");
public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空");
public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空");
public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手机号格式错误");
public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手机号不存在");
public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密码错误");

在com.kirin.miaosha.util包中,新建ValidatorUtil.java,用于判断用户登录时手机号的格式

package com.kirin.miaosha.util;

//用于判断用户登录时输入的格式
public class ValidatorUtil {
    
    private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}"); //以1开头,后跟10个数字
    
    public static boolean isMobile(String src) {
        if(StringUtils.isEmpty(src)) {
            return false;
        }
        Matcher m = mobile_pattern.matcher(src);
        return m.matches();
    }
}

8、在com.kirin.miaosha.domain包中,新建MiaoshaUser.java,对应miaosha_user数据库表

package com.kirin.miaosha.domain;

import java.util.Date;

public class MiaoshaUser {
    private Long id;
    private String nickname;
    private String password;
    private String salt;
    private String head;
    private Date registerDate;
    private Date lastLoginDate;
    private Integer loginCount;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getSalt() {
        return salt;
    }
    public void setSalt(String salt) {
        this.salt = salt;
    }
    public String getHead() {
        return head;
    }
    public void setHead(String head) {
        this.head = head;
    }
    public Date getRegisterDate() {
        return registerDate;
    }
    public void setRegisterDate(Date registerDate) {
        this.registerDate = registerDate;
    }
    public Date getLastLoginDate() {
        return lastLoginDate;
    }
    public void setLastLoginDate(Date lastLoginDate) {
        this.lastLoginDate = lastLoginDate;
    }
    public Integer getLoginCount() {
        return loginCount;
    }
    public void setLoginCount(Integer loginCount) {
        this.loginCount = loginCount;
    }
}

9、在com.kirin.miaosha.dao包中,新建MiaoshaUserDao.java,对应MiaoshaUser.java的接口方法

package com.kirin.miaosha.dao;

@Mapper
public interface MiaoshaUserDao {
    
    @Select("select * from miaosha_user where id = #{id}")
    public MiaoshaUser getById(@Param("id")long id);
}

10、在com.kirin.miaosha.service包中,新建MiaoshaUserService.java,对应MiaoshaUser.java的接口方法

8、完整代码:

com.kirin.miaosha.util / MD5Util.java:

package com.kirin.miaosha.util;

public class MD5Util {
    
    //做一次MD5
    public static String md5(String src) {
        return DigestUtils.md5Hex(src); //调用DigestUtils,实现md5处理
    }
    
    //1.做第一次MD5
    //静态的salt,用于第一次MD5,在输入的密码后进行拼接
    private static final String salt = "1a2b3c4d";
    
    //1.做第一次MD5:把用户输入的明文密码转换成Form表单格式
    public static String inputPassToFormPass(String inputPass) {
        //拼接字符时没有添加"",出现了登录验证失败的问题
        String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4); 
        System.out.println(str);
        return md5(str); //做一次MD5
    }
    
    //2.做第二次MD5:把Form表单格式的密码转换成DB格式的密码
    public static String formPassToDBPass(String formPass, String salt) { //动态的salt,取随机值
        String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4);
        return md5(str); //做一次MD5
    }
    
    //3.合并第一和第二次转换(MD5):把用户输入的明文密码转换成DB格式的密码
    public static String inputPassToDbPass(String inputPass, String saltDB) {
        String formPass = inputPassToFormPass(inputPass);
        String dbPass = formPassToDBPass(formPass, saltDB);
        return dbPass;
    }
    
    //编写主类进行测试
    public static void main(String[] args) {
        //1.做第一次MD5
        //System.out.println(inputPassToFormPass("123456")); //输出:d3b1294a61a07da9b49b6e22b2cbd7f9 ——> 破解后12123456c3
        //2.做第二次MD5
        //System.out.println(formPassToDBPass(inputPassToFormPass("123456"), "1a2b3c4d")); //设定动态salt的值为1a2b3c4d
        System.out.println(inputPassToDbPass("123456", "1a2b3c4d")); //b7797cce01b4b131b433b6acf4add449
    }

}

com.kirin.miaosha.util / ValidatorUtil.java:

package com.kirin.miaosha.util;

//用于判断用户登录时输入的格式
public class ValidatorUtil {
    
    private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}"); //以1开头,后跟10个数字
    
    public static boolean isMobile(String src) {
        if(StringUtils.isEmpty(src)) {
            return false;
        }
        Matcher m = mobile_pattern.matcher(src);
        return m.matches();
    }
}

com.kirin.miaosha.vo / LoginVo.java:

package com.imooc.miaosha.vo;

public class LoginVo {
    
    private String mobile;private String password;
    
    public String getMobile() {
        return mobile;
    }
    public void setMobile(String mobile) {
        this.mobile = mobile;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    @Override
    public String toString() {
        return "LoginVo [mobile=" + mobile + ", password=" + password + "]";
    }
}

com.kirin.miaosha.domain / MiaoshaUser.java:

package com.kirin.miaosha.domain;

public class MiaoshaUser {
    private Long id;
    private String nickname;
    private String password;
    private String salt;
    private String head;
    private Date registerDate;
    private Date lastLoginDate;
    private Integer loginCount;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getSalt() {
        return salt;
    }
    public void setSalt(String salt) {
        this.salt = salt;
    }
    public String getHead() {
        return head;
    }
    public void setHead(String head) {
        this.head = head;
    }
    public Date getRegisterDate() {
        return registerDate;
    }
    public void setRegisterDate(Date registerDate) {
        this.registerDate = registerDate;
    }
    public Date getLastLoginDate() {
        return lastLoginDate;
    }
    public void setLastLoginDate(Date lastLoginDate) {
        this.lastLoginDate = lastLoginDate;
    }
    public Integer getLoginCount() {
        return loginCount;
    }
    public void setLoginCount(Integer loginCount) {
        this.loginCount = loginCount;
    }
}

com.kirin.miaosha.service / MiaoshaUserService.java:

package com.kirin.miaosha.service;

@Service
public class MiaoshaUserService {

    @Autowired
    MiaoshaUserDao miaoshaUserDao;

    public MiaoshaUser getById(long id) {
        return miaoshaUserDao.getById(id);
    }
    
    public CodeMsg login(LoginVo loginVo) {
        if(loginVo == null) {
            return CodeMsg.SERVER_ERROR;
        }
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();
        //判断手机号是否存在
        MiaoshaUser user = getById(Long.parseLong(mobile));
        if(user == null) {
            return CodeMsg.MOBILE_NOT_EXIST;
        }
        //验证密码
        String dbPass = user.getPassword(); //获取数据库中的密码
        String saltDB = user.getSalt(); //获取数据库中的静态salt
        String calcPass = MD5Util.formPassToDBPass(formPass, saltDB); //做2次MD5
        if(!calcPass.equals(dbPass)) { //判断计算出来的MD5加密密码和数据库中的密码是否一致
            return CodeMsg.PASSWORD_ERROR;
        }
        return CodeMsg.SUCCESS;
    }
}

com.kirin.miaosha.controller / LoginController.java:

package com.kirin.miaosha.controller;

@Controller
@RequestMapping("/login")
public class LoginController {

    private static Logger log = LoggerFactory.getLogger(LoginController.class);
    
    @Autowired
    MiaoshaUserService userService;
    
    @Autowired
    RedisService redisService;
    
    @RequestMapping("/to_login")
    public String toLogin() {
        return "login"; //返回页面
    }
    
    @RequestMapping("/do_login")
    @ResponseBody
    public Result doLogin(LoginVo loginVo) {
        log.info(loginVo.toString()); //在LoginVo.java中生成toString()
        //参数校验
        String passInput = loginVo.getPassword();
        String mobile = loginVo.getMobile();
        if(StringUtils.isEmpty(passInput)) {
            return Result.error(CodeMsg.PASSWORD_EMPTY);
        }
        if(StringUtils.isEmpty(mobile)) {
            return Result.error(CodeMsg.MOBILE_EMPTY);
        }
        if(!ValidatorUtil.isMobile(mobile)) { //验证mobile的输入的格式,是否是以1开头,后跟10个数字
            return Result.error(CodeMsg.MOBILE_ERROR);
        }
        //登录
        CodeMsg cm = userService.login(loginVo);
        if(cm.getCode() == 0) {
            return Result.success(true);
        }else {
            return Result.error(cm);
        }
    }
}

三、JSR303参数检验+全局异常处理器

1、添加JSR参数校验依赖



    org.springframework.boot
    spring-boot-starter-validation

2、

① com.kirin.miaosha.controller / LoginController.java:

② 新建com.kirin.miaosha.validator包

新建IsMobile类,作为自定义验证器

package com.kirin.miaosha.validator;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) //能够标注的范围
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class }) //这个注解帮助我们处理逻辑,其中IsMobileValidator.class是真正处理逻辑的类
public @interface IsMobile { //根据已有注解@NotNull,仿写而来
    
    boolean required() default true;
    
    String message() default "手机号码格式错误"; //添加错误信息

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

新建IsMobileValidator类

先看类的声明部分,public class IsMobileValidator implements ConstraintValidator,它有两个泛型,第一个是自定义的注解类,第二个是要验证的参数类型,另外实现该接口的逻辑类,被spring管理成bean,可以在需要的地方进行装配

其中有一个initialize,初始化方法,它调用的是我们自定义注解中写的required()方法,默认需要有值

另一个方法isValid,则对逻辑进行验证,true验证通过,false验证失败

package com.kirin.miaosha.validator;

public class IsMobileValidator implements ConstraintValidator {

    private boolean required = false;
    
    public void initialize(IsMobile constraintAnnotation) {
        required = constraintAnnotation.required();
    }

    public boolean isValid(String value, ConstraintValidatorContext context) {
        if(required) { //在必须有值的情况下
            return ValidatorUtil.isMobile(value);
        }else {
            if(StringUtils.isEmpty(value)) { //在不要求有值的情况下
                return true; //空值是允许的
            }else { //有值就给它判断
                return ValidatorUtil.isMobile(value);
            }
        }
    }
}

3、全局异常处理器

(1)新建com.kirin.miaosha.exception包

(2)新建GlobalExceptionHandler类

在CodeMsg.java中,添加一条异常定义

package com.kirin.miaosha.result;

public class CodeMsg {
    private int code;
    private String msg;
    
    //定义通用异常
    public static CodeMsg SUCCESS = new CodeMsg(0, "success");
    public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常");
    public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s"); //带参数
    
    //登录模块 5002XX
      public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效");
      public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空");
      public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空");
      public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手机号格式错误");
      public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手机号不存在");
      public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密码错误");

    //商品模块5003XX

    //订单模块5004XX

    //秒杀模块5005XX

      private CodeMsg( ) {
    }
            
    private CodeMsg( int code,String msg ) {
        this.code = code;
        this.msg = msg;
    }
    
    public int getCode() {
        return code;
    }
    public void setCode(int code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    
    //定义带参数的CodeMsg
    public CodeMsg fillArgs(Object... args) {
        int code = this.code;
        String message = String.format(this.msg, args); //把原始的message拼接上参数
        return new CodeMsg(code, message);
    }

    @Override
    public String toString() {
        return "CodeMsg [code=" + code + ", msg=" + msg + "]";
    }
}

其中String.format()能够根据传入的字符串格式,比如"参数校验异常:%s",其中%s,能被第二个传入的参数进行替换,从而形成动态的字符串

GlobalExceptionHandler.java:

package com.kirin.miaosha.exception;

//全局异常处理器:处理登录异常没有显示在页面上的问题
@ControllerAdvice //它是增强的Controller,能够实现全局异常处理和全局数据绑定
@ResponseBody
public class GlobalExceptionHandler {
    //配合@ExceptionHandler(value = Exception.class),它能够实现对所有异常的接受,而在方法中,对不同的异常进行处理
    @ExceptionHandler(value=Exception.class) //拦截所有的异常
    public Result exceptionHandler(HttpServletRequest request, Exception e){
        if(e instanceof BindException) {
            BindException ex = (BindException)e; //获取错误列表,拿取其中的第一个
            List errors = ex.getAllErrors();
            ObjectError error = errors.get(0);
            String msg = error.getDefaultMessage();
            return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
        }else {
            return Result.error(CodeMsg.SERVER_ERROR);
        }
    }
}

运行:出现提示框,则成功!

(3)自动新建GlobalException类:全局异常

修改com.kirin.miaosha.service / MiaoshaUserService.java代码:

修改com.kirin.miaosha.exception / GlobalExceptionHandler.java代码:

修改com.kirin.miaosha.controller / LoginController.java代码:

package com.kirin.miaosha.controller;

@Controller
@RequestMapping("/login")
public class LoginController {

    private static Logger log = LoggerFactory.getLogger(LoginController.class);
    
    @Autowired
    MiaoshaUserService userService;
    
    @Autowired
    RedisService redisService;
    
    @RequestMapping("/to_login")
    public String toLogin() {
        return "login"; //返回页面
    }
    
    @RequestMapping("/do_login")
    @ResponseBody
//    public Result doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {
    public Result doLogin(@Valid LoginVo loginVo) { //2.JSR303参数检验
        log.info(loginVo.toString()); //在LoginVo.java中生成toString()
        //1.自己写的参数校验
//        String passInput = loginVo.getPassword();
//        String mobile = loginVo.getMobile();
//        if(StringUtils.isEmpty(passInput)) {
//            return Result.error(CodeMsg.PASSWORD_EMPTY);
//        }
//        if(StringUtils.isEmpty(mobile)) {
//            return Result.error(CodeMsg.MOBILE_EMPTY);
//        }
//        if(!ValidatorUtil.isMobile(mobile)) { //验证mobile的输入的格式,是否是以1开头,后跟10个数字
//            return Result.error(CodeMsg.MOBILE_ERROR);
//        }
        
        //登录
//        userService.login(response,loginVo);
        userService.login(loginVo);
        return Result.success(true);
//        if(cm.getCode() == 0) {
//            return Result.success(true);
//        }else {
//            return Result.error(cm);
//        }
    }
}

四、分布式Session

1、★原理图

【问题】

服务器中的原生session是无法满足需求的,因为用户的请求有可能随机落入到不同的服务器中,这样的结果将会导致用户的session丢失,传统做法中有解决方案,是进行session同步,将一个服务器上的session进行同步到另一个服务器上,在一个集群中无论你访问哪个服务器都可以共享,但是这种方法有个明显缺陷,就是性能问题,传输有时延问题,其次这样每台服务器的session重复拥有,这样其内存必然受到影响,如果只有几台服务器还好,如果是十台,二十台服务器呢?这种恐怖的场景会是什么样的体验呢,我就无法得知了。

【原理】

用户每次登录的时候,生成一个类似sessionId的东西【=token标识用户,这是全局的唯一标识,如UUID,作用类似于(sessionId)】,将其写到cookie中,传递给客户端,客户端对数据库访问过程中不断上传这个token,而服务端拿到这个token就可以获取用户的session信息。

这个道理其实在很多地方是相通的,比如我们容器中实现原生session,也是将生成的id写入cookie当中。

2、Cookie

(1)com.kirin.miaosha.service / MiaoshaUserService.java:登录成功后,生成cookie

封装addCookie():

private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
  redisService.set(MiaoshaUserKey.token, token, user);
  Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
  cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
  cookie.setPath("/");
  response.addCookie(cookie);
}

(2)在com.kirin.miaosha.util包中,新建UUIDUtil.java,UUID的工具类

package com.kirin.miaosha.util;

import java.util.UUID;

public class UUIDUtil {
    public static String uuid() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}

(3)新建商品列表页goods_list.html

(4)在com.kirin.miaosha.controller包中,新建GoodsController.java,使用户登录后跳转到商品列表页goods_list.html

package com.kirin.miaosha.controller;

@Controller
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    MiaoshaUserService userService;
    
    @Autowired
    RedisService redisService;
    
    @RequestMapping("/to_list")
    public String toList(HttpServletResponse response,Model model,
        @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String cookieToken, //根据参数value在Cookie中获取值
        @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String paramToken){ //让我们在Request中能获取参数,解决的主要是,移动手机端不使用Cookie存值的问题
        //参数校验
        if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
            return "login";
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        MiaoshaUser user = userService.getByToken(response,token);
        model.addAttribute("user",user);

        return "goods_list";
    }
}

(5)在(4)中,每次获取参数都要写这么一大段,简化方法:

  @RequestMapping("/to_list") //跳转到商品列表页goods_list.html
    public String toList(HttpServletResponse response,Model model,MiaoshaUser user){
        model.addAttribute("user",user);
        return "goods_list";
    }

【原理】WebMvcConfigurerAdapter

采用的是继承WebMvcConfigurerAdapter,重写其中addArgumentResolvers()方法,该方法实现的是参数解析的功能

【解决方法】

新建com.kirin.miaosha.config包,新建WebConfig.java:

package com.kirin.miaosha.config;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
    
    @Autowired
    UserArgumentResolver userArgumentResolver;
    
    @Override
    public void addArgumentResolvers(List argumentResolvers) {
        argumentResolvers.add(userArgumentResolver);
    }
}

新建UserArgumentResolver.java:

package com.kirin.miaosha.config;

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
//实现HandlerMethodArgumentResolver接口,必须重写其中的两个方法,supportsParameter()和resolveArgument()

    @Autowired
    MiaoshaUserService userService;
    
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> clazz = parameter.getParameterType();
        return clazz==MiaoshaUser.class;
    }

    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        
        String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
        String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
        if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return null;
        }
        String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
        return userService.getByToken(response, token);
    }

    private String getCookieValue(HttpServletRequest request, String cookiName) {
        Cookie[]  cookies = request.getCookies();
        for(Cookie cookie : cookies) {
            if(cookie.getName().equals(cookiName)) {
                return cookie.getValue();
            }
        }
        return null;
    }
}

实现HandlerMethodArgumentResolver接口,必须重写其中的两个方法,supportsParameter()和resolveArgument()

supportsParameter():对要进行解析的参数类型进行判断,符合才执行后者

resolveArgument():实现对参数的处理逻辑(2种情况):1.从request中获取token值   2.从cookie中拿取token值,根据token值来获取到对应的user

省去了@CookieValue和@RequestParam注解的冗余,而且我们对user的获取也方便多了

完整代码:

com.kirin.miaosha.service / MiaoshaUserService.java:

package com.kirin.miaosha.service;

@Service
public class MiaoshaUserService {
    
    public static final String COOKI_NAME_TOKEN = "token";
    
    @Autowired
    MiaoshaUserDao miaoshaUserDao;
    
    @Autowired
    RedisService redisService; //为了用token标识用户,要将用户信息写入Redis缓存中
    
    public MiaoshaUser getById(long id) {
        return miaoshaUserDao.getById(id);
    }

    public boolean login(HttpServletResponse response, LoginVo loginVo) {
        if(loginVo == null) {
            throw new GlobalException(CodeMsg.SERVER_ERROR);
        }
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();
        //判断手机号是否存在
        MiaoshaUser user = getById(Long.parseLong(mobile));
        if(user == null) {
            throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
        }
        //验证密码
        String dbPass = user.getPassword(); //获取数据库中的密码
        String saltDB = user.getSalt(); //获取数据库中的静态salt
        String calcPass = MD5Util.formPassToDBPass(formPass, saltDB); //做2次MD5
        if(!calcPass.equals(dbPass)) { //判断计算出来的MD5加密密码和数据库中的密码是否一致
            throw new GlobalException(CodeMsg.PASSWORD_ERROR);
        }
        //登录成功后,生成cookie
        String token = UUIDUtil.uuid();
        addCookie(response, token, user);
        return true;
    }

    private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
        redisService.set(MiaoshaUserKey.token, token, user);
        Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
        cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
        cookie.setPath("/");
        response.addCookie(cookie);
    }

    public MiaoshaUser getByToken(HttpServletResponse response, String token) {
        //参数校验
        if(StringUtils.isEmpty(token)) {
            return null;
        }
        MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
        //延长有效期
        if(user != null) {
            addCookie(response, token, user);
        }
        return user;
    }
}

com.kirin.miaosha.controller / GoodsController.java:

package com.kirin.miaosha.controller;

@Controller
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    MiaoshaUserService userService;
    
    @Autowired
    RedisService redisService;
    
    @RequestMapping("/to_list") //跳转到商品列表页goods_list.html
    public String toList(HttpServletResponse response,Model model,MiaoshaUser user){
        model.addAttribute("user",user);
        return "goods_list";
    }
}

相关