【商城秒杀项目】-- 分布式Session的实现、使用Aop校验Token令牌

通常秒杀项目可能不止部署在一个服务器上,而是使用分布式部署在多台服务器,这时候假如用户登录是在第一个服务器,第一个请求到了第一台服务器,这是没问题的;但是第二个请求到了第二个服务器,那么用户的Session信息就丢失了

解决:使用session同步,无论访问那一台服务器,session都可以取得到

本项目:利用一台缓存服务器集中管理session,即利用缓存统一管理session

分布式Session的几种实现方式

  • 使用Session Replication方式管理 (即session复制)

简介:将一台机器上的Session数据广播复制到集群中其余机器上

使用场景:机器较少,网络流量较小

优点:实现简单、配置较少、当网络中有机器Down掉时不影响用户访问

缺点:广播式复制到其余机器有一定廷时,带来一定网络开销

  • 使用Session Sticky方式管理

简介:即粘性Session、当用户访问集群中某台机器后,强制指定后续所有请求均落到此机器上

使用场景:机器数适中、对稳定性要求不是非常苛刻

优点:实现简单、配置方便、没有额外网络开销

缺点:网络中有机器Down掉时、用户Session会丢失、容易造成单点故障

  • 使用缓存集中式管理

简介:将Session存入分布式缓存集群中的某台机器上,当用户访问不同节点时先从缓存中拿Session信息

使用场景:集群中机器数多、网络环境复杂

优点:可靠性好

缺点:实现复杂、稳定性依赖于缓存的稳定性、Session信息放入缓存时要有合理的策略写入

本项目分布式Session的实现

实现思路:用户登录成功之后,给这个用户生成一个sessionId(用token来标识这个用户),并写到cookie中传递给客户端;然后客户端在随后的访问中,都在cookie中传递这个token,服务端拿到这个token之后,就根据这个token来取得对应的session信息(token利用uuid生成)

登录成功后给用户生成sessionId:

生成sessionId的具体代码:

addCookie方法解读:将MiaoshaUserKey前缀+sessionId(sessionId即token)组成了一个完整的Key,例如:“MiaoshaUserKey:tk4470ee9b98eb4e63bbc52a4e9b65052e”,其中MiaoshaUserKey前缀=“MiaoshaUserKey:tk”,token=“4470ee9b98eb4e63bbc52a4e9b65052e”,作为Key和对应的用户信息(user对象信息会转换为字符串类型)一起存入Redis 缓存中;此token对应的是一个用户,将用户信息存放到一个第三方的缓存中,当访问其他页面的时候,就可以从cookie中获取到token,再访问redis拿到用户信息来判断登录情况,存入redis中的内容如下:

客户端在随后的访问中,都会在cookie中传递这个token,服务端拿到这个token之后,就根据这个token去缓存中取得对应的(用户信息)session信息,如下:

后端验证session代码如下:

这里就是登录成功之后,后端把token以响应头的形式返回给前端,然后在后面请求的时候,会带上这个token,那么后端就可以根据该token去缓存里面取得相对应的用户信息,从而实现分布式session

像上面那样使用@RequestParam和@CookieValue来获取token比较麻烦,可想办法直接在controller的请求方法上面直接注入MiaoshaUser(用户的信息),然后直接通过方法的参数就可以获取用户的信息,从而简化代码;就像SpringMVC中的controller 方法中可以有很多参数可以直接使用(例如request和response对象),有些参数不需要传值,就可以直接获取到一样

例如优化后的代码:

优化校验token令牌所需具体代码

创建一个UserArgumentResolver类并且实现HandlerMethodArgumentResolver接口,然后重写里面的resolveArgument和supportsParameter方法,既然要让MiaoshaUser这个实例对象可以像SpringMVC中的controller那样直接使用HttpServletRequest的实例对象request,那么解析前端传来的token或者请求参数里面的token的业务逻辑就在这里完成:

package com.javaxl.miaosha_05.config;

import com.javaxl.miaosha_05.domain.MiaoshaUser;
import com.javaxl.miaosha_05.service.MiaoshaUserService;
import com.javaxl.miaosha_05.util.UserContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

//将UserArgumentResolver注册到config里面去
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    //既然能注入service,那么可以用来容器来管理,将其放在容器中
    @Autowired
    MiaoshaUserService miaoshaUserService;

    public Object resolveArgument(MethodParameter arg0, ModelAndViewContainer arg1, NativeWebRequest webRequest,
                                  WebDataBinderFactory arg3) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);
        //获取cookie
        String cookieToken = UserContext.getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return null;
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        MiaoshaUser user = miaoshaUserService.getByToken(response, token);
        return user;
    }

    public boolean supportsParameter(MethodParameter parameter) {
        //返回参数的类型
        Class<?> clazz = parameter.getParameterType();
        return clazz == MiaoshaUser.class;
    }
}

