簡(jiǎn)述一下項(xiàng)目中手寫(xiě)的Token驗(yàn)證服務(wù)設(shè)計(jì)過(guò)程
PART A 設(shè)計(jì)校驗(yàn)的哈希算法
這里直接展示整個(gè)項(xiàng)目中用到的算法庫(kù),其中涉及位運(yùn)算的可不管
直接應(yīng)用到的方法是hash(str)
大概流程如下
1.構(gòu)造一個(gè)大素?cái)?shù)表并隨機(jī)打亂
2.提供足夠快的快速冪
3.哈希規(guī)則:\sum 下標(biāo)對(duì)應(yīng)byte^^randomPrimes[下標(biāo) % 素?cái)?shù)表長(zhǎng)度] % 128
為了更快的hash過(guò)程其實(shí)可以把下標(biāo)進(jìn)一步轉(zhuǎn)為其bitcount,這樣算冪會(huì)把log的復(fù)雜度略降一點(diǎn)
package com.noresp.oj.utils;/*** 方便OJ搭建的簡(jiǎn)易算法庫(kù)* 目前可提供:* 隨機(jī)大素?cái)?shù)表* 隨機(jī)打亂* 哈希(注意:特定用途)* 整型交換、bitcount、fastPow* 隨機(jī)數(shù)*/
public class AlgsUtils {private static final int[] bitmasks = new int[0x100];private static final int[] randomPrimes = new int[1<<10];public static final long magicNumber = 19260817L;public static class SimpleRandom {long seed = 1L;public void setSeed(long seed) {this.seed = seed;}/*** 簡(jiǎn)易高效的手寫(xiě)隨機(jī)數(shù)* 大概比Math.random快20倍(2^^26數(shù)量級(jí)下)* @return 隨機(jī)數(shù)*/public long next() {seed = seed*1103515245+12345 & 0xffffffffL; // 模擬unsigned int // 切記0xffffffff沒(méi)有L會(huì)翻車。。return seed >> 16;}public int next(int mod) {return (int)(next()%mod);}}static {initializeBitmasks();initializePrimeTable();randomShuffle(randomPrimes,magicNumber);}/*** O(n)打長(zhǎng)度為n的二進(jìn)制表* 測(cè)試通過(guò)*/private static void initializeBitmasks() {for(int i = 0xff; i > 0; --i) {if(bitmasks[i] != 0) continue;for(int j = i; j > 0; j -= j&-j) {bitmasks[i]++;}for(int j = i, k = 0; j > 0; j -= j&-j, k++) {bitmasks[j] = bitmasks[i]-k;}}}/*** 計(jì)算二進(jìn)制1的個(gè)數(shù)* 測(cè)試通過(guò)* @param value* @return*/public static int bitCount(int value) {int result = 0;for(; value > 0; value >>>= 8) {result += bitmasks[value & 0xff];}return result;}/*** 通過(guò)固定的隨機(jī)素?cái)?shù)進(jìn)行哈希/加密* 哈希串 = \sum 下標(biāo)對(duì)應(yīng)byte^^randomPrimes[下標(biāo) % 素?cái)?shù)表長(zhǎng)度] % 128* 時(shí)間復(fù)雜度O(n log2(log2m)),其中n為字符串長(zhǎng)度,m為最大的隨機(jī)素?cái)?shù)大小* @param string* @return*/public static String hash(String string) {if(string == null || string.length() == 0) return null;byte[] b = string.getBytes();for(int i = 0; i < b.length; i++) {int j = b[i];int k = randomPrimes[i & randomPrimes.length-1]; // 仿java.util思路 二進(jìn)制長(zhǎng)用&優(yōu)化取代%k = bitCount(k);b[i] = (byte)fastPow(j,k,0x7f);}return new String(b); //注意不要b.toString()}/*** 快速冪求解a^^n%mod,n不支持負(fù)數(shù)* @param a* @param n* @param mod* @return*/public static int fastPow(long a,long n,int mod) {long res = 1; //long防相乘溢出while(n > 0) {if((n&1) == 1) {res = res*a;if(res >= mod) res %= mod;}a *= a;if(a >= mod) a %= mod;n >>= 1;}return (int)res;}/*** 簡(jiǎn)單的篩法初始化素?cái)?shù)表*/private static void initializePrimeTable() {int n = randomPrimes.length << 8; // 一個(gè)大概的打表估值,預(yù)計(jì)素?cái)?shù)的大小在1e5數(shù)量級(jí)boolean[] notPrime = new boolean[n];for(int i = 2; i*i < n; i++) {if(!notPrime[i]) {for(int j = i; j < n; j += i) {notPrime[j] = true;}}}// 優(yōu)先選取大素?cái)?shù),因此倒序處理+插入兩個(gè)極大的素?cái)?shù)randomPrimes[0] = (int)1e9+7;randomPrimes[1] = 998244353;for(int i = n-1, j = 2; true; --i) {if(!notPrime[i]) randomPrimes[j++] = i;if(j == randomPrimes.length) return;}}/*** 隨機(jī)打亂一個(gè)整型數(shù)組* @param toRandom*/public static void randomShuffle(int[] toRandom,long seed) {SimpleRandom roll = new AlgsUtils.SimpleRandom();roll.setSeed(seed);for(int i = toRandom.length-1; i > 0; --i) {swap(toRandom,i,roll.next(i+1));}}/*** 交換兩個(gè)數(shù),注意安全使用*/public static void swap(int[] arr,int i,int j) {if(SafeUtils.isOutOfBound(arr,i)) return;if(SafeUtils.isOutOfBound(arr,j)) return;int t = arr[i];arr[i] = arr[j];arr[j] = t;}public static void swapRange(int[] arr,int lo,int hi) {while(lo < hi) swap(arr,lo++,hi--);}
}
PART B 進(jìn)一步的哈希
由PART A可以看到任意編碼的字符串都把char限制在0-127范圍內(nèi),但可能存在特殊的轉(zhuǎn)義符影響面向文本的協(xié)議
因此需要把0-127映射到ASCII中a-z A-Z 0-9的范圍內(nèi)
為了滿足盡可能的均勻分布,又亂寫(xiě)了一個(gè)算法(其實(shí)ch+i是多余的)
public static String visualizableHash(String str) {StringBuilder sb = new StringBuilder("");for(int i = 0; i < str.length(); i++) {char ch = str.charAt(i);if(isVisualChar(ch)) sb.append(ch);else {char curChar = 'a';long factor = (int)(ch)*17+i*23;int pos = (int)(factor % (26+26+10));if(pos < 26) curChar = (char)('a'+pos);else if(pos-26 < 26) curChar = (char)('A'+pos-26);else curChar = (char)('0'+pos-26-26);sb.append(curChar);}}return sb.toString();}
這樣調(diào)用visualizableHash(hash(str))就能獲得一個(gè)還可以的文本哈希了
PART C util方法封裝
其中payload就是我要負(fù)載的內(nèi)容
Sign作為簽名校驗(yàn)
目前是使用簡(jiǎn)單的String,也提供了簡(jiǎn)單的Map轉(zhuǎn)換
格式見(jiàn)doc說(shuō)明
package com.noresp.oj.utils;import java.util.*;/*** 使用Token,解放Session* 注:一個(gè)Token的格式* [encode(key1).encode(val1).encode(key2).encode(val2).....mySign]* 目前encode默認(rèn)是base64*/
public class TokenUtils {private static String encode(String str) {if(str == null || str.length() == 0) return "";return Base64.getEncoder().encodeToString(str.getBytes());}private static String decode(String str) {if(str == null || str.length() == 0) return "";return new String(Base64.getDecoder().decode(str.getBytes()));}public static String getTokenPayload(String key) {return encode(key);}public static String getTokenSign(String... base64Payloads) {StringBuilder sb = new StringBuilder("");for(String payload : base64Payloads) {sb.append(StringUtils.visualizableHash(AlgsUtils.hash(payload)));}return sb.toString();}public static String getToken(String... payloads) {StringBuilder sb = new StringBuilder("");String[] encodedPayloads = new String[payloads.length];for(int i = 0; i < payloads.length; i++) {encodedPayloads[i] = getTokenPayload(payloads[i]);sb.append(encodedPayloads[i]+".");}sb.append(getTokenSign(encodedPayloads));return sb.toString();}/*** 解密和校驗(yàn)Token* @param token* @return 如果校驗(yàn)失敗,會(huì)返回null,否則返回Token解密內(nèi)容*/public static String[] decodeTokenAndValidate(String token) {if(token == null) return null;List<String> result = new LinkedList<>();for(int i = 0, len = 1; i < token.length(); i++,len++) {if(token.charAt(i) == '.') {String payload = (token.substring(i-len+1,i));result.add(payload);len = 0;}if(i == token.length()-1) {String salt = token.substring(i-len+1,i+1); len = 0;String[] encodedPayloads = new String[result.size()];Iterator<String> itor = result.iterator();while(itor.hasNext()) {encodedPayloads[len++] = itor.next();}String comp = getTokenSign(encodedPayloads);if(!salt.equals(comp)) {return null;}String[] decodedPayloads = encodedPayloads; // 引用是一樣的for(len = 0; len < encodedPayloads.length; len++) {decodedPayloads[len] = decode(encodedPayloads[len]);}return decodedPayloads;}}return null;}public static Map<String,String> tokenMap(String[] decodedToken) {Map<String,String> result = new HashMap<>();if(decodedToken == null) return result;for(int i = 0; i < decodedToken.length; i+=2) {result.put(decodedToken[i],decodedToken[i+1]);}return result;}public static String getTokenAttribute(String token,String key) {Map<String,String> tokenMap = tokenMap(decodeTokenAndValidate(token));return tokenMap.getOrDefault(key,null);}
}
PART D 應(yīng)用于WEB
目前用于token的payload有userid和ip,后者是為了進(jìn)一步提高安全性
并且token是直接放在Cookie里頭,方便管理生命周期
寫(xiě)得比較雜亂,先貼部分感受一下吧
@PostMapping("/register")public @ResponseBody String registerPost(HttpServletRequest request, HttpServletResponse response,@RequestParam(value = "email") String email,@RequestParam(value = "username") String username,@RequestParam(value = "password") String password) throws IOException {Boolean isCreated =userService.createUser(username,password,email,userService.getDefaultUserGroup());Map<String,Boolean> result = new HashMap<>();result.put("isCreated",isCreated);if(isCreated) {String token = TokenUtils.getToken("userID",String.valueOf(userService.getUserByUsername(username).getUserID()),"ip",controllerUtils.getRemoteAddr(request));Cookie cookie = new Cookie("token",token);cookie.setMaxAge(60*60*24*7);cookie.setHttpOnly(true);response.addCookie(cookie);}return JSONUtils.toJSON(result);}
其中g(shù)etRemoteAddr的實(shí)現(xiàn)為
public String getRemoteAddr(HttpServletRequest request) {if ( request.getHeader("X-Real-IP") != null ) {return request.getHeader("X-Real-IP");}return request.getRemoteAddr();}
PART E 更方便的使用
校驗(yàn)過(guò)程太繁瑣了,當(dāng)然要用到AOP,這里采用注解的方式來(lái)實(shí)現(xiàn)
1.先給一個(gè)注解標(biāo)記
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NeedLogin {
}
2.接著就是AOP
(請(qǐng)無(wú)視直接println
@Component
@Aspect
public class LoginAspect {@AutowiredControllerUtils controllerUtils;/*** 【約定大于配置】* 當(dāng)需要使用@NeedLogin時(shí),token需作為入?yún)⒌牡谝粋€(gè)保證AOP成功攔截* Token校驗(yàn)包括了加鹽的檢驗(yàn)和IP的對(duì)比,以及開(kāi)啟HttpOnly安全設(shè)置* 如果有錯(cuò)會(huì)及時(shí)把劫持的Cookie刪除 // PS.有點(diǎn)小瑕疵* @param proceedingJoinPoint* @param token* @return* @throws Throwable*/@Around(value = "@annotation(com.noresp.oj.annotations.NeedLogin) && args(token,request,response,..)")public ModelAndView loginCheck(ProceedingJoinPoint proceedingJoinPoint,String token,HttpServletRequest request,HttpServletResponse response) throws Throwable {if(token == null) {System.out.println("沒(méi)有token");return ViewUtils.redirect("/",new ErrorInfo("login required"));}Map<String,String> tokenMap = TokenUtils.tokenMap(TokenUtils.decodeTokenAndValidate(token));Integer userID = StringUtils.safeStringToInteger(tokenMap.get("userID"));String recordedIP = tokenMap.get("ip");System.out.println(userID+" "+recordedIP);boolean tokenIllegal =userID == null || !controllerUtils.getRemoteAddr(request).equals(recordedIP);if(tokenIllegal) {System.out.println("token錯(cuò)誤");Cookie fakeToken = controllerUtils.getCookie(request,"token");if(fakeToken != null) {fakeToken.setMaxAge(0);}return ViewUtils.redirect("/",new ErrorInfo("login required"));}return (ModelAndView)proceedingJoinPoint.proceed();}
}
3.使用樣例
需要注意AOP沒(méi)有很好的arg通配方法,這里使用的規(guī)約見(jiàn)上面定義
@NeedLogin@GetMapping("/{problemID}/submit")public ModelAndView submitView(@CookieValue(value = "token",required = false) String token,HttpServletRequest request,HttpServletResponse response,@PathVariable("problemID") int problemID) {ModelAndView view = new ModelAndView("/problems/submit");Problem problem = problemService.getProblem(problemID);if(problem == null) {return ViewUtils.redirect("/",new ErrorInfo("No Such Problem."));}view.addObject("problem",problem);return view;}
目前的不足
1.token長(zhǎng)度受限于Cookie
2.校驗(yàn)的復(fù)雜度還是大了點(diǎn)
轉(zhuǎn)載于:https://www.cnblogs.com/caturra/p/11206484.html
總結(jié)
以上是生活随笔為你收集整理的手动设计简单的Token验证的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。