互联网开放平台API安全设计
互联网开放平台设计
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。