【商城秒殺項目】-- 分佈式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校驗

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