秒杀项目中核心功能的实现

秒杀项目-登录中的重难点

一、两次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;
    }

posted @ 2021-07-10 15:43  heyhy  Views(83)  Comments(0Edit  收藏  举报