QBS将来会接入更多第三方应用,当第三方应用访问QBS系统资源时,须要通过QBS认证系统认证,认证经过以后可对QBS资源进行访问。安全
基于公网的HTTP请求存在被拦截,篡改,重发的可能,须要对用户请求参数信息进行有效保护。服务器
认证流程采用OAuth受权码方式,经过受权码获取access_token,经过access_token结合sign方式对请求入参进行加密。app
OAuth测试
开放协议标准,容许第三方应用访问服务提供者的资源,而不须要将用户名,密码提供给第三方应用加密
QBS认证系统url
独立的QBS资源系统的认证系统,对第三方应用进行备案及受权访问及限流/防刷操作系统
QBS资源服务器设计
题库系统,包含试题,试卷等信息code
client_keyorm
每一个准备访问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
根据参数名=参数值(参数值首尾不能包含空格)的格式,按首字符字典顺序 (ASCII值大小)升序排序。若遇到相同首字符,则判断第二个字符。以此类推,待签名字符串须要。
以参数名1=参数值1&参数名2=参数值2...&参数名N=参数值N的规则进行拼接。 参数首尾加上备案生成的client_secret, 最后得出的字符串进行md5Hex得出sign。
例子:
示例: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 * * @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(), "受权码无效"); }
/** * 更新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更新失败"); }