互聯網開放平台設計
1.需求:現在A公司與B公司進行合作,B公司需要調用A公司開放的外網接口獲取數據,
如何保證外網開放接口的安全性。
2.常用解決辦法:
2.1 使用加簽名方式,防止篡改數據
2.2 使用Https加密傳輸
2.3 搭建OAuth2.0認證授權
2.4 使用令牌方式
2.5 搭建網關實現黑名單和白名單
案例:A公司調用B公司的外網接口獲取數據,如何保證外網開放接口的安全性。
如何保證外網開放接口的安全性:
1.搭建API網關控制接口訪問權限
2.開放平台設計 開放憑他設計oauth2.0協議 QQ授權 第三方聯合登錄
3.采用Https加密傳輸協議 (使用Nginx配置Https)
4.API接口數字簽名(移動的接口)非對稱加密RSA
5. 基於令牌方式實現API接口調用。基於accessToken實現API調用 防止抓包分析篡改數據 基於accessToken實現AP調用
基於令牌方式實現:
表的設計:
開放平台提供者需要為每個合作機構提供對應的APPID APPSerect
基於令牌方式實現 AppId區分不同結構 永遠不能變 AppSerect 在傳輸中實現加密功能(密鑰) 可以發生改變 Appid+AppSerect生成對應access_token
表字段:
App_Name 表機構名稱
App_ID 應用id
App_Serect 應用密鑰(可更改)
Is_flag 是否可用
acess_token 上一次access_token
開發步驟:
Appid+AppSerect 對應生成accessToken
對應accessToken去表里查詢。查詢出的結果要求is_flag 是1, 0 代表不開放權限了
使用令牌方式搭建搭建API開放平台
原理:為每個合作機構創建對應的appid、app_secret,生成對應的access_token(有效期2小時),在調用外網開放接口的時候,必須傳遞有效的access_token。
生成accessToken之后
用accessToken對接口進行調用 此時會被 配置 API/* 攔截到,獲取到攜帶哦的accessToken,然后去redis中查詢對應的appId,如果redis中有對應的appId,利用appId去表中查詢對應的app信息
信息包括isFlag 看權限是否開啟 如果開啟 繼續往下走
放行到被攔截的 業務邏輯中
生成accessToken:
controller:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.alibaba.fastjson.JSONObject; import com.itmayiedu.base.BaseApiService; import com.itmayiedu.base.ResponseBase; import com.itmayiedu.entity.AppEntity; import com.itmayiedu.mapper.AppMapper; import com.itmayiedu.utils.BaseRedisService; import com.itmayiedu.utils.TokenUtils; // 創建獲取getAccessToken @RestController @RequestMapping(value = "/auth") public class AuthController extends BaseApiService { @Autowired private BaseRedisService baseRedisService; private long timeToken = 60 * 60 * 2; @Autowired private AppMapper appMapper; // 使用appId+appSecret 生成AccessToke @RequestMapping("/getAccessToken") public ResponseBase getAccessToken(AppEntity appEntity) { AppEntity appResult = appMapper.findApp(appEntity); if (appResult == null) { return setResultError("沒有對應機構的認證信息"); } int isFlag = appResult.getIsFlag(); if (isFlag == 1) { return setResultError("您現在沒有權限生成對應的AccessToken"); } // ### 獲取新的accessToken 之前刪除之前老的accessToken // 從redis中刪除之前的accessToken String accessToken = appResult.getAccessToken(); baseRedisService.delKey(accessToken); // 生成的新的accessToken String newAccessToken = newAccessToken(appResult.getAppId()); JSONObject jsonObject = new JSONObject(); jsonObject.put("accessToken", newAccessToken); return setResultSuccessData(jsonObject); //繼承的屬性 } private String newAccessToken(String appId) { // 使用appid+appsecret 生成對應的AccessToken 保存兩個小時 保證唯一且臨時 String accessToken = TokenUtils.getAccessToken(); // 保證在同一個事物redis 事務中 // 生成最新的token key為accessToken value 為 appid baseRedisService.setString(accessToken, appId, timeToken); //key為accessToken // 表中保存當前accessToken appMapper.updateAccessToken(accessToken, appId); return accessToken; } }
去調用API接口:
攔截器去進行處理驗證:
import java.io.IOException; import java.io.PrintWriter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import com.alibaba.fastjson.JSONObject; import com.itmayiedu.base.BaseApiService; import com.itmayiedu.utils.BaseRedisService; //驗證AccessToken 是否正確 @Component public class AccessTokenInterceptor extends BaseApiService implements HandlerInterceptor { @Autowired private BaseRedisService baseRedisService; public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { System.out.println("---------------------開始進入請求地址攔截----------------------------"); String accessToken = httpServletRequest.getParameter("accessToken"); // 判斷accessToken是否空 if (StringUtils.isEmpty(accessToken)) { // 參數Token accessToken resultError(" this is parameter accessToken null ", httpServletResponse); return false; } String appId = (String) baseRedisService.getString(accessToken); if (StringUtils.isEmpty(appId)) { // accessToken 已經失效! resultError(" this is accessToken Invalid ", httpServletResponse); return false; } // 正常執行業務邏輯... return true; } public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { System.out.println("--------------處理請求完成后視圖渲染之前的處理操作---------------"); } public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { System.out.println("---------------視圖渲染之后的操作-------------------------0"); } // 返回錯誤提示 public void resultError(String errorMsg, HttpServletResponse httpServletResponse) throws IOException { PrintWriter printWriter = httpServletResponse.getWriter(); printWriter.write(new JSONObject().toJSONString(setResultError(errorMsg))); } }
攔截器的配置類:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebAppConfig { @Autowired private AccessTokenInterceptor accessTokenInterceptor; @Bean public WebMvcConfigurer WebMvcConfigurer() { return new WebMvcConfigurer() { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(accessTokenInterceptor).addPathPatterns("/openApi/*"); }; }; } }
接口controller:
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.itmayiedu.base.BaseApiService; import com.itmayiedu.base.ResponseBase; @RestController @RequestMapping("/openApi") public class MemberController extends BaseApiService { @RequestMapping("/getUser") public ResponseBase getUser() { return setResultSuccess("獲取會員信息接口"); } }
Base類:
@Component public class BaseApiService { public ResponseBase setResultError(Integer code, String msg) { return setResult(code, msg, null); } // 返回錯誤,可以傳msg public ResponseBase setResultError(String msg) { return setResult(Constants.HTTP_RES_CODE_500, msg, null); } // 返回成功,可以傳data值 public ResponseBase setResultSuccessData(Object data) { return setResult(Constants.HTTP_RES_CODE_200, Constants.HTTP_RES_CODE_200_VALUE, data); } public ResponseBase setResultSuccessData(Integer code, Object data) { return setResult(code, Constants.HTTP_RES_CODE_200_VALUE, data); } // 返回成功,沒有data值 public ResponseBase setResultSuccess() { return setResult(Constants.HTTP_RES_CODE_200, Constants.HTTP_RES_CODE_200_VALUE, null); } // 返回成功,沒有data值 public ResponseBase setResultSuccess(String msg) { return setResult(Constants.HTTP_RES_CODE_200, msg, null); } // 通用封裝 public ResponseBase setResult(Integer code, String msg, Object data) { return new ResponseBase(code, msg, data); } }
package com.itmayiedu.base; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Getter @Setter @Slf4j public class ResponseBase { private Integer rtnCode; private String msg; private Object data; public ResponseBase() { } public ResponseBase(Integer rtnCode, String msg, Object data) { super(); this.rtnCode = rtnCode; this.msg = msg; this.data = data; } public static void main(String[] args) { ResponseBase responseBase = new ResponseBase(); responseBase.setData("123456"); responseBase.setMsg("success"); responseBase.setRtnCode(200); System.out.println(responseBase.toString()); log.info("itmayiedu..."); } @Override public String toString() { return "ResponseBase [rtnCode=" + rtnCode + ", msg=" + msg + ", data=" + data + "]"; } }
entity:表的實體類
public class AppEntity { private long id; private String appId; private String appName; private String appSecret; private String accessToken; private int isFlag; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getAppName() { return appName; } public void setAppName(String appName) { this.appName = appName; } public String getAppSecret() { return appSecret; } public void setAppSecret(String appSecret) { this.appSecret = appSecret; } public int getIsFlag() { return isFlag; } public void setIsFlag(int isFlag) { this.isFlag = isFlag; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } }
Mapper:
ublic interface AppMapper { @Select("SELECT ID AS ID ,APP_NAME AS appName, app_id as appId, app_secret as appSecret ,is_flag as isFlag , access_token as accessToken from m_app " + "where app_id=#{appId} and app_secret=#{appSecret} ") AppEntity findApp(AppEntity appEntity); @Select("SELECT ID AS ID ,APP_NAME AS appName, app_id as appId, app_secret as appSecret ,is_flag as isFlag access_token as accessToken from m_app " + "where app_id=#{appId} and app_secret=#{appSecret} ") AppEntity findAppId(@Param("appId") String appId); @Update(" update m_app set access_token =#{accessToken} where app_id=#{appId} ") int updateAccessToken(@Param("accessToken") String accessToken, @Param("appId") String appId); }
Utils類:
import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component public class BaseRedisService { @Autowired private StringRedisTemplate stringRedisTemplate; public void setString(String key, Object data, Long timeout) { if (data instanceof String) { String value = (String) data; stringRedisTemplate.opsForValue().set(key, value); } if (timeout != null) { stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } } public Object getString(String key) { return stringRedisTemplate.opsForValue().get(key); } public void delKey(String key) { stringRedisTemplate.delete(key); } }
public interface Constants { // 響應請求成功 String HTTP_RES_CODE_200_VALUE = "success"; // 系統錯誤 String HTTP_RES_CODE_500_VALUE = "fial"; // 響應請求成功code Integer HTTP_RES_CODE_200 = 200; // 系統錯誤 Integer HTTP_RES_CODE_500 = 500; // 未關聯QQ賬號 Integer HTTP_RES_CODE_201 = 201; // 發送郵件 String MSG_EMAIL = "email"; // 會員token String TOKEN_MEMBER = "TOKEN_MEMBER"; // 支付token String TOKEN_PAY = "TOKEN_pay"; // 支付成功 String PAY_SUCCESS = "success"; // 支付白 String PAY_FAIL = "fail"; // 用戶有效期 90天 Long TOKEN_MEMBER_TIME = (long) (60 * 60 * 24 * 90); int COOKIE_TOKEN_MEMBER_TIME = (60 * 60 * 24 * 90); Long PAY_TOKEN_MEMBER_TIME = (long) (60 * 15); // cookie 會員 totoken 名稱 String COOKIE_MEMBER_TOKEN = "cookie_member_token"; }
public class TokenUtils { @RequestMapping("/getToken") public static String getAccessToken() { return UUID.randomUUID().toString().replace("-", ""); } }
yml:
spring: mvc: view: # 頁面默認前綴目錄 prefix: /WEB-INF/jsp/ # 響應頁面默認后綴 suffix: .jsp spring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver test-while-idle: true test-on-borrow: true validation-query: SELECT 1 FROM DUAL time-between-eviction-runs-millis: 300000 min-evictable-idle-time-millis: 1800000 redis: database: 1 host: 106.15.185.133 port: 6379 password: meitedu.+@ jedis: pool: max-active: 8 max-wait: -1 max-idle: 8 min-idle: 0 timeout: 10000
表單設計:
CREATE TABLE `m_app` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(255) DEFAULT NULL,
`app_id` varchar(255) DEFAULT NULL,
`app_secret` varchar(255) DEFAULT NULL,
`is_flag` varchar(255) DEFAULT NULL,
`access_token` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
App_Name 表示機構名稱
App_ID 應用id
App_Secret 應用密鑰 (可更改)
Is_flag 是否可用 (是否對某個機構開放)
access_token 上一次access_token
小結: 第一步 先去生成accessToken。 如果獲取最新的accessToken(使用appid+appsecret 生成對應的AccessToken 保存兩個小時 保證唯一且臨時),需要從redis刪除原先的accessTokne。
通過兩個字段去查詢:
第二步去調用接口時候 會去攔截器進行限制 ,從redis獲取appId,然后去app表中獲取信息,看看是否有權限。
原理:為每個合作機構創建對應的appid、app_secret,生成對應的access_token(有效期2小時),在調用外網開放接口的時候,必須傳遞有效的access_token。