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更新失敗"); }