- 基于SpringBoot、Redis和RabbitMQ的秒杀系统
- 登录功能总结
- 一、两次MD5加密
- 二、参数校验
- 三、记录用户信息
- 四、分布式Session
- 4.1、Spring Session配合Redis实现分布式Session
- 4.2、直接将用户信息存入Redis
- 五、优化登录功能
登录功能总结 一、两次MD5加密秒杀系统解决的主要问题:并发读、并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。
- 前端:Password=MD5(明文+固定Salt)
- 后端:Password=MD5(用户输入+随机Salt)
用户端MD5加密是为了防止用户密码在网络中明文传输,服务端MD5加密是为了提高密码安全性,双重保险。
-
引入pom.xml
commons-codec commons-codec -
编写MD5工具类
public class MD5Util { public static String md5(String src) { return DigestUtils.md5Hex(src); } private static final String salt = "1a2b3c4d"; public static String inputPassToFormPass(String inputPass) { String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4); return md5(str); } public static String formPassToDBPass(String formPass, String salt) { String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4); return md5(str); } 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) { System.out.println(inputPassToFormPass("123456"));//d3b1294a61a07da9b49b6e22b2cbd7f9 System.out.println(formPassToDBPass(inputPassToFormPass("123456"), "1a2b3c4d")); System.out.println(inputPassToDbPass("123456", "1a2b3c4d")); } }
每个类都写大量的健壮性判断过于麻烦,我们可以使用 validation 简化我们的代码
-
pom.xml
org.springframework.boot spring-boot-starter-validation -
自定义手机号码验证规则
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 context) { if (required){ return ValidatorUtil.isMobile(value); }else { if (StringUtils.isEmpty(value)){ return true; }else { return ValidatorUtil.isMobile(value); } } } } -
自定义注解IsMobile
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @documented @Constraint(validatedBy = {IsMobilevalidator.class}) public @interface IsMobile { boolean required() default true; String message() default "手机号码格式错误"; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; } -
校验手机号工具类
public class ValidatorUtil { private static final Pattern mobile_pattern = Pattern.compile("[1]([3-9])[0-9]{9}$"); public static boolean isMobile(String mobile){ if (StringUtils.isEmpty(mobile)) { return false; } Matcher matcher = mobile_pattern.matcher(mobile); return matcher.matches(); } } -
使用方法:直接加在需要校验的字段上
@Data @NoArgsConstructor @AllArgsConstructor public class LoginVo { @NotNull @IsMobile private String mobile; @NotNull @Length(min = 32) private String password; } -
登录方法的入参添加@Valid
@RequestMapping("/doLogin") @ResponseBody public RespBean doLogin(@Valid LoginVo loginVo) { log.info(loginVo.toString()); return userService.login(loginVo); }
最常用的就是使用cookie+session记录用户信息
-
cookie工具类
public class cookieUtil { public static String getcookievalue(HttpServletRequest request, String cookieName) { return getcookievalue(request, cookieName, false); } 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 < cookieList.length; i++) { if (cookieList[i].getName().equals(cookieName)) { if (isDecoder) { retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8"); } else { retValue = cookieList[i].getValue(); } break; } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return retValue; } public static String getcookievalue(HttpServletRequest request, String cookieName, String encodeString) { cookie[] cookieList = request.getcookies(); if (cookieList == null || cookieName == null) { return null; } String retValue = null; try { for (int i = 0; i < cookieList.length; i++) { if (cookieList[i].getName().equals(cookieName)) { retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString); break; } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return retValue; } public static void setcookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookievalue) { setcookie(request, response, cookieName, cookievalue, -1); } public static void setcookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookievalue, int cookieMaxage) { setcookie(request, response, cookieName, cookievalue, cookieMaxage, false); } public static void setcookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookievalue, boolean isEncode) { setcookie(request, response, cookieName, cookievalue, -1, isEncode); } public static void setcookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookievalue, int cookieMaxage, boolean isEncode) { doSetcookie(request, response, cookieName, cookievalue, cookieMaxage, isEncode); } public static void setcookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookievalue, int cookieMaxage, String encodeString) { doSetcookie(request, response, cookieName, cookievalue, cookieMaxage, encodeString); } public static void deletecookie(HttpServletRequest request, HttpServletResponse response, String cookieName) { doSetcookie(request, response, cookieName, "", -1, false); } private static final void doSetcookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookievalue, int cookieMaxage, boolean isEncode) { try { if (cookievalue == null) { cookievalue = ""; } else if (isEncode) { cookievalue = URLEncoder.encode(cookievalue, "utf-8"); } 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(); } } 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(); } } private static final String getDomainName(HttpServletRequest request) { String domainName = null; // 通过request对象获取访问的url地址 String serverName = request.getRequestURL().toString(); if (serverName == null || serverName.equals("")) { 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.xxx.com.cn domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1]; } else if (len <= 3 && len > 1) { // xxx.com or xxx.cn domainName = domains[len - 2] + "." + domains[len - 1]; } else { domainName = serverName; } } if (domainName != null && domainName.indexOf(":") > 0) { String[] ary = domainName.split("\:"); domainName = ary[0]; } return domainName; } } -
UUID工具类
public class UUIDUtil { public static String uuid() { return UUID.randomUUID().toString().replace("-", ""); } } -
登录业务中加入以下代码
//生成cookie String ticket = UUIDUtil.uuid(); request.getSession().setAttribute(ticket,user); cookieUtil.setcookie(request,response,"userTicket",ticket);
4.1、Spring Session配合Redis实现分布式Session之前的代码如果所有操作都在一台Tomcat上,没有什么问题。当我们部署多台系统,配合Nginx的时候会出现用户登录的问题,由于 Nginx 使用默认负载均衡策略(轮询),请求将会按照时间顺序逐一分发到后端应用上。也就是说刚开始我们在 Tomcat1 登录之后,用户信息放在 Tomcat1 的 Session 里。过了一会,请求又被 Nginx 分发到了 Tomcat2 上,这时 Tomcat2 上 Session 里还没有用户信息,于是又要登录。
-
pom.xml
org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.session spring-session-data-redis -
配置redis,修改application.yml
spring: redis: #超时时间 timeout: 10000ms #服务器地址 host: xxx.xxx.xxx.xxx #服务器端口 port: 6379 #数据库 database: 0 #密码 password: root lettuce: pool: #最大连接数,默认8 max-active: 1024 #最大连接阻塞等待时间,默认-1 max-wait: 10000ms #最大空闲连接 max-idle: 200 #最小空闲连接 min-idle: 5 -
完成上述操作后进行用户登录会在redis中产生用户信息相关的数据
-
Redis配置类RedisConfig.java
@Configuration public class RedisConfig { @Bean public RedisTemplateredisTemplate(RedisConnectionFactory connectionFactory){ RedisTemplate redisTemplate = new RedisTemplate<>(); //key序列器 redisTemplate.setKeySerializer(new StringRedisSerializer()); //value序列器 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); //Hash类型 key序列器 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); //Hash类型 value序列器 redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } } -
修改之前添加cookie的代码
//生成cookie String ticket = UUIDUtil.uuid(); redisTemplate.opsForValue().set("user:" + ticket, user); // request.getSession().setAttribute(ticket,user); cookieUtil.setcookie(request,response,"userTicket",ticket);
五、优化登录功能此时有一个弊端:用户登录后直接将用户信息存入Redis中了,没有设置过期时间,cookie过期后,重新登录又会产生新的cookie并加到Redis中。
想象一下用户登录后,随意跳转到其它页面是不是都要传入cookie判断用户是否存在,这样重复琐碎的操作可以使用MVC配置类在传入参数前就进行校验。若没有做此层的优化,后续在秒杀功能时,创建的订单将无用户id,原因就是取不到前面传入的用户信息。
-
UserArgumentResolver.java
@Component public class UserArgumentResolver implements HandlerMethodArgumentResolver { @Autowired private IUserService userService; @Override public boolean supportsParameter(MethodParameter parameter) { Class> clazz = parameter.getParameterType(); return clazz == User.class; } @Override 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 ticket = cookieUtil.getcookievalue(request, "userTicket"); if (StringUtils.isEmpty(ticket)) { return null; } return userService.getUserBycookie(ticket, request, response); } } -
MVC配置类WebConfig.java
@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Autowired private UserArgumentResolver userArgumentResolver; @Override public void addArgumentResolvers(Listresolvers) { resolvers.add(userArgumentResolver); } }



