实际应用中使用分布式的多台应用服务器,若使用原生的Session,用户第一个请求在第一台服务器上,第二个请求在另一台服务器上,用户的Session信息就丢失了。
解决方法是使用Session同步,从一台服务器上同步到另一台服务器上,但因性能问题,实现复杂,并不常用。
这里使用分布式缓存存储Session信息来实现分布式Session
- 用户登录成功后生成token(SessionId)来标识用户,写到Cookie中传递给客户端,在随后的访问中Cookie中都包含这个token。
- 服务端根据传来的token取得用户的Session信息
实现分布式Session
MiaoshaUserService中
- 使用UUID生成token
- 将token(key)和对应用户(Session信息,value)写入第三方缓存Redis
- 生成Cookie,将token写入Cookie
- 将Cookie写入response
public String login(HttpServletResponse response, LoginVo loginVo) {
if (loginVo == null) {
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在
MiaoshaUser user = getById(Long.parseLong(mobile));
if (user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//验证密码
String dbPass = user.getPassword();
String saltDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
if (!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
//生成token,Cookie,将token和对应用户写入第三方缓存redis
String token = UUIDUtil.uuid();
addCookie(user, response, token);
return token;
}
private void addCookie(MiaoshaUser user, HttpServletResponse response, String token) {
//将token和对应用户写入第三方缓存redis
//set(prefix, key, value),这里使用prefix+key作为redis的key
redisService.set(MiaoshaUserKey.token,token,user);
//生成Cookie
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds()); //设置有效期和session有效期一致
//设置网站的根目录
cookie.setPath("/");
//写到response中
response.addCookie(cookie);
}
//从redis获得session信息
//session有效期为最后一次访问时间加有效期时长
public MiaoshaUser getByToken(String token, HttpServletResponse response) {
if (StringUtils.isEmpty(token)) {
return null;
}
MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
//生成新cookie,来延长有效期
if (user != null) {
addCookie(user, response, token);
}
return user;
}
UUIDUtil
public class UUIDUtil {
public static String uuid() {
//去掉原生UUID中的横杠
return UUID.randomUUID().toString().replace("-","");
}
}
登录成功后查看发现请求Cookie中包含了token
这里登录成功后跳转到goods/to_list
$.ajax({
url:"/login/do_login",
type:"POST",
data:{
mobile:$("#mobile").val(),
password: password
},
success:function (res) {
layer.closeAll();
if (res.code == 0) {
layer.msg("成功");
window.location.href="/goods/to_list";
} else {
layer.msg(res.msg);
}
console.log(res);
},
error:function () {
layer.closeAll();
}
});
跳转到goods/to_list,服务端接收token以获取Session信息,若无token跳转回登录页面,若成功获得session信息,生成新Cookie装入token,并修改缓存来延长session有效期
//有时候手机端不会把token放入cookie,直接作为参数传
//首先先从param中取token,取不到再从cookie中取
@RequestMapping("/to_list")
public String toList(Model model,@CookieValue(value=MiaoshaUserService.COOKIE_NAME_TOKEN)String cookieToken,
@RequestParam(value=MiaoshaUserService.COOKIE_NAME_TOKEN)String paramToken,HttpServletResponse response) {
if(StringUtils.isEmpty(paramToken)&&StringUtils.isEmpty(cookieToken)) {
return "login";//返回登录页面
}
String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
MiaoshaUser user=miaoshaUserService.getByToken(token,response);
model.addAttribute("user", user);
return "goods_list";//返回商品列表页面
}
参数解析
其他页面有可能也需要获取token,获取MiaoshaUser对象,为了简化代码
我们希望直接将MiaoshaUser作为参数注入到Controller的方法中,像request, response, model一样可以直接获取,这些参数都是由ArgumentResolver往Controller框架里赋值。同样我们要对MiaoshaUser做参数解析,这样遍历方法参数时,发现有该参数,就会给它赋值。
@RequestMapping("/to_list")
public String toList(Model model, MiaoshaUser user) {
model.addAttribute("user", user);
return "goods_list";
}
- 为MiaoshaUser写一个UserArgumentResolver,实现接口HandlerMethodArgumentResolver
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
MiaoshaUserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz == MiaoshaUser.class; //类型是MiaoshaUser才做处理
}
//做获取token和session信息的操作
@Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer, NativeWebRequest webRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return "login";
}
String token = StringUtils.isEmpty(paramToken)? cookieToken : paramToken;
return userService.getByToken(token, response);
}
private String getCookieValue(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
//遍历所有cookie
for (Cookie cookie:cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
return null;
}
}
- 新建WebConfig继承WebMvcConfigurerAdapter,注册UserArgumentResolver
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
UserArgumentResolver userArgumentResolver;
//给controller的方法赋值
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}