分布式项目,分布式Session

分布式系统Session同步问题

在搭建完集群环境后,不得不考虑的一个问题就是用户访问产生的session如何处理。如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在A、B两台服务器,用户在第一次访问网站时,Nginx通过其负载均衡机制将用户请求转发到A服务器,这时A服务器就会给用户创建一个Session(session用于保存用户信息)。当用户第二次发送请求时,Nginx将其负载均衡到B服务器,而这时候B服务器并不存在Session,所以就会将用户踢到登录页面。这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的。

实现步骤:

  • 在用户登录的时候,通过UUidUtils工具类,生成随机UUid,以此作为token(令牌)
  • 通过redis存储用户信息,通过hset命令创建map数据格式,map的名称(自定义常量类,用于代表session信息),map的key(uuid生成的token),map的value(登录用户user对象).设置有效期
  • 创建Cookie,设置Cookie的key和value值,key(自定义常量,用于代表是用户信息),value(uuid生成的token)
  • 设置cookie的有效期(同创建redis有效期),设置cookie的路径path("/")
  • 延长redis中session有效期,在查询redis中session信息同时,重新把用户信息保存到redis中,并设置给cookie返回。
    注:
  • 用户信息保存在redis中设置map数据名称的自定义常量,例:redisToken
  • cookie设置key的自定义常量,例如:cookieToken
    方便在搜索的时候快速查询同时区分其它redis和cookie数据.
    随机生成的uuid作为保存用户信息的唯一标识(可以用AtomicInteger代替)保存在redis中持久化到硬盘数据,服务端通过获取用户的cookie值,就可以获取到保存于redis中的用户信息数据

扩展WebMvcConfigurer实现抽取获取session信息代码

WebMvcConfigurerAdapter是SpringMVC的自动配置适配器接口,内部有很多springmvc扩展方法当spring版本为5.x以上,或者springboot版本为2.x以上,WebMvcConfigurerAdapter被废弃了,使用WebMvcConfigurer替代
WebMvcConfigurerAdapter 是一个实现了WebMvcConfigurer 接口的抽象类,并提供了全部方法的空实现,我们可以在其子类中覆盖这些方法,以实现我们自己的配置,如视图解析器,拦截器和跨域支持等…,由于Java的版本更新,在Java 8中,可以使用default关键词为接口添加默认的方法,Spring在升级的过程中也同步支持了Java 8中这一新特性,所以WebMvcConfigurer 接口可以使用default关键词为接口添加默认的方法,从而摒弃了WebMvcConfigurerAdapter 。
WebMvcConfigurerAdapter 是抽象类实现WebMvcConfigurer 接口,通过继承WebMvcConfigurerAdapter 并重写需要的方法实现自定义WebMvc配置(典型的接口适配模式)

WebMvcConfigurerAdapter部分源码展示:
/**
 * An implementation of {@link WebMvcConfigurer} with empty methods allowing
 * subclasses to override only the methods they're interested in.
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 * @deprecated as of 5.0 {@link WebMvcConfigurer} has default methods (made
 * possible by a Java 8 baseline) and can be implemented directly without the
 * need for this adapter
 */
@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {

    /**
     * {@inheritDoc}
     * <p>This implementation is empty.
     */
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
    }

    /**
     * {@inheritDoc}
     * <p>This implementation is empty.
     */
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }
    
    ...
} 
在JDK1.8之后,可以使用default关键词为接口添加默认的方法,通过直接实现WebMvcCinfigurer接口,实现指定方法来自定义WebMvc配置
public interface WebMvcConfigurer {

    default void configurePathMatch(PathMatchConfigurer configurer) {
    }

    /**
     * Configure content negotiation options.
     */
    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }

    /**
     * Configure asynchronous request handling options.
     */
    default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    }
    
   ...
}       

根据spring/springboot版本编写一个配置类继承/实现WebMvcConfigurationAdaper/WebMvcConfigurer,重写addArgumentResolvers(自定义参数解析器)方法,需要添加自定义的ArgumentResolver

