文章只展示部分核心代码,完整代码请参考源码
一、数据库设计
创建秒杀用户表
CREATE TABLE `miaosha_user` (`id` bigint(20) NOT NULL COMMENT '用户id',`nickname` varchar(255) NOT NULL COMMENT '用户昵称',`password` varchar(32) DEFAULT NULL COMMENT '用户密码',`salt` varchar(10) DEFAULT NULL,`head` varchar(128) DEFAULT NULL COMMENT '用户头像',`register_date` datetime DEFAULT NULL COMMENT '注册时间',`last_login_date` datetime DEFAULT NULL COMMENT '上次登陆时间',`login_count` int(11) unsigned zerofill DEFAULT NULL COMMENT '登陆次数',PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
二、明文密码两次MD5处理
用户端: 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"; }
