人的知识就好比一个圆圈,圆圈里面是已知的,圆圈外面是未知的。你知道得越多,圆圈也就越大,你不知道的也就越多。

0%

第三方开放平台登录授权设计与实现

数据表设计

公司信息表

字段名 字段描述
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类图

接口实现UML类图

  • ThirdAccountController:第三方账号 Controller 抽象类
  • JdAccountController:京东账号 Controller 实现类,注入京东账号 Service
  • SnAccountController:苏宁账号 Controller 实现类,注入苏宁账号 Service
  • ThirdAccountService:第三方账号 Service 抽象类
  • JdAccountService:京东账号 Service 实现类
  • SnAccountService:苏宁账号 Service 实现类

Token 请求流程

第三方token请求流程

Token 初始化与定时刷新

Token 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 第三方帐号 Service
*
* @author cdrcool
*/
@Slf4j
public abstract class ThirdAccountService {
/**
* 应用启动后,初始化所有的第三方token
*/
@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
/**
* 第三方token刷新定时任务
*
* @author cdrcool
*/
@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
/**
* 第三方帐号 Controller
*
* @author cdrcool
*/
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);

// 请求第三方token
thirdAccountService.requestAccessToken(taxpayerId, deferredResult);

// 超时回调
deferredResult.onTimeout(() -> {
throw new ThirdAccountException("请求第三方token超时,请稍后重试");
});
// 失败回调
deferredResult.onError(e -> {
throw new ThirdAccountException("请求第三方token异常:" + e.getMessage());
});

// 等待第三方回调应用 callback-token 接口,在 callback-token 接口里获取到token之后,再调用 deferredResult.setResult(token)
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
/**
* 第三方帐号 Service
*
* @author cdrcool
*/
@Slf4j
public abstract class ThirdAccountService {
private static final Map<String, TokenRequestInfo> STORE = new ConcurrentHashMap<>();

/**
* 请求第三方token
*
* @param taxpayerId 纳税人识别号
* @param deferredResult {@link DeferredResult}
*/
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());
// 保存当前请求的deferredResult,在token回调成功后对其setResult
tokenRequestInfo.setDeferredResult(deferredResult);
STORE.put(state, tokenRequestInfo);
}

/**
* 获取第三方token回调
*
* @param code 授权码
* @param state 回传state(与传递给第三方的state一致)
*/
@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());

// 将第三方帐号及其对应的token存到redis缓存
redisTemplate.opsForValue().set(tokenRequestInfo.getAccount(), response.getAccessToken(),
System.currentTimeMillis() - (response.getTime() + response.getExpiresIn() * 1000), TimeUnit.MICROSECONDS);

// 查找第三方帐号,并更新其对应的token响应
LambdaQueryWrapper<ThirdAccount> jdAccountWrapper = Wrappers.lambdaQuery();
jdAccountWrapper.eq(ThirdAccount::getAccount, tokenRequestInfo.getAccount());
ThirdAccount thirdAccount = thirdAccountMapper.selectOne(jdAccountWrapper);
if (thirdAccount == null) {
throw new ThirdAccountException("未找到第三方帐号:" + tokenRequestInfo.getAccount());
}
// 更新第三方账号对应的token信息
updateThirdAccount(thirdAccount, response);

DeferredResult<String> deferredResult = tokenRequestInfo.getDeferredResult();
if (deferredResult != null) {
// 将token设置到deferredResult里
deferredResult.setResult(response.getAccessToken());
}
}
}
小礼物走一走,来 Github 关注我