【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":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章