新建一个WebConfig类继承WebMvcConfigurerAdapter,并且重写addArgumentResolvers方法,并且注入之前写好的UserArgumentResolver类,因为UserArgumentResolver类使用了@Service进行标注,已经放到容器里面了,所以这里可以直接注入:

package com.javaxl.miaosha_05.config;

import com.javaxl.miaosha_05.interceptor.AccessInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
	@Autowired
	UserArgumentResolver userArgumentResolver;
	@Autowired
	AccessInterceptor accessInterceptor;
	
	@Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
		//将UserArgumentResolver注册到config里面去	
		argumentResolvers.add(userArgumentResolver);
	}	
	
	/**
	 * 注册拦截器
	 */
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		//注册
		registry.addInterceptor(accessInterceptor);
		super.addInterceptors(registry);
	}	
}

然后就可以直接在controller里面的方法里获取我们想要的MiaoshaUser参数并判断session,如下:

从上述代码来看,即使是优化过后,每个接口里面也要写一段重复的代码:

if (user == null) {
   return Result.error(CodeMsg.SESSION_ERROR);
}

所以,还需要进一步优化,使得这一段代码不用重复写,那就是使用自定义注解+Aop来处理token令牌的校验

使用自定义注解+Aop来进一步优化token令牌校验

在我们的系统里,有的接口需要进行token令牌校验,而有的接口是不需要进行token令牌校验的,比如登录接口;我们可以使用自定义注解来判断接口是否需要进行token令牌校验,使得系统更加灵活

新建一个自定义注解DisableToken,用于在切面里判断是否需要进行校验,如果有则跳过token校验,如果没有则不跳过

package com.javaxl.miaosha_05.annotation;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 作用在方法上,以及类上
 */
@Target({ METHOD, TYPE })
@Retention(RUNTIME)
@Inherited
public @interface DisableToken {
}

新建一个用于校验token令牌的切面MiaoshaUserTokenAspect,判断是否需要进行校验也在这里完成:

package com.javaxl.miaosha_05.aspect;

import com.javaxl.miaosha_05.annotation.DisableToken;
import com.javaxl.miaosha_05.domain.MiaoshaUser;
import com.javaxl.miaosha_05.exception.GlobalException;
import com.javaxl.miaosha_05.result.CodeMsg;
import com.javaxl.miaosha_05.service.MiaoshaUserService;
import com.javaxl.miaosha_05.util.UserContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
public class MiaoshaUserTokenAspect {
    @Autowired
    MiaoshaUserService userService;

    @Pointcut("execution( * com.javaxl..controller.*.*(..))")
    public void miaoshaUserTokenCut() {
    }

    @Around("miaoshaUserTokenCut()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();

        Signature s = pjp.getSignature();
        MethodSignature ms = (MethodSignature) s;
        Method m = ms.getMethod();
        Annotation[] annotations = m.getAnnotations();
        for (Annotation annotation : annotations) {
            //如果在方法上添加了DisableToken注解,那么此方法是不需要token令牌就能访问的
            if (annotation instanceof DisableToken) {
                //直接放行
                return pjp.proceed(args);
            }
        }

        int count = 0;
        HttpServletRequest request = null;
        HttpServletResponse response = null;
        MiaoshaUser miaoshaUser = null;
        //主要是对参数数组的中的秒杀User做封装处理
        int loop = 0;

        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof HttpServletRequest) {
                count++;
                request = (HttpServletRequest) args[i];
            } else if (args[i] instanceof HttpServletResponse) {
                count++;
                response = (HttpServletResponse) args[i];
            } else if (args[i] instanceof MiaoshaUser) {
                count++;
                miaoshaUser = (MiaoshaUser) args[i];
                loop = i;
            }
        }

        if (count == 3) {
            String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);
            String cookieToken = UserContext.getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
            //如果前端没传token过来
            if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
                throw new GlobalException(CodeMsg.SESSION_ERROR);
            }
            String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
            //根据token从缓存取用户信息
            miaoshaUser = userService.getByToken(response, token);
            args[loop] = miaoshaUser;
        }
        Object ob = pjp.proceed(args);//ob为方法的返回值
        return ob;
    }
}

使用如下:

校验token令牌除了使用Aop,也可以使用拦截器,详情可参考:

自定义注解+拦截器+SSM应用于前端token校验

发布了133 篇原创文章 · 获赞 94 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_42687829/article/details/104445741