2019独角兽企业重金招聘Python工程师标准>>>
背景
QBS未来会接入更多第三方应用,当第三方应用访问QBS系统资源时,需要经过QBS认证系统认证,认证通过之后可对QBS资源进行访问。
基于公网的HTTP请求存在被拦截,篡改,重发的可能,需要对用户请求参数信息进行有效保护。
认证流程采用OAuth授权码方式,通过授权码获取access_token,通过access_token结合sign方式对请求入参进行加密。
设计原则:
- 轻量级;
- 适合于异构系统(跨操作系统,多语言简易实现);
- 易于开发;
- 易于测试;
- 易于部署;
- 满足接口安全要求,无过度设计;
名词解释
OAuth
开放协议标准,允许第三方应用访问服务提供者的资源,而不需要将用户名,密码提供给第三方应用
QBS认证系统
独立的QBS资源系统的认证系统,对第三方应用进行备案及授权访问及限流/防刷
QBS资源服务器
题库系统,包含试题,试卷等信息
client_key
每个准备访问QBS资源系统的第三方应用,需要在QBS认证系统进行备案,client_key可以为应用名称
client_secret
第三方应用进行备案时生成的密钥,用于接口请求sign的生成
redirect_url
第三方应用进行备案时配置的应用重定向地址,为安全起见,认证通过重定向到此地址,不依赖请求中的redirect_url
code(授权码)
第三方应用通过获取此授权码进而获取access_token
access_token
第三方应用通过access_token进行QBS的资源访问
refresh_token
当access_token过期之后,可通过refresh_token进行新的access_token获取
sign
签名,用于对请求入参加密,具体生成方法见后面
简要方案
方案详情
应用备案
第三方授权管理配置界面,配置参数: client_key: xxx// 第三方平台名称 client_secret:xxx // 为第三方平台分配的密钥 redirect_url: xxx.com/authxxx // QBS认证系统下发授权码的重定向地址
授权
获取授权码
入参: response_type: code // 固定值 client_key: xxx// 配置约定 state: xxx // 第三方应用当前状态,QBS认证系统不关心,原样返回
响应参数: grant_type: authorization_code // 固定值 code: xxx // 认证系统下发的授权码
获取access_token
入参: grant_type: authorization_code // 固定值 code: xxx // 授权码 client_key: xxx// 配置约定 client_secret: xxx // 配置约定
响应参数: access_token: aaa // 发放的access_token expire_in: 3600 // 过期时间 refresh_token: bbb // access_token过期后,通过refresh_token更新access_token
sign生成规则:
根据参数名=参数值(参数值首尾不能包含空格)的格式,按首字符字典顺序 (ASCII值大小)升序排序。若遇到相同首字符,则判断第二个字符。以此类推,待签名字符串需要。
以参数名1=参数值1&参数名2=参数值2...&参数名N=参数值N的规则进行拼接。 参数首尾加上备案生成的client_secret, 最后得出的字符串进行md5Hex得出sign。
例子:
- 有c=3,b=2,a=1 三个参
- 把参数名和参数值链接组成字符串, a1b2c3timestamp12345678
- 用申请得到的client_secret链接到拼接字符串的头部和尾部,然后进行md5Hex,最后得到加密摘要转换成大写,
示例:md5(client_secreta1b2c3timestamp12345678client_secret),取得md5Hex摘要值 C5F3EB5D7DC2748AED89E90AF00081E6 。
接口入参参考:
例子:
获取试题详情,HTTP请求入参:
{
client_key:xxx,
access_token: xxx //生成的资源访问access_token
sign:xxx // 加密校验值,生成方式见下面
questionId:1 //请求核心参数
}
实体类
# ClientKey
private int id;
private String company;
private String clientKey;
private String clientSecret;
private String redirectUrl;
private Date updateAt;
# 授权码
private int id;
private String clientKey;
private String redirectUrl;
private String code;
private Date createdAt;
private Date expiresAt;
# accessToken
private int id;
private String clientKey;
private String redirectUrl;
private String accessToken;
private String refreshToken;
private Date createdAt;
private Date expiresAt;
private Date refreshTokenExpiresAt;
# refreshToken
private int id;
private String clientKey;
private String refreshToken;
private Date refreshTokenExpiresAt;
获取授权码
/**
* 获取授权码
*
* @param client_key
* @param redirect_url
* @param response_type
* @param state
* @throws IOException
*/
@GetMapping("code")
public RetDTO code(
@RequestParam("response_type") String response_type, // 固定值 code
@RequestParam("client_key") String client_key, // 配置项
@RequestParam("redirect_url") String redirect_url, // 配置项
@RequestParam("state") String state // 客户端状态,原样返回
) {
if (!StringUtils.isEmpty(response_type.trim())
&& ("code").equals(response_type.trim())
&& !StringUtils.isEmpty(client_key.trim())
&& !StringUtils.isEmpty(redirect_url.trim())) {
try {
AuthClientCode code = clientService.createCode(client_key, redirect_url);
if (null != code) { // 20 分钟
String redirectUrl = String.format(REDIRECT_URL, code.getRedirectUrl(), state, code.getCode());
logger.info("response_type: {}, client_key: {}, redirect_url: {}, code: {}", response_type, client_key, redirect_url, code);
return RetDTO.getSuccess(redirectUrl);
}
} catch (Exception e) {
logger.error("response_type: {}, client_key: {}, redirect_url: {}, e: {}", response_type, client_key, redirect_url, e.getMessage());
throw new BizException(e.getMessage());
}
}
logger.info("response_type: {}, client_key: {}, redirect_url: {}, 获取授权码失败", response_type, client_key, redirect_url);
return new RetDTO(HttpStatus.OK.value(), "获取授权码失败");
}
获取access_token
/**
* 获取access_token
*
* @param grant_type
* @param code
* @param client_key
* @param redirect_url
* @return
*/
@PostMapping("access-token")
public RetDTO accessToken(
@RequestParam("grant_type") String grant_type, // 固定值 authorization_code
@RequestParam("code") String code, // 授权码
@RequestParam("client_key") String client_key, // 配置项
@RequestParam("redirect_url") String redirect_url // 配置项
) {
// code,client_key,redirect_uri,确认无误后,发放令牌access_token
if (!StringUtils.isEmpty(grant_type.trim())
&& ("authorization_code").equals(grant_type.trim())
&& !StringUtils.isEmpty(code.trim())
&& !StringUtils.isEmpty(client_key.trim())
&& !StringUtils.isEmpty(redirect_url.trim())) {
try {
String clientCode = clientService.getCode(client_key, redirect_url);
if("".equals(clientCode)){
return new RetDTO(HttpStatus.OK.value(), "备案信息无效");
}
if (code.equals(clientCode)) { // 授权码有效,生成access_token
AuthClientAccessToken authClientAccessToken = clientService.createAccessToken(client_key, redirect_url, code);
logger.info("grant_type: {}, client_key: {}, redirect_url: {}, code: {}, access_token: {}", grant_type, client_key, redirect_url, code, authClientAccessToken.getAccessToken());
return RetDTO.getSuccess(authClientAccessToken);
}
} catch (Exception e) {
logger.error("grant_type: {}, client_key: {}, redirect_url: {}, code: {}, e: {}", grant_type, client_key, redirect_url, code, e.getMessage());
throw new BizException(e.getMessage());
}
}
logger.info("grant_type: {}, client_key: {}, redirect_url: {}, code: {}, 授权码无效", grant_type, client_key, redirect_url, code);
return new RetDTO(HttpStatus.OK.value(), "授权码无效");
}
更新access_token
/**
* 更新token
*
* @param grant_type
* @param refresh_token
* @param client_key
* @param redirect_url
* @return
*/
@PostMapping("refresh-token")
public RetDTO refresh_token(
@RequestParam("grant_type") String grant_type, // 固定值 authorization_code
@RequestParam("refresh_token") String refresh_token, // refresh_token
@RequestParam("client_key") String client_key, // 配置项
@RequestParam("redirect_url") String redirect_url // 配置项
) {
// refresh_token,client_key,redirect_uri,确认无误后,发放令牌access_token
if (!StringUtils.isEmpty(grant_type.trim())
&& ("authorization_code").equals(grant_type.trim())
&& !StringUtils.isEmpty(refresh_token.trim())
&& !StringUtils.isEmpty(client_key.trim())
&& !StringUtils.isEmpty(redirect_url.trim())) {
try {
String refreshToken = clientService.getRefreshToken(client_key, redirect_url);
if (!("").equals(refreshToken) && refresh_token.equals(refreshToken)) { // 授权码有效,生成access_token
AuthClientAccessToken authClientAccessToken = clientService.createAccessToken(client_key, redirect_url, refreshToken);
logger.info("grant_type: {}, client_key: {}, redirect_url: {}, refresh_token: {}, 新access_token: {}", grant_type, client_key, redirect_url, refresh_token, authClientAccessToken.getAccessToken());
return RetDTO.getSuccess(authClientAccessToken);
} else {
return new RetDTO(HttpStatus.OK.value(), "refresh_token无效");
}
} catch (Exception e) {
logger.info("grant_type: {}, client_key: {}, redirect_url: {}, refresh_token: {}, e: {}", grant_type, client_key, redirect_url, refresh_token, e.getMessage());
throw new BizException(e.getMessage());
}
}
logger.info("grant_type: {}, client_key: {}, redirect_url: {}, refresh_token: {}, access_token更新失败", grant_type, client_key, redirect_url, refresh_token);
return new RetDTO(HttpStatus.OK.value(), "refresh_token更新失败");
}