Java高性能高併發實戰之登錄功能設計(二)

前言

翻看了之前買的課程,今天又重溫了一遍之前的代碼,每次查看也總是能發現自己不足的地方,這裏也開始做一個記錄。也算是開始對項目複習。
系列文章如下:

章節名稱 博客地址
安裝部署Redis 集成Redis(已完結)
頁面登陸功能設計 登錄功能設計(更新優化中)
秒殺頁面具體設計 秒殺詳情頁(已完結)
JMeter初級壓測學習 Jmeter壓測入門學習(已完結)
頁面優化設計 頁面優化設計(已完結)
接口優化 RabbbitMq接口優化(已完結)
圖形驗證碼等 圖形驗證碼及接口防刷(更新優化中)

兩次MD5加密設置。

之前學習過就是以用戶登錄時候設置是以用戶名作爲鹽值來實現MD5加密服務,現在使用固定的鹽值(當然這個鹽值還是在數據庫中進行讀取)下面可以看一下數據庫的設計:
在這裏插入圖片描述
這裏我們指定一個鹽值,在進行加密時候加入到其中。這裏我們進行的兩次MD5操作一方面是保證對於數據在網絡上傳輸時候,我們輸入的密碼傳輸到服務端時候在向底層傳輸時候進行一級的加密操作。這個時候在服務端操作的密碼也不是明文。第二個方面是在防止別人對我們的數據庫盜取時候,不會因爲一級加密就簡單進行反解密拿到我們的數據庫密碼,在服務端中存到底層的密碼是二度加密過。

前端設計

在拿到用戶輸入的密碼以後我們先進行一個初級加密,這個加密的流程和鹽值都是和後端相同的。
在這裏插入圖片描述
前端這樣設計,保證拿到後端的密碼是進行過一級加密的。
在這裏插入圖片描述
這裏介紹一下自己定義的工具類。


public class MD5Util {

	public static  String md5(String src){
		return  DigestUtils.md5Hex(src);
	}
	private static final String salt = "1a2b3c4d";

	public static String inputPassToFormPass(String inputPass) {
		String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
		System.out.println(str);
		// 一級加密
		return md5(str);
	}

	public static String formPassToDBPass(String formPass, String salt) {
		String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4);
		// 二級加密
		return md5(str);
	}

	public static String inputPassToDbPass(String inputPass, String saltDB) {
		String formPass = inputPassToFormPass(inputPass);
		String dbPass = formPassToDBPass(formPass, saltDB);
		// 結合起來的一次完整加密。
		return dbPass;
	}
	
}

這樣,在密碼傳到後臺時候是已經進行過一級的加密操作。然後再進行自定義工具類的二級加密,判斷和從數據庫中根據用戶名取出的密碼是否相同來進行下一步操作。打印出錯誤的信息,還是登錄成功。

數據合法性驗證

既然前端表示是電話號碼,所以必須是11位電話號碼,並且第一位必須是一,所以我們用到了JSR303參數校驗算是一個原生的代碼庫,我們在使用時候還是先進行一個引用處理。然後在我們想要進行校驗的參數前加上@Valid

	<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

如下圖所示我們在想要進行校驗地方加上對應的註解:
在這裏插入圖片描述
然後去進行具體的使用之前先查看都有什麼方法可以進行使用,如下圖所示左邊的就是可以進行使用的方法
在這裏插入圖片描述
具體含義:
在這裏插入圖片描述
但是我們思考一下對於電話號碼來說並不能只是單純的不能爲空,我們需要判斷值爲11位,減輕後臺的負擔,思考一下,對於格式不符合標準的電話號碼,我們都不用請求數據庫,因爲此時請求數據庫,也是電話號碼不存在,所在在前端就進行驗證,不正確不予以通過。這裏我們可以仿照他們的格式來自己編寫一個判斷是否是正確電話號碼格式的註解。

判斷是否是11位電話號碼

點擊源碼進行查看,發現就一些註解和原生的兩個函數一個默認值。我們仿照進行完成自己的校驗器。
在這裏插入圖片描述
@isMobiles

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {
isMobVa.class}//指向具體的實現
)
public @interface isMobiles  {
    boolean required() default true;

    String message() default "手機號碼格式錯誤";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

isMobVa

public class isMobVa implements ConstraintValidator<isMobiles,String> {

    private  boolean required=false;
    @Override
    public void initialize(isMobiles isMobiles) {
        required=isMobiles.required();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if(required){// 表示傳入了電話號碼值。
            return ValidatorUtil.isMobile(s);
        }else {
            if(StringUtils.isEmpty(s))
            {
                return  true;
            }
            else {
                return ValidatorUtil.isMobile(s);
            }
        }


    }
}

具體實現

public class ValidatorUtil {
	
	private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
	//表示1後面跟上幾個數字 
	
	public static boolean isMobile(String src) {
		if(StringUtils.isEmpty(src)) {
			return false;
		}
		Matcher m = mobile_pattern.matcher(src);
		return m.matches();
	}
	
}

全局異常處理

我們在登錄時候有可能會出現各種的問題,但是若是在我們的業務函數中處理這些異常時候,代碼就會顯得很是臃腫,所以我們定義一個異常全局處理函數GlobalExceptionHandler,進行異常的全局處理,在業務邏輯中出現了各種的異常,我們只需要繼續拋出,就會由異常處理器進行處理。

@ControllerAdvice
@ResponseBody

public class GlobalExceptionHandler {

