秒杀项目中核心功能的实现
秒杀项目-登录中的重难点
一、两次MD5的作用
做法:进行两次加密,调用MD5Util工具类的md5()进行加密。
用户端:Password=MD5(明文+固定的Salt)===用户输入
服务端:Password=MD5(用户输入+随机salt)
主要是考虑到安全问题。第一次MD5,防止明文密码在网络端进行传输。第二次MD5,防止数据库泄露之后,密码被反推,密码被盗。
代码实现
public class MD5Util {
public static String md5(String src){
return DigestUtils.md5Hex(src);//对密码进行加密
}
private static final String salt="1a2b3c4d";
public static String inputPassFormPass(String inputPass){//第一次MD5
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){//第二次MD5
String str=""+salt.charAt(0)+salt.charAt(2)+formpass+salt.charAt(5)+salt.charAt(4);
return md5(str);
}
public static String inputPassToDbPass(String input,String saltDB){//转化为数据库中的密码
String s = inputPassFormPass(input);
String dbpass = formPassToDBPass(s, saltDB);
return dbpass;
}
二、分布式Session一致性的四种解决方案
1、cookie和session的区别和联系
cookie是本地客户端用来存储少量的用户数据信息的,保存在客户端,用户很容易读取,安全性不高,存储的数据量小。
session是服务器端用来存储部分用户数据信息的,保存在服务器,用户不容易获取,安全性高。存储的数据量大,但是保存在服务器端,会占有一定的服务器资源。
2、session有什么用?
在一次客户端与服务端的会话中,客户端向浏览器发送请求,首先cookie会携带上次请求存储的数据(jsessionID)到服务器,服务器根据请求参数中的JessionID去服务器的Session 库中看看有没有这个Jsession, 如果存在,那么服务器就知道此用户是谁,如果不存在,就会创建一个JSESSIONID,并在本次请求结束后将JSESSIONID返回给客户端,同时将此JSESSIONID在客户端cookie中进行保存。
客户端和服务器之间是通过http协议进行通信,但是http协议是无状态的,不同次请求会话是没有任何关联的,但是优点是处理速度快。
session是一次浏览器和服务器的交互的会话,当浏览器关闭的时候,会话就结束了,但是会话session还在,默认session是还要保留30分钟的。
3、分布式Session一致性
客户端发一个请求,经过负载均衡后会被分配到其中任意一个服务器上。但是由于不同的服务器含有不同的web服务器,不同的web服务器就不能发现之前的web服务器保存的session信息,就会再生成一个jsessionID,之前的状态就会丢失。
方案(一)客户端存储
直接将信息存储在cookie中,cookie是存储在客户端上的一小段数据,客户端通过http协议和服务器进行cookie交互,通常用来存储一些不敏感信息。
缺点:不安全,cookie大小,存储类型存在限制 ;而且一次请求中cookie过大,会增加网络的开销。
代码
private static String redisKey = "user:session";
/**
* 登录成功后生成并保存token
*
* @param response
* @param user
* @return
*/
public boolean login(HttpServletResponse response, User user) {
// 验证用户身份
User user = userService.check(……);
// salt值建议做成可配置化
String salt = "";
String token = DigestUtils.md5Hex(user.getName() + salt); //这里token作为用户信息唯一标识
addCookie(response, token);
return true;
}
/**
* 添加至redis和cookie
*
* @param response
* @param token
*/
private void addCookie(HttpServletResponse response, String token) {
redisTemplate.opsForValue().set(redisKey, token, 366, TimeUnit.DAYS);//放入缓存
Cookie cookie = new Cookie("token", token);
cookie.setMaxAge(3600 * 24 * 366); //和Redis缓存失效时间一致
cookie.setPath("/");
response.addCookie(cookie);
}
/**
* 获取已登录的用户信息
* @param response
* @return
*/
public String getByToken(HttpServletResponse response) {
String userinfo = redisTemplate.opsForValue().get(redisKey);
//延长session有效期,过期时间=最后一次使用+失效时间,cookie可以不延长
if (StringUtils.isNotEmpty(userinfo)) {
addCookie(response, userinfo);
}
return userinfo;
}
方案二:session复制
session复制是小型企业应用使用较多的一种服务器集群session管理机制,在真正的开发使用的并不是很多,通过对web服务器(例如Tomcat)进行搭建集群。
存在的问题
session同步的原理是在同一个局域网里面通过发送广播来同步session的,一旦服务器多了,并发上来了,session需要同步的数据量就大了,需要将其他服务器上的session全部同步到本服务器上,会带来一定的网路开销,在用户量特别大的时候,会出现内存不足的情况
优点
服务器之间的session信息都是同步的,任何一台服务器宕机的时候不会影响另外服务器中session的状态,配置相对简单。
Tomcat内部已经支持分布式架构开发管理机制,可以对tomcat修改配置来支持session复制,在集群中的几台服务器之间同步session对象,使每台服务器上都保存了所有用户的session信息,这样任何一台本机宕机都不会导致session数据的丢失,而服务器使用session时,也只需要在本机获取即可。
方案三:session绑定
Nginx是一款自由的、开源的、高性能的http服务器和反向代理服务器。
Nginx能做什么?
反向代理、负载均衡、http服务器(动静代理)、正向代理
如何使用nginx进行session绑定
我们利用nginx的反向代理和负载均衡,之前是客户端会被分配到其中一台服务器进行处理,具体分配到哪台服务器进行处理还得看服务器的负载均衡算法(轮询、随机、ip-hash、权重等),但是我们可以基于nginx的ip-hash策略,可以对客户端和服务器进行绑定,同一个客户端就只能访问该服务器,无论客户端发送多少次请求都被同一个服务器处理。
在nginx安装目录下的conf目录中的nginx.conf文件
upstream aaa {
Ip_hash;
server 39.105.59.4:8080;
Server 39.105.59.4:8081;
}
server {
listen80;
server_name www.wanyingjing.cn;
#root /usr/local/nginx/html;
#index index.html index.htm;
location / {
proxy_pass http:39.105.59.4;
index index.html index.htm;
}
}
缺点
容易造成单点故障,如果有一台服务器宕机,那么该台服务器上的session信息将会丢失。
前端不能有负载均衡,如果有,session绑定将会出问题。
优点
配置简单
方案四:基于redis存储session方案
原理是使用Redis作为session存储容器,登录时将session信息存储至cookie客户端,同时服务端将session信息存至redis缓存,双重保障,接下来的接口调用直接可以获取到cookie中的token信息作为参数传递进来即可,如果发现token为空,则再从redis中获取,如果两者都为空,则说明session已过期。
本项目采用的是方案四
我们是这样做的。
第一次登陆的时候,会随机产生一个Token字符串,并向cookie中加入Token,在Redis中记录Token与用户信息的映射。Key值为:MiaoShaUserKey:”fjfnernrejfnejnf”。Value:是一个User对象。get MiaoshaUserKey:tk24aaa7309a7d44b1bf7eee9a99045bd0 {"id":15700761667,"lastLoginDate":1627360841000,"loginCount":1,"nickname":"heyuan","password":"b7797cce01b4b131b433b6acf4add449","registerDate":1626237634000,"salt":"1a2b3c4d"}"
客户端随后访问服务端的时候携带Cookie,服务端取出cookie中字段值,访问Redis,就能得到用户的相关信息。
代码
/*
登录功能的实现
*/
public boolean login(HttpServletResponse response, LoginVo loginVo){
if(loginVo==null){
throw new GlobalException(CodeMsg.SERVER_ERROR);
//return CodeMsg.SERVER_ERROR;
}
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
//判断手机号是否存在
MiaoshaUser user=getById(Long.parseLong(mobile));//手机号String 转换为Long
if(user==null){
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
// return CodeMsg.MOBILE_NOT_EXIST;//手机号码不存在
}
//验证密码
String dbpass = user.getPassword();//数据库得到的密码
String saltDB = user.getSalt();//
String calcPass = MD5Util.formPassToDBPass(password, saltDB);//
if(!calcPass.equals(dbpass)){//用户输入的密码进行加密然后与数据库的密码进行比较
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
// return CodeMsg.PASSWORD_ERROR;//输入密码错误
}
//生成cookie
//1生成token
String token= UUIDUtil.uuid();
//2生成cookie
addCookie(response,user,token);
return true;
}
/*
往缓存里添加该值;
生成一个新的cookie:包含Token,以及用户信息
有效期设置为永不过期。就是登陆一次,就可以一直地访问,不用担心Cookie过期。
*/
private void addCookie(HttpServletResponse response,MiaoshaUser user,String token){
//1、将Token与用户的映射信息加入缓存
redisService.set(MiaoshaUserKey.token,token,user);//放user进入redis缓存,取名为token
//2、生成cookie
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());//设置有效期
cookie.setPath("/");
//3、将cookie返回给客户端
response.addCookie(cookie);//
}
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
MiaoshaUserService userService;
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
Class<?>clazz= methodParameter.getParameterType();
return clazz== MiaoshaUser.class;
}
/*
一种获取session的方式
从请求中获取用户信息 getToken()
*/
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.COOKIE_NAME_TOKEN);//从请求中获得token
String cookieToken=getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
return null;
}
String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;//如果paramToken参数为空,那么token就是cookieToken
return userService.getByToken(response,token);//该token就是从cookie中拿到的token.
}
public String getCookieValue(HttpServletRequest request,String cookieName){
Cookie[] cookies=request.getCookies();
for(Cookie cookie:cookies){
if(cookie.getName().equals(cookieName)){
return cookie.getValue();
}
}
return null;
}
}
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
UserArgumentResolver userArgumentResolver;
/*
框架会回调这个方法。把response,等参数添加进去
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}
/*
获得Token
*/
public MiaoshaUser getByToken( HttpServletResponse response,String token) {
if(StringUtils.isEmpty(token)) {
return null;
}
//System.out.println(redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class));//拿到的是Miaoshauser的地址
MiaoshaUser user= redisService.get(MiaoshaUserKey.token,token,MiaoshaUser.class);
//延长有效期。当我们10点访问的时候,cookie在10点30过期。但是在10点10分访问的时候,那么就在10点40过期了。实现
if(user!=null){
addCookie(response,user,token);
}
return user;
}