以下WebMvcConfigurerAdapter 比较常用的重写接口
/** 解决跨域问题 **/
public void addCorsMappings(CorsRegistry registry) ;
/** 添加拦截器 **/
void addInterceptors(InterceptorRegistry registry);
/** 这里配置视图解析器 **/
void configureViewResolvers(ViewResolverRegistry registry);
/** 配置内容裁决的一些选项 **/
void configureContentNegotiation(ContentNegotiationConfigurer configurer);
/** 视图跳转控制器 **/
void addViewControllers(ViewControllerRegistry registry);
/** 静态资源处理 **/
void addResourceHandlers(ResourceHandlerRegistry registry);
/** 默认静态资源处理器 **/
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);

自定义类实现WebMvcConfigurer接口扩展,重写所需要自定义的方法

package com.supplier.config;

import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

//声明这是一个配置类
@Configuration
public class MyConfiguration implements WebMvcConfigurer {

	// EL读取配置文件中指定值
	@Value("${file.staticAccessPath}")
	private String staticAccessPath;

	@Value("${file.uploadFolder}")
	private String uploadFolder;

	/**
	 * addResourceHandlers 静态资源处理 处理图片资源映射
	 * 将访问的图片资源路径由staticAccessPath替换成uploadFolder
	 **/
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler(staticAccessPath).addResourceLocations("file:" + uploadFolder);
	}

	/**
	 * UserArgumentResolver实现HandlerMethodArgumentResolver-自定义参数解析器
	 * 
	 * @return 声明一个自定义bean
	 */
	@Bean
	public UserArgumentResolver argumentResolver() {
		UserArgumentResolver userArgumentResolver = new UserArgumentResolver();
		return userArgumentResolver;
	}

	/**
	 * addArgumentResolvers 添加自定义参数解析器
	 */
	@Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolver) {
		argumentResolver.add(argumentResolver()); // 添加自定义的参数解析器
	}

}

自定义参数解析器对象实现HandlerMethodArgumentResolver接口

重写

  • supportsParameter:用于判断是否需要处理该参数,返回true为需要,并会去调用下面的方法resolveArgument,其中MethodParameter方法参数对象
    通过它可以获取该方法参数上的一些信息
  • resolvveArgument方法:真正用于处理参数解析的方法
package com.supplier.config;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
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 com.supplier.entity.Police;
import com.supplier.utils.CookieUtil;
import com.supplier.utils.RedisUtil;

/**
 * HandlerMethodArgumentResolver 自定义参数解析器
 * 
 * @author 张江丰
 *
 */
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

	@Autowired
	private CookieUtil cookieUtil;

	@Autowired
	private RedisUtil redisUtil;

	/**
	 * 用于判断是否需要处理参数,返回true为需要,并会去调用下面的方法resolveArgument
	 * 这里直接返回true,无论如何都会执行参数解析方法resolveArgument
	 */
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> parameterType = parameter.getParameterType();
		System.out.println(parameterType == Police.class ? true : false);
		return true;
	}

	/**
	 * **resolvveArgument方法:真正用于处理参数解析的方法**
	 */
	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
		// 转换成我们需要的request和response
		HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
		HttpServletResponse response = (HttpServletResponse) webRequest.getNativeResponse();
                //通过cookie工具类获取指定名称的cookie
		Cookie cookie = CookieUtil.get(request, CookieUtil.token);
		// cookie不为空,通过cookie的value获取到redis不为空
		if (cookie != null && redisUtil.hget(RedisUtil.getUser, cookie.getValue()) != null) {
                        //redis中保存有用户对象,从而实现分布式session,处理逻辑...
			// request.getSession().setAttribute("user",
			// redisUtil.hget(RedisUtil.getUser, cookie.getValue()));
			return null;
		} else {
                        //未获取到,redis中没有用户,处理逻辑...
			return null;
		}

	}

}

