分佈式項目,分佈式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);
		}

	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章