記錄先後端分離的系統應用下應用場景————用戶信息傳遞html
照例先看看web
系統的一張經典架構圖,這張圖參考自網絡:java
在 Dubbo 自定義異常,你是怎麼處理的? 中已經對該架構作了簡單說明,這裏再也不描述。web
簡單描述下在該架構中用戶信息(如userId)的傳遞方式
:算法
如今絕大多數的項目都是先後端分離的開發模式,採用token
方式進行用戶鑑權:spring
token
,在token
中放入用戶信息(如userId)
等返回給客戶端token
,跟表單一併提交到服務端web
層統一解析token
鑑權,同時取出用戶信息(如userId)
並繼續向底層傳遞,傳到服務層操做業務邏輯service
層取到用戶信息(如userId)
後,執行相應的業務邏輯操做問題:apache
爲何必定要把用戶信息(如userId)
藏在token
中,服務端再解析token
取出?直接登陸後向客戶端返回用戶信息(如userId)
不是更方便麼?json
跟用戶強相關的信息是至關敏感的,通常用戶信息(如userId)
不會直接明文暴露給客戶端,會帶來風險。後端
用戶信息(如userId)
的傳遞流程什麼是單體應用? 簡要描述就是web
層,service
層所有在一個jvm
進程中,更通俗的講就是隻有一個項目
。網絡
看看下面的登陸接口僞代碼:架構
web
層接口:
@Loggable(descp = "用戶登陸", include = "loginParam") @PostMapping("/login") public BaseResult<LoginVo> accountLogin(LoginParam loginParam) { return mAccountService.login(loginParam); }
service
層接口僞代碼:
public BaseResult<LoginVo> login(LoginParam param) throws BaseException { //1.登陸邏輯判斷 LoginVo loginVo = handleLogin(param); //2.簽發token String subject = userId; String jwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(), subject, "token-server", BaseConstants.TOKEN_PERIOD_TIME, "", null, SignatureAlgorithm.HS512); loginVo.setJwt(jwt); return ResultUtil.success(loginVo); }
注意到上述僞代碼中,簽發token
時把userId
放入客戶標識subject
中,簽發到token
中返回給客戶端。這裏使用的是JJWT
生成的token
引入依賴:
<!--jjwt--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.8.9</version> </dependency>
相關工具類JsonWebTokenUtil
:
public class JsonWebTokenUtil { //祕鑰 public static final String SECRET_KEY = BaseConstant.SECRET_KEY; private static final ObjectMapper MAPPER = new ObjectMapper(); private static CompressionCodecResolver codecResolver = new DefaultCompressionCodecResolver(); //私有化構造 private JsonWebTokenUtil() { } /* * * @Description json web token 簽發 * @param id 令牌ID * @param subject 用戶標識 * @param issuer 簽發人 * @param period 有效時間(秒) * @param roles 訪問主張-角色 * @param permissions 訪問主張-權限 * @param algorithm 加密算法 * @Return java.lang.String */ public static String issueJWT(String id,String subject, String issuer, Long period, String roles, String permissions, SignatureAlgorithm algorithm) { // 當前時間戳 Long currentTimeMillis = System.currentTimeMillis(); // 祕鑰 byte[] secreKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY); JwtBuilder jwtBuilder = Jwts.builder(); if (StringUtils.isNotBlank(id)) { jwtBuilder.setId(id); } if (StringUtils.isNotBlank(subject)) { jwtBuilder.setSubject(subject); } if (StringUtils.isNotBlank(issuer)) { jwtBuilder.setIssuer(issuer); } // 設置簽發時間 jwtBuilder.setIssuedAt(new Date(currentTimeMillis)); // 設置到期時間 if (null != period) { jwtBuilder.setExpiration(new Date(currentTimeMillis + period*1000)); } if (StringUtils.isNotBlank(roles)) { jwtBuilder.claim("roles",roles); } if (StringUtils.isNotBlank(permissions)) { jwtBuilder.claim("perms",permissions); } // 壓縮,可選GZIP jwtBuilder.compressWith(CompressionCodecs.DEFLATE); // 加密設置 jwtBuilder.signWith(algorithm,secreKeyBytes); return jwtBuilder.compact(); } /** * 解析JWT的Payload */ public static String parseJwtPayload(String jwt){ Assert.hasText(jwt, "JWT String argument cannot be null or empty."); String base64UrlEncodedHeader = null; String base64UrlEncodedPayload = null; String base64UrlEncodedDigest = null; int delimiterCount = 0; StringBuilder sb = new StringBuilder(128); for (char c : jwt.toCharArray()) { if (c == '.') { CharSequence tokenSeq = io.jsonwebtoken.lang.Strings.clean(sb); String token = tokenSeq!=null?tokenSeq.toString():null; if (delimiterCount == 0) { base64UrlEncodedHeader = token; } else if (delimiterCount == 1) { base64UrlEncodedPayload = token; } delimiterCount++; sb.setLength(0); } else { sb.append(c); } } if (delimiterCount != 2) { String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount; throw new MalformedJwtException(msg); } if (sb.length() > 0) { base64UrlEncodedDigest = sb.toString(); } if (base64UrlEncodedPayload == null) { throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload."); } // =============== Header ================= Header header = null; CompressionCodec compressionCodec = null; if (base64UrlEncodedHeader != null) { String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader); Map<String, Object> m = readValue(origValue); if (base64UrlEncodedDigest != null) { header = new DefaultJwsHeader(m); } else { header = new DefaultHeader(m); } compressionCodec = codecResolver.resolveCompressionCodec(header); } // =============== Body ================= String payload; if (compressionCodec != null) { byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload)); payload = new String(decompressed, io.jsonwebtoken.lang.Strings.UTF_8); } else { payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload); } return payload; } /** * 驗籤JWT * * @param jwt json web token */ public static JwtAccount parseJwt(String jwt, String appKey) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(appKey)) .parseClaimsJws(jwt) .getBody(); JwtAccount jwtAccount = new JwtAccount(); //令牌ID jwtAccount.setTokenId(claims.getId()); //客戶標識 String subject = claims.getSubject(); jwtAccount.setSubject(subject); //用戶id jwtAccount.setUserId(subject); //簽發者 jwtAccount.setIssuer(claims.getIssuer()); //簽發時間 jwtAccount.setIssuedAt(claims.getIssuedAt()); //接收方 jwtAccount.setAudience(claims.getAudience()); //訪問主張-角色 jwtAccount.setRoles(claims.get("roles", String.class)); //訪問主張-權限 jwtAccount.setPerms(claims.get("perms", String.class)); return jwtAccount; } public static Map<String, Object> readValue(String val) { try { return MAPPER.readValue(val, Map.class); } catch (IOException e) { throw new MalformedJwtException("Unable to userpager JSON value: " + val, e); } } }
JWT
相關實體JwtAccount
:
@Data public class JwtAccount implements Serializable { private static final long serialVersionUID = -895875540581785581L; /** * 令牌id */ private String tokenId; /** * 客戶標識(用戶id) */ private String subject; /** * 用戶id */ private String userId; /** * 簽發者(JWT令牌此項有值) */ private String issuer; /** * 簽發時間 */ private Date issuedAt; /** * 接收方(JWT令牌此項有值) */ private String audience; /** * 訪問主張-角色(JWT令牌此項有值) */ private String roles; /** * 訪問主張-資源(JWT令牌此項有值) */ private String perms; /** * 客戶地址 */ private String host; public JwtAccount() { } }
web
層統一鑑權,解析token
客戶端訪問服務端接口,須要在頭部攜帶token
,跟表單一併提交到服務端,服務端則在web
層新增MVC
攔截器統一作處理
新增MVC
攔截器以下:
public class UpmsInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { BaseResult result = null; //獲取請求uri String requestURI = request.getRequestURI(); ...省略部分邏輯 //獲取認證token String jwt = request.getHeader(BaseConstant.AUTHORIZATION); //不傳認證token,判斷爲無效請求 if (StringUtils.isBlank(jwt)) { result = ResultUtil.error(ResultEnum.ERROR_REQUEST); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } //其餘請求均需驗證token有效性 JwtAccount jwtAccount = null; String payload = null; try { // 解析Payload payload = JsonWebTokenUtil.parseJwtPayload(jwt); //取出payload中字段信息 if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { Map<String, Object> payloadMap = JsonWebTokenUtil.readValue(payload); //客戶標識(userId) String subject = (String) payloadMap.get("sub"); //查詢用戶簽發祕鑰 } //驗籤token jwtAccount = JsonWebTokenUtil.parseJwt(jwt, JsonWebTokenUtil.SECRET_KEY); } catch (SignatureException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) { //令牌錯誤 result = ResultUtil.error(ResultEnum.ERROR_JWT); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } catch (ExpiredJwtException e) { //令牌過時 result = ResultUtil.error(ResultEnum.EXPIRED_JWT); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } catch (Exception e) { //解析異常 result = ResultUtil.error(ResultEnum.ERROR_JWT); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } if (null == jwtAccount) { //令牌錯誤 result = ResultUtil.error(ResultEnum.ERROR_JWT); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } //將用戶信息放入threadLocal中,線程共享 ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId()); return true; } //...省略部分代碼 }
整個token
解析過程已經在代碼註釋中說明,能夠看到解析完token
後取出userId
,將用戶信息放入了threadLocal
中,關於threadLocal
的用法,本文暫不討論.
//將用戶信息放入threadLocal中,線程共享 ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());
添加配置使攔截器生效:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ...省略部分代碼"> <!-- web攔截器 --> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <bean class="com.easywits.upms.client.interceptor.UpmsInterceptor"/> </mvc:interceptor> </mvc:interceptors> </beans>
相關工具代碼ThreadLocalUtil
:
public class ThreadLocalUtil { private ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>(); //new一個實例 private static final ThreadLocalUtil instance = new ThreadLocalUtil(); //私有化構造 private ThreadLocalUtil() { } //獲取單例 public static ThreadLocalUtil getInstance() { return instance; } /** * 將用戶對象綁定到當前線程中,鍵爲userInfoThreadLocal對象,值爲userInfo對象 * * @param userInfo */ public void bind(UserInfo userInfo) { userInfoThreadLocal.set(userInfo); } /** * 將用戶數據綁定到當前線程中,鍵爲userInfoThreadLocal對象,值爲userInfo對象 * * @param companyId * @param userId */ public void bind(String userId) { UserInfo userInfo = new UserInfo(); userInfo.setUserId(userId); bind(userInfo); } /** * 獲得綁定的用戶對象 * * @return */ public UserInfo getUserInfo() { UserInfo userInfo = userInfoThreadLocal.get(); remove(); return userInfo; } /** * 移除綁定的用戶對象 */ public void remove() { userInfoThreadLocal.remove(); } }
那麼在web
層和service
均可以這樣拿到userId
:
@Loggable(descp = "用戶我的資料", include = "") @GetMapping(value = "/info") public BaseResult<UserInfoVo> userInfo() { //拿到用戶信息 UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo(); return mUserService.userInfo(); }
service
層獲取userId
:
public BaseResult<UserInfoVo> userInfo() throws BaseException { //拿到用戶信息 UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo(); UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId); return ResultUtil.success(userInfoVo); }
用戶信息(如userId)
的傳遞流程分佈式應用與單體應用最大的區別就是從單個應用拆分紅多個應用,service
層與web
層分爲兩個獨立的應用,使用rpc
調用方式處理業務邏輯。而上述作法中咱們將用戶信息放入了threadLocal
中,是相對單應用進程而言的,假如service
層接口在另一個服務進程中,那麼將獲取不到。
有什麼辦法能解決跨進程傳遞用戶信息呢?翻看了下Dubbo
官方文檔,有隱式參數
功能:
文檔很清晰,只須要在web
層統一的攔截器中調用以下代碼,就能將用戶id
傳到service
層
RpcContext.getContext().setAttachment("userId", xxx);
相應地調整web
層攔截器代碼:
public class UpmsInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //...省略部分代碼 //將用戶信息放入threadLocal中,線程共享 ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId()); //將用戶信息隱式透傳到服務層 RpcContext.getContext().setAttachment("userId", jwtAccount.getUserId()); return true; } //...省略部分代碼 }
那麼服務層能夠這樣獲取用戶id
了:
public BaseResult<UserInfoVo> userInfo() throws BaseException { //拿到用戶信息 String userId = RpcContext.getContext().getAttachment("userId"); UserInfoVo userInfoVo = getUserInfoVo(userId); return ResultUtil.success(userInfoVo); }
爲了便於統一管理,咱們能夠在service
層攔截器中將獲取到的userId
再放入threadLocal
中,service
層攔截器能夠看看這篇推文:Dubbo自定義日誌攔截器
public class DubboServiceFilter implements Filter { private static final Logger LOGGER = LoggerFactory.getLogger(DubboServiceFilter.class); @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { //...省略部分邏輯 //獲取web層透傳過來的用戶參數 String userId = RpcContext.getContext().getAttachment("userId"); //放入全局threadlocal 線程共享 if (StringUtils.isNotBlank(userId)) { ThreadLocalUtil.getInstance().bind(userId); } //執行業務邏輯 返回結果 Result result = invoker.invoke(invocation); //清除 防止內存泄露 ThreadLocalUtil.getInstance().remove(); //...省略部分邏輯 return result; } }
這樣處理,service
層依然能夠經過以下代碼獲取用戶信息了:
public BaseResult<UserInfoVo> userInfo() throws BaseException { //拿到用戶信息 UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo(); UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId); return ResultUtil.success(userInfoVo); }
關於jwt
:https://blog.leapoahead.com/2015/09/06/understanding-jwt/
關於dubbo
:http://dubbo.apache.org/zh-cn/docs/user/demos/attachment.html
篇幅較長,總結一個較爲實用的web
應用場景,後續會不按期更新原創文章,歡迎關注公衆號 「張少林同窗」!