逻辑注释:

  • supportsParament方法的参数MethodParament对象,获取所有请求参数对象,判断是否有指定参数对象返回true,如果是登录方法在resolveArgument放行,如果是其它方法在resolveArgument拦截
  • resolveArgument方法参数,获取request和response对象,获取请求参数cookie数据,通过cookie保存的的token,查询redis数据得到登录用户user

问题:
是否可以用AOP切面,在访问controller之前判断redis中用户状态?
使用WebMvcConfigurer扩展接口HandlerMethodArgumentResolver 自定义参数解析器,可以在supportsParament方法中通过MethodParament对象更加细化的判断请求参数对象,根据业务逻辑需求定制化处理用户信息状态,而使用AOP切面来实现则无法控制.

用户登录Controller方法(集成shiro权限控制)

	@RequestMapping("/tologin")
	@ResponseBody
	public CommonResponse login(HttpServletRequest request, String tokens, HttpServletResponse response,
			Map<String, Object> paramMap) {
		Manager man = null;
		Police po = null;
		String username = null;
		String password = null;
		String flags = null;

		// 直接从redis中获取用户
		Cookie cookie = CookieUtil.get(request, CookieUtil.token);
		if (cookie != null) {
			Object obj = redisUtil.hget(RedisUtil.getUser, cookie.getValue());
			System.out.println("redis中保存的对象,从而实现分布式共享" + obj);
		}
		username = request.getParameter("userName");
		password = request.getParameter("password");
		flags = request.getParameter("flag");

		boolean rememberMe = request.getParameter("rememberMe") != null;
		UsernamePasswordToken token = new UsernamePasswordToken(username, password, flags);
		// RememberMe这个参数设置为true后,在登陆的时候就会在客户端设置remenberme的相应cookie。
		// 下次访问带上这个cookie,访问链接为user链接器的,就不需要进行登录验证,直接进入权限验证。
		if (rememberMe) {
			token.setRememberMe(true);
		}
		Subject subject = SecurityUtils.getSubject();
		String error = null;

		try {
			subject.login(token);
			// 这里的catch到的异常会被继承的父类BaseController处理
		} catch (UnknownAccountException | IncorrectCredentialsException e) {
			error = "用户名或密码错误";
		} catch (ExcessiveAttemptsException e) {
			error = "登录错误次数超过五次,请十分钟后登录!";
		} catch (AuthenticationException e) {
			error = "其它错误:" + e.getMessage();
		}
		logger.error("错误信息:" + error);
		// 获取shiro保存的用户
		if (flags.equals("0")) {
			man = (Manager) subject.getPrincipal();
		} else {
			po = (Police) subject.getPrincipal();
		}
		if (error != null) {
			paramMap.put("error", error);
			return CommonResponseUtil.success(paramMap);
		} else {
			if (flags.equals("0")) {
				paramMap.put("manager", man);
				paramMap.put("flag", flags);
				// 获取唯一uuid
				UUID randomUUID = UUID.randomUUID();
				System.out.println("当前登录用户的uuid:" + randomUUID.toString() + ",redis的key");
                               //第一次登录--设置登录用户的cookie,保存用户信息到redis
				CookieUtil.set(response, CookieUtil.token, randomUUID.toString(), 36000);
				redisUtil.hset(redisUtil.getUser, randomUUID.toString(), man);
			} else {
				paramMap.put("po", po);
				paramMap.put("flag", flags);
				UUID randomUUID = UUID.randomUUID();
				System.out.println("当前登录用户的uuid:" + randomUUID.toString() + ",redis的key");
                                 //第一次登录--设置登录用户的cookie,保存用户信息到redis
				CookieUtil.set(response, CookieUtil.token, randomUUID.toString(), 36000);
				redisUtil.hset(redisUtil.getUser, randomUUID.toString(), po);
			}

			return CommonResponseUtil.success(paramMap);
		}

	}
发布了32 篇原创文章 · 获赞 53 · 访问量 2475

猜你喜欢

转载自blog.csdn.net/qq_41714882/article/details/104165579