数据表设计
公司信息表
字段名 |
字段描述 |
id |
主键 |
company_name |
公司名称 |
taxpayer_id |
纳税人识别号 |
company_phone |
公司电话 |
company_registered_address |
公司注册地址 |
bank_name |
开户行名称 |
bank_account |
开户行账号 |
jd_account |
京东账号名 |
sn_account |
苏宁账号名 |
第三方账号表
字段名 |
字段描述 |
id |
主键 |
account_type |
账号类型(jd-京东;sn-苏宁) |
account |
账号名 |
password |
账号密码 |
access_token |
访问令牌 |
refresh_token |
刷新令牌 |
access_token_expires_at |
访问令牌过期时间 |
refresh_token_expires_at |
刷新令牌过期时间 |
第三方账号,除了账号名、账号密码之外,还有App Key、App Secret等关键信息,这些信息都是一样的,就保存在配置文件中(也可以保存在数据库里),如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| sdk: accounts: jd: auth-url: server-url: account: password: app-key: app-secret: redirect-uri: rsa-key: sn: auth-url: server-url: account: password: app-key: app-secret: redirect-uri: rsa-key:
|
接口设计
请求第三方token
请求地址:POST /{account-type}-account/request-access-token
请求参数:
参数名 |
参数描述 |
taxpayerId |
纳税人识别号 |
timeout |
超时时间(单位:秒),默认值 3 |
如果未传递 taxpayerId,则请求默认的第三方token;如果在指定时间内,第三方未返回token,则抛出错误提示:请求第三方token超时,请稍后重试。
请求响应:token字符串
第三方回调地址
调用请求第三方token接口后,由第三方回调该接口。
请求地址:GET /{account-type}-account/callback-token
请求参数:
参数名 |
参数描述 |
code |
第三方返回的鉴权码 |
state |
第三方回传的state(与传递给第三方的保持一致) |
请求响应:token字符串
刷新第三方token
请求地址:POST /{account-type}-account/refresh-token
请求参数:
参数名 |
参数描述 |
taxpayerId |
纳税人识别号 |
如果未传递 taxpayerId,则请求默认的第三方token。
请求响应:token字符串
获取第三方token
请求地址:POST /{account-type}-account/get-access-token
请求参数:
参数名 |
参数描述 |
taxpayerId |
纳税人识别号 |
timeout |
超时时间(单位:秒),默认值 3 |
如果未传递 taxpayerId,则请求默认的第三方token;如果在指定时间内,未返回第三方token,则抛出错误提示:获取第三方token超时,请稍后重试。
请求响应:token字符串
初始化所有的第三方token
请求地址:POST /{account-type}-account/init-all-token
刷新所有的第三方token
请求地址:POST /{account-type}-account/refresh-all-token
接口实现-UML类图
- ThirdAccountController:第三方账号 Controller 抽象类
- JdAccountController:京东账号 Controller 实现类,注入京东账号 Service
- SnAccountController:苏宁账号 Controller 实现类,注入苏宁账号 Service
- ThirdAccountService:第三方账号 Service 抽象类
- JdAccountService:京东账号 Service 实现类
- SnAccountService:苏宁账号 Service 实现类
Token 请求流程
Token 初始化与定时刷新
Token 初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@Slf4j public abstract class ThirdAccountService {
@PostConstruct public void postConstruct() { initAllToken(); } }
|
Token 定时刷新
每天凌晨(可配置)自动刷新所有第三方账号的token。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
@Slf4j @Component public class ThirdTokenRefreshTask { private final Map<String, ThirdAccountService> thirdAccountServices;
public ThirdTokenRefreshTask(Map<String, ThirdAccountService> thirdAccountServices) { this.thirdAccountServices = thirdAccountServices; }
@Scheduled(cron = "${sdk.refresh-token-cron}") public void execute() { thirdAccountServices.values().forEach(thirdAccountService -> { try { thirdAccountService.refreshAllToken(); } catch (Exception e) { log.error("定时刷新第三方token异常,class name:{}", thirdAccountService.getClass().getSimpleName()); } }); } }
|
1 2
| sdk: refresh-token-cron: 0 0 0 * * ?
|
异步返回 Token
使用DeferredResult
(异步 Servlet)来异步返回 token。
在DeferredResult
未拿到返回数据之前,DispatcherServlet
和所有的Filter
会退出Servlet
容器线程并释放其资源,同时也允许容器去处理其它请求,但响应保持打开状态。 一旦DeferredResult
拿到返回数据,DispatcherServlet
会被再次调用并处理,以异步产生的方式,向请求端返回值。这么做的好处就是请求不会长时间占用容器连接池,提高服务器的吞吐量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public abstract class ThirdAccountController {
@ApiOperation("请求第三方token(未传递纳税人识别号,则请求的默认第三方token)") @PostMapping("/request-access-token") public DeferredResult<String> requestAccessToken(@ApiParam("纳税人识别号") String taxpayerId, @ApiParam(value = "超时时间(单位:秒)", defaultValue = "3") @RequestParam(defaultValue = "3") Integer timeout) { DeferredResult<String> deferredResult = new DeferredResult<>(timeout.longValue() * 1000);
thirdAccountService.requestAccessToken(taxpayerId, deferredResult);
deferredResult.onTimeout(() -> { throw new ThirdAccountException("请求第三方token超时,请稍后重试"); }); deferredResult.onError(e -> { throw new ThirdAccountException("请求第三方token异常:" + e.getMessage()); });
return deferredResult; } }
|
Oauth 2.0 state 参数
在请求第三方code时,还需要传递state参数,它是由client使用的不透明参数,用于请求阶段和回调阶段之间的状态保持。请求示例:
1
| https://open-oauth.jd.com/oauth2/authorizeForVOP?app_key=xxx&redirect_uri=xxx&username=xxx&password=xxx&response_type=code&scope=snsapi_base&state=xxx
|
state可以是一个随机字符串,然后保存在内存里,回调时检查state参数和内存里的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
@Slf4j public abstract class ThirdAccountService { private static final Map<String, TokenRequestInfo> STORE = new ConcurrentHashMap<>();
public void requestAccessToken(String taxpayerId, DeferredResult<String> deferredResult) { ThirdAccount thirdAccount = getThirdAccount(taxpayerId); String state = UUID.randomUUID().toString(); requestAccessToken(thirdAccount, state);
TokenRequestInfo tokenRequestInfo = new TokenRequestInfo(state, accountType(), thirdAccount.getAccount()); tokenRequestInfo.setDeferredResult(deferredResult); STORE.put(state, tokenRequestInfo); }
@Async public void callbackToken(String code, String state) { TokenResponse response = getTokenResponse(code);
TokenRequestInfo tokenRequestInfo = STORE.get(state); if (tokenRequestInfo == null) { throw new ThirdAccountException("未找到与state:{}对应的token请求信息:" + state); } log.info("找到state:{}对应的第三方帐号:{}", state, tokenRequestInfo.getAccount());
redisTemplate.opsForValue().set(tokenRequestInfo.getAccount(), response.getAccessToken(), System.currentTimeMillis() - (response.getTime() + response.getExpiresIn() * 1000), TimeUnit.MICROSECONDS);
LambdaQueryWrapper<ThirdAccount> jdAccountWrapper = Wrappers.lambdaQuery(); jdAccountWrapper.eq(ThirdAccount::getAccount, tokenRequestInfo.getAccount()); ThirdAccount thirdAccount = thirdAccountMapper.selectOne(jdAccountWrapper); if (thirdAccount == null) { throw new ThirdAccountException("未找到第三方帐号:" + tokenRequestInfo.getAccount()); } updateThirdAccount(thirdAccount, response);
DeferredResult<String> deferredResult = tokenRequestInfo.getDeferredResult(); if (deferredResult != null) { deferredResult.setResult(response.getAccessToken()); } } }
|