文章只展示部分核心代码,完整代码请参考源码


一、数据库设计

  • 创建秒杀用户表

    1. CREATE TABLE `miaosha_user` (
    2. `id` bigint(20) NOT NULL COMMENT '用户id',
    3. `nickname` varchar(255) NOT NULL COMMENT '用户昵称',
    4. `password` varchar(32) DEFAULT NULL COMMENT '用户密码',
    5. `salt` varchar(10) DEFAULT NULL,
    6. `head` varchar(128) DEFAULT NULL COMMENT '用户头像',
    7. `register_date` datetime DEFAULT NULL COMMENT '注册时间',
    8. `last_login_date` datetime DEFAULT NULL COMMENT '上次登陆时间',
    9. `login_count` int(11) unsigned zerofill DEFAULT NULL COMMENT '登陆次数',
    10. PRIMARY KEY (`id`)
    11. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    二、明文密码两次MD5处理

  • 用户端: pass = md5(明文+固定salt)

  • 服务端: pass = md5(用户输入+随机salt)

    1. 添加依赖

    <dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    </dependency>
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.6</version>
    </dependency>
    

    2.编写工具类 MD5Util

    public class MD5Util {
    
      private static final String salt = "1a2b3c4d";
    
      public static String md5(String src) {
          return DigestUtils.md5Hex(src);
      }
      //第一次md5加密,使用固定salt
      public static String inputPassToFormPass(String inputPass) {
          String str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
          return md5(str);
      }
      //第二次md5加密,使用随机salt
      public static String formPassToDBPass(String formPass ,String saltDB){
          String str = "" + saltDB.charAt(0) + saltDB.charAt(2) + formPass + saltDB.charAt(5) + saltDB.charAt(4);
          return md5(str);
      }
      //两次加密
      public static String inputPassToDBPass(String formPass ,String saltDB){
          return formPassToDBPass(inputPassToFormPass(formPass),saltDB);
      }
    
      //test
      public static void main(String[] args) {
          System.out.println(inputPassToFormPass("123456"));
          System.out.println(inputPassToDBPass("123456","1a2b3c4d"));
      }
    }
    

    3.前端页面使用 ajax 异步发送登陆请求,并完成第一次加密

      function login(){
          $("#loginForm").validate({
              submitHandler:function(form){
                  doLogin();
              }
          });
      }
      function doLogin(){
          g_showLoading();
    
          var inputPass = $("#password").val();
          var salt = g_passsword_salt;
          var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
          var password = md5(str);
    
          $.ajax({
              url: "/login/do_login",
              type: "POST",
              data:{
                  mobile:$("#mobile").val(),
                  password: password
              },
              success:function(data){
                  layer.closeAll();
                  if(data.code == 0){
                      layer.msg("成功");
                      window.location.href="/goods/to_list";
                  }else{
                      layer.msg(data.msg);
                  }
              },
              error:function(){
                  layer.closeAll();
              }
          });
      }
    

    4.登陆 login 业务逻辑校验

      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();
          String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
          if (!calcPass.equals(dbPass)){
              throw new GlobalException(CodeMsg.PASSWORD_ERROR);
          }
          //生成cookie
          String token = UUIDUtil.uuid();
          addCookie(response,token,user);
          return true;
      }
    

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

    1.引入依赖

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    

    2.用户 id 参数校验

  • 自定义校验器

    public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
      //不需要默认值
      private boolean required = false;
      @Override
      public void initialize(IsMobile constraintAnnotation) {
          required = constraintAnnotation.required();
      }
      //校验逻辑
      @Override
      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);
              }
          }
      }
    }
    
  • 自定义注解并添加校验器

    @Documented
    @Constraint(validatedBy = {IsMobileValidator.class})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    public @interface IsMobile {
    
      boolean required() default true;
    
      String message() default "手机号码格式错误";
    
      Class<?>[] groups() default { };
    
      Class<? extends Payload>[] payload() default { };
    }
    
  • 为属性添加注解

    @NotNull
    @IsMobile
    private Long id;
    private String nickname;
    @NotNull
    @Length(min = 32)
    private String password;
    

    3. 全局异常类

    public class GlobalException extends RuntimeException {
    
      private static final long serialVersionUID = 1L;
    
      private CodeMsg codeMsg;
    
      public GlobalException(CodeMsg codeMsg){
          super(codeMsg.toString());
          this.codeMsg = codeMsg;
      }
    
      public CodeMsg getCodeMsg(){
          return codeMsg;
      }
    }
    

    4. 全局异常处理类

    @ControllerAdvice
    @ResponseBody
    public class GlobleExceptionHandler {
    
      @ExceptionHandler(value = Exception.class)
      public Result<String> exceptionHandler(HttpServletRequest request,Exception e){
          e.printStackTrace();
          if (e instanceof GlobalException){
              GlobalException gx = (GlobalException) e;
              return Result.error(gx.getCodeMsg());
          }else if (e instanceof BindException){
              BindException ex = (BindException) e;
              List<ObjectError> 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);
          }
      }
    }
    

    四、分布式Session

  • 场景:由于有数个不同的服务器,客户不同的请求由于负载均衡,交给了另一台服务器,那么用户的登陆会失效,这样会影响客户的体验感。

  • 解决方案:通过第三方redis缓存,缓存用户的session信息。实现方式:用户第一次请求时,服务端生成用户 session对应的cookie并将用户的cookie请求存入redis缓存中,再一次请求时,如果跳转到了另一台服务器,那么服务器通过redis获取cookie信息,从而达到目的。

    1. 生成 cookie

  • 生成随机id

    public static String uuid(){
      return UUID.randomUUID().toString().replace("-","");
    }
    
  • 将通过验证的用户 user 和 token 写入到 response 的 cookie 中

    private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
      //将 token 和与之对应的 user 写入到 redis
      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. 存入 redis 缓存

    public <T> boolean set(KeyPrefix prefix, String key, T value) {
      Jedis jedis = null;
      try {
          jedis = jedisPool.getResource();
          String str = BeanToString(value);
          if (str == null || str.length() <= 0) {
              return false;
          }
          //生成真正的key
          String realkey = prefix.getPrefix() + key;
          int seconds = prefix.expireSeconds();
          if (seconds <= 0){
              jedis.set(realkey, str);
          }else {
              jedis.setex(realkey,seconds,str);
          }
          return true;
      } finally {
          returnToPool(jedis);
      }
    }
    

    3. 页面取到 user 对象

  • 自定义参数解析器,登陆成功后,根据 token 从 redis 缓存直接取到 user 对象,返回给前端

    //获取请求中的token
    String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
    //获取客户端cookie中的token
    String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
    if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
      return null;
    }
    //优先请求中的token
    String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
    return userService.getByToken(response, token);
    
    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;
    }
    
  • 为前端配置参数解析器

    @Configuration
    public class WebConfig extends WebMvcConfigurerAdapter {
    
      @Autowired
      UserArgumentResolver userArgumentResolver;
    
      @Override
      public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
          argumentResolvers.add(userArgumentResolver);
      }
    }
    
  • 此时前端控制器只需将 user 对象添加到 model

    @RequestMapping("/to_list")
    public String list(Model model , MiaoshaUser user){
      model.addAttribute("user",user);
      return "goods_list";
    }