【HZERO微服務平臺3】源碼分析之oauth服務token生成、校驗、獲取信息、傳遞

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"概述","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"hzero-oauth","attrs":{}}],"attrs":{}},{"type":"text","text":" 服務是基於 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"spring security","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"spring security oauth","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"JWT","attrs":{}}],"attrs":{}},{"type":"text","text":" 實現的統一認證服務中心,支持 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"oauth2.0","attrs":{}}],"attrs":{}},{"type":"text","text":" 的四種授權模式:","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"授權碼模式","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"簡化模式","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"密碼模式","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"客戶端模式","attrs":{}}],"attrs":{}},{"type":"text","text":",授權流程跟標準的 oauth2 流程一致。web 端採用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"簡化模式(implicit)","attrs":{}}],"attrs":{}},{"type":"text","text":"登錄系統,移動端可使用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"密碼模式(password)","attrs":{}}],"attrs":{}},{"type":"text","text":"登錄系統 。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完整的功能介紹: ","attrs":{}},{"type":"link","attrs":{"href":"https://open.hand-china.com/hzero-docs/v1.3/zh/docs/service/oauth/","title":"","type":null},"content":[{"type":"text","text":"認證服務","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"深入瞭解hzero-oauth需要熟練使用spring security oauth, 簡單描述一下它的功能:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"oauth2是開放的標準協議, spring security oauth提供了實現, 授權中心(authorization server)用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"@EnableAuthorizationServer","attrs":{}}],"attrs":{}},{"type":"text","text":"及相關配置實現, 資源服務(resource server)用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"@EnableResourceServer","attrs":{}}],"attrs":{}},{"type":"text","text":"及相關配置實現;授權中心提供授權(/oauth/authorize)、獲取token(/oauth/token)等接口, 資源服務實現對token的校驗、信息提取; hzero的oauth服務既是授權中心也是資源服務;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"資料:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OAuth2.0的RFC文檔: ","attrs":{}},{"type":"link","attrs":{"href":"https://tools.ietf.org/html/rfc6749","title":"","type":null},"content":[{"type":"text","text":"RFC 6749 - The OAuth 2.0 Authorization Framework","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"spring官方開發文檔: ","attrs":{}},{"type":"link","attrs":{"href":"https://projects.spring.io/spring-security-oauth/docs/oauth2.html","title":"","type":null},"content":[{"type":"text","text":"OAuth 2 Developers Guide","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.cnblogs.com/QIAOXINGXING001/p/15571809.html","title":"","type":null},"content":[{"type":"text","text":"如何以純文本方式快速記錄java代碼的調用過程","attrs":{}}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"服務間token的傳遞過程流程圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前端使用oauth2流程獲取token(uuid格式), 之後的請求必須攜帶token, token在服務間傳遞的示意圖:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c1/c1b5fad74ca4e9166ca5f96ace84e39c.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"前端獲取的uuid格式的token(相當於sessionId), 傳遞給網關;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"網關使用uuid token獲取用戶信息, 把用戶信息轉換jwt token, 並添加到","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"jwt_token","attrs":{}}],"attrs":{}},{"type":"text","text":"header裏, 傳遞到後端服務; 如果獲取用戶信息失敗, 直接返回401(認證失敗);","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"後端服務從jwt_token裏解析、獲取用戶信息;","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"從oauth服務獲取token的過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"post /oauth/token","attrs":{}}],"attrs":{}},{"type":"text","text":"接口獲取token的過程:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"TokenEndpoint#postAccessToken\nOAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); //責任鏈模式, 每種授權模式對應一個granter\nAbstractTokenGranter#grant\nClientDetails client = clientDetailsService.loadClientByClientId(clientId);\nAbstractTokenGranter#getAccessToken\nreturn tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));\n AbstractTokenGranter#getOAuth2Authentication //這個方法會被子類granter覆寫\n return new OAuth2Authentication(storedOAuth2Request, null); \nDefaultTokenServices#createAccessToken(OAuth2Authentication) //hzero修改版\nDefaultTokenServices#createAccessToken(OAuth2Authentication , OAuth2RefreshToken) //hzero修改版\nreturn accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調試技巧: 在最內層的方法上打斷點, 看調用堆棧;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"\"hzero修改版\"表示: hzero直接把spring的某些代碼保留包名、類名複製到了項目裏, 相當於直接替換了源碼, 一種不太好的hack方法;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"AbstractTokenGranter","attrs":{}}],"attrs":{}},{"type":"text","text":"的子類","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"AuthorizationCodeTokenGranter、ImplicitTokenGranter、ClientCredentialsTokenGranter、ResourceOwnerPasswordTokenGranter","attrs":{}}],"attrs":{}},{"type":"text","text":"分別對應四種授權模式, 可以增加新的Granter, 優雅的實現新的認證方式.","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調用過程的閱讀方式: ","attrs":{}},{"type":"link","attrs":{"href":"https://www.cnblogs.com/QIAOXINGXING001/p/15571809.html","title":"","type":null},"content":[{"type":"text","text":"如何以純文本方式快速記錄java代碼的調用過程","attrs":{}}]}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"oauth服務校驗token的過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"攜帶token調用接口時, 對token的檢驗過程:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"OAuth2AuthenticationProcessingFilter#doFilter\nAuthentication authResult = authenticationManager.authenticate(authentication);\nOAuth2AuthenticationManager#authenticate\nOAuth2Authentication auth = tokenServices.loadAuthentication(token);\nDefaultTokenServices#loadAuthentication\nOAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);\nCustomRedisTokenStore#readAccessToken //從redis裏讀取、反序列化\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"如果快過期, 自動延長有效時間;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"DefaultTokenServices#loadAuthentication","attrs":{}}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"//如果快過期, 自動增加有效時間;\nif (accessToken.getExpiresIn() < 3600) {\n Long deltaMs = 4 * 3600 * 1000L; //4小時, 單位是毫秒;\n ((DefaultOAuth2AccessToken) accessToken).setExpiration(new Date(System.currentTimeMillis() + deltaMs));\n tokenStore.storeAccessToken(accessToken, result);\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"gateway獲取用戶信息(principal)的過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"gateway把uuid轉換爲jwt是在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"AddJwtFilter","attrs":{}}],"attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶信息最終是oauth服務從","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"CustomRedisTokenStore","attrs":{}}],"attrs":{}},{"type":"text","text":"裏讀取的;","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"gateway服務裏","attrs":{}},{"type":"text","text":":從gateway調用非public的任意接口時:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"GetUserDetailsFilter#run\nCustomUserDetailsWithResult result = this.getUserDetailsService.getUserDetails(accessToken);\nGetUserDetailsServiceImpl#getUserDetails //調用oauth服務的/oauth/api/user\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意: ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"oauth/api/user","attrs":{}}],"attrs":{}},{"type":"text","text":"接口是within接口, 直接從網關調用會報錯: error.permission.withinForbidden","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"xml"},"content":[{"type":"text","text":"PERMISSION_WITH_INerror.permission.withinForbiddenNo access to within interface\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"oauth服務裏","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// oauth/api/user\nOauthController#user\nreturn principal;\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"principal來自SecurityContext, SecurityContext來自OAuth2AuthenticationProcessingFilter:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"OAuth2AuthenticationProcessingFilter#doFilter\nAuthentication authResult = authenticationManager.authenticate(authentication);\n OAuth2AuthenticationManager#authenticate\n OAuth2Authentication auth = tokenServices.loadAuthentication(token);\nSecurityContextHolder.getContext().setAuthentication(authResult);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"additionInfo","attrs":{}}],"attrs":{}},{"type":"text","text":"字段:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"DefaultTokenServices#loadAuthentication的返回結果包含additionInfo, 但序列化的之後不包含, 因爲spring添加了ignore註解;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"principal序列化把additionInfo字段裏信息, 放到了和client_id同級的位置;","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"oauth服務創建用戶信息(principal)的過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"principal來自","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Object SecurityContext.getAuthentication().getPrincipal()","attrs":{}}],"attrs":{}},{"type":"text","text":", Object具體是什麼類型需要看","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"AuthenticationToken","attrs":{}}],"attrs":{}},{"type":"text","text":"設置了什麼值;","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"client_credentials模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"principal是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"CustomClientDetails","attrs":{}}],"attrs":{}},{"type":"text","text":"類型:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"...\nClientCredentialsTokenGranter#grant\nAbstractTokenGranter#grant\nClientDetails client = clientDetailsService.loadClientByClientId(clientId);\n CustomClientDetailsService#loadClientByClientId\n clientDetailsWrapper.warp(clientDetails, client.getId(), client.getOrganizationId()); //角色、租戶等信息來自這裏\nreturn getAccessToken(client, tokenRequest);\nAbstractTokenGranter#getAccessToken\nreturn tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));\nClientCredentialsTokenGranter#getOAuth2Authentication //hzero修改版\nreturn new ClientOAuth2Authentication(storedOAuth2Request, new ClientAuthenticationToken(client)); //new ClientAuthenticationToken(client)的入參client是principal, 是CustomClientDetails\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a49e9e7877838b1e712c81ae1241b0f4.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"password模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"principal是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"CustomUserDetails","attrs":{}}],"attrs":{}},{"type":"text","text":"類型:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"...\nResourceOwnerPasswordTokenGranter#getOAuth2Authentication\nuserAuth = authenticationManager.authenticate(userAuth);\n ProviderManager#authenticate\n AbstractUserDetailsAuthenticationProvider#authenticate\n user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);\n CustomAuthenticationProvider#retrieveUser\n return getUserDetailsService().loadUserByUsername(username);\n CustomUserDetailsService#loadUserByUsername\n return createSuccessAuthentication(principalToReturn, authentication, user);\nreturn new OAuth2Authentication(storedOAuth2Request, userAuth);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f6/f60f44e186a02ee4d3474695ce0b0c2c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"業務服務從jwt_token獲取用戶信息的過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調試思路: 給","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"JwtTokenExtractor","attrs":{}}],"attrs":{}},{"type":"text","text":"打斷點, 看調用堆棧;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"業務服務裏hzero沒有用spring oauth的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"@EnableResourceServer","attrs":{}}],"attrs":{}},{"type":"text","text":", 自定義了","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"JwtTokenFilter","attrs":{}}],"attrs":{}},{"type":"text","text":", 相當於","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"OAuth2AuthenticationProcessingFilter","attrs":{}}],"attrs":{}},{"type":"text","text":"的功能:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"JwtTokenFilter#doFilter\nAuthentication authentication = this.tokenExtractor.extract(httpRequest);\nAuthentication authResult = this.authenticate(authentication);\n JwtTokenFilter#authenticate\n this.tokenServices.loadAuthentication(token); \nSecurityContextHolder.getContext().setAuthentication(authResult);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用方法: 封裝好的方法:","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"DetailsHelper.getUserDetails()","attrs":{}}],"attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章