	@ExceptionHandler(value=Exception.class)
	public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
		e.printStackTrace();
		if(e instanceof GlobalException) {
			// 內核異常
			GlobalException ex = (GlobalException)e;
			return Result.error(ex.getCm());
		}else if(e instanceof BindException) {
			// 綁定異常。
			BindException ex = (BindException)e;
			List<ObjectError> errors = ex.getAllErrors();
			// 參數校驗有很多的錯誤。
			ObjectError error = errors.get(0);
			String msg = error.getDefaultMessage();
			return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
		}else {
			return Result.error(CodeMsg.SERVER_ERROR);
		}
	}
}

例如我們在login過程中,可能會有密碼錯誤,用戶名不存在這些錯誤,一般也都是在service層中進行處理,但是這裏我們只是將異常進行拋出,不進行具體的處置。減少了代碼的冗餘度。
在這裏插入圖片描述

分佈式Session

對於分佈式session來說也是應用比較廣泛也是非常重要的一門技術。對於秒殺項目來說,並不可能將所有的請求都打在同一臺服務器上,對於訪問量和請求量比較大的情況下性能也會沒辦法達到,就需要我們多臺服務器搭建集羣進行數據的處理。此時我們假設一個情況,對於一個用戶的請求,第一次打到了第一臺服務器上,服務器記錄session信息,對於第二次請求打到第二太服務器上,難道用戶還用重新登錄嗎?這當然是不現實的,所以此時需要做的就是對於集羣內的服務器進行一個session的同步,對於原生的session同步方法就是對於集羣內的所有session進行一個同步處理,但是這樣的效率太低下。對於一個集羣裏面的數據,同步起來也是相當消耗時間。
所以現在常用的方法就是: 用戶在登錄成功以後給用戶生成一個類似於session id的東西,叫做token來標識這個用戶,寫到cookie中傳遞給客戶端。隨後的客戶端在訪問的過程中,都在cookie中上傳這個token,服務端就會根據這個token來取到用戶對應的session信息。和容器實現原生的session類似

設置token

  1. 首先設置UUID信息,爲每一個用戶都生成唯一的UUID
public class UUIDUtil {
	public static String uuid() {
		return UUID.randomUUID().toString().replace("-", "");
	}
}
  1. 此時我們有了token以後,設計將token加入到cookie中.
public static final String COOKI_NAME_TOKEN = "token";
public static final int TOKEN_EXPIRE = 3600*24 * 2;
	private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
		redisService.set(MiaoshaUserKey.token, token, user);
		// 加入第三方緩存中,在下一次的加載中從redis中通過key(token)取到。
		Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
		cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
		// 有效期。
		cookie.setPath("/");
		// 完成以後 將cookie寫到response中。
		response.addCookie(cookie);
	}

以上就是設置token信息到cookie中,現在我們模擬在用戶登錄時候取出對應的信息。

public MiaoshaUser getByToken(HttpServletResponse response, String token) {
		if(StringUtils.isEmpty(token)) {
			return null;
		}
		MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
		// 注意這裏 若是不爲空 表示取到了對應的信息可以返回 但是我們要實時更新信息 表示用戶又登錄成功,過期值又重新設定。
		if(user != null) {
			addCookie(response, token, user);
		}
		return user;
	}

初值的判定。

以上我們就實現了根據服務端將一個token寫到cookie中,然後客戶端在隨後的訪問中攜帶這個cookie,這個時候服務端就可以根據cookie中的token找到這個用戶。
這個時候就需要進一步的優化處理,我們對於商品頁來說,會有很多的方法,都需要進行用戶權限的判定,所以這個時候,就利用到了Sping Mvc的addArgumentResolvers方法,對於Controller的方法中會有很多的參數例如request,modle等,但是我們並沒有顯示賦值,都是通過這個方法的回掉處理,這個時候我們就利用這個特性來對我們的用戶對象做一個處理:判斷當前對象類型是不是我們的已經進行過驗證的類型:

UserArguementResolver
繼承HandlerMethodArgumentResolver 以後會要就實現以下兩個函數,具體完成即可。


@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
	

	@Autowired
	MiaoshaUserService userService;

	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> clazz = parameter.getParameterType();
		return clazz==MiaoshaUser.class;
	}

	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
		HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
		HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);

		String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
		String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
		if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
			return null;
		}
		String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
		return userService.getByToken(response, token);
	}

	private String getCookieValue(HttpServletRequest request, String cookiName) {
		Cookie[]  cookies = request.getCookies();
		for(Cookie cookie : cookies) {
			if(cookie.getName().equals(cookiName)) {
				return cookie.getValue();
			}
		}
		return null;
	}
}

後記

這裏也只是講出部分內容,具體的調試大家可以去下載源碼自行調試與運行。
對於具體的源碼部分對應到我githubSeckill倉庫的miaosha_2
倉庫鏈接:Seckill

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