通常秒杀项目可能不止部署在一个服务器上,而是使用分布式部署在多台服务器,这时候假如用户登录是在第一个服务器,第一个请求到了第一台服务器,这是没问题的;但是第二个请求到了第二个服务器,那么用户的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,也可以使用拦截器,详情可参考: