一、什么是appid、appkey、appsecret
AppID:应用的唯一标识。
AppKey:公匙(相当于账号)。
AppSecret:私匙(相当于密码)
token:令牌(过期失效)
app_id:是用来标记你的开发者账号的,是你的用户id,这个id 在数据库添加检索,方便快速查找。
app_key 和 app_secret 是一对出现的账号,同一个 app_id 可以对应多个 app_key+app_secret,这样平台就可以分配你不一样的权限,比如 app_key1 + app_secect1 只有只读权限 但是 app_key2+app_secret2 有读写权限......,这样你就可以把对应的权限放给不同的开发者,其中权限的配置都是直接跟app_key 做关联的,app_key 也需要添加数据库检索,方便快速查找。
至于为什么 要有app_key + app_secret 这种成对出现的机制呢,因为 要加密,通常 在首次验证(类似登录场景) ,你需要用 app_key(标记要申请的权限有哪些) + app_secret(密码,表示你真的拥有这个权限) 来申请一个token,就是我们经常用到的 access_token,之后的数据请求,就直接提供access_token 就可以验证权限了。
简化的场景
- 1、省去 app_id,他默认每一个用户有且仅有一套权限配置,所以直接将 app_id = app_key,然后外加一个app_secret就够了。
- 2、省去app_id 和 app_key,相当于 app_id = app_key = app_secret,通常用于开放性接口的地方,特别是很多地图类api 都采用这种模式,这种模式下,带上app_id 的目的仅仅是统计 某一个用户调用接口的次数而已了。
使用方法
- 1、向第三方服务器请求授权时,带上AppKey和AppSecret(需存在服务器端)
- 2、第三方服务器验证AppKey和AppSecret在DB中有无记录
- 3、如果有,生成一串唯一的字符串(token令牌),返回给服务器,服务器再返回给客户端
- 4、客户端下次请求敏感数据时带上令牌
二、云服务AppId或AppKey和AppSecret生成策略
App key简称API接口验证序号,是用于验证API接入合法性的。接入哪个网站的API接口,就需要这个网站允许才能够接入,如果简单比喻的话:可以理解成是登陆网站的用户名。
App Secret简称API接口密钥,是跟App Key配套使用的,可以简单理解成是密码。
App Key 和 App Secret 配合在一起,通过其他网站的协议要求,就可以接入API接口调用或使用API提供的各种功能和数据。
比如淘宝联盟的API接口,就是淘宝客网站开发的必要接入,淘客程序通过API接口直接对淘宝联盟的数据库调用近亿商品实时数据。做到了轻松维护,自动更新。
2.1 UUID
UUID是指在一台机器在同一时间中生成的数字在所有机器中都是唯一的。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字
UUID由以下几部分的组合:
- 1、当前日期和时间。
- 2、时钟序列。
- 3、全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),以连字号分为五段形式的36个字符,示例:550e8400-e29b-41d4-a716-446655440000
Java标准类库中已经提供了UUID的API。
UUID.randomUUID()
2.2 代码实现
- AppSecret 使用SHA-1生成20位byte数组,基本很难重复,再转化为40位16进制数字字符串。
/**
* @author: huangyibo
* @Date: 2022/6/15 16:17
* @Description: AppSecret 使用SHA-1生成20位byte数组,基本很难重复,再转化为40位16进制数字字符串。
*/
public class AppUtils {
//生成 app_secret 密钥
private final static String SERVER_NAME = "mazhq_abc123";
private final static String[] CHARS = new String[]{"a", "b", "c", "d", "e", "f",
"g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s",
"t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z"};
/**
* @Description: <p>
* 短8位UUID思想其实借鉴微博短域名的生成方式,但是其重复概率过高,而且每次生成4个,需要随即选取一个。
* 本算法利用62个可打印字符,通过随机生成32位UUID,由于UUID都为十六进制,所以将UUID分成8组,每4个为一组,然后通过模62操作,结果作为索引取出字符,
* 这样重复率大大降低。
* 经测试,在生成一千万个数据也没有出现重复,完全满足大部分需求。
* </p>
*/
public static String getAppId() {
StringBuilder shortBuffer = new StringBuilder();
String uuid = UUID.randomUUID().toString().replace("-", "");
for (int i = 0; i < 8; i++) {
String str = uuid.substring(i * 4, i * 4 + 4);
int x = Integer.parseInt(str, 16);
shortBuffer.append(CHARS[x % 0x3E]);
}
return shortBuffer.toString();
}
/**
* <p>
* 通过appId和内置关键词生成APP Secret
* </P>
*/
public static String getAppSecret(String appId) {
try {
String[] array = new String[]{appId, SERVER_NAME};
StringBuilder sb = new StringBuilder();
// 字符串排序
Arrays.sort(array);
for (String str : array) {
sb.append(str);
}
String str = sb.toString();
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
System.out.println(digest.length);
StringBuilder hexstr = new StringBuilder();
String shaHex = "";
for (int i = 0; i < digest.length; i++) {
shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexstr.append(0);
}
hexstr.append(shaHex);
}
return hexstr.toString();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
}
public static void main(String[] args) {
String appId = getAppId();
String appSecret = getAppSecret(appId);
System.out.println("appId: "+appId);
System.out.println("appSecret: "+appSecret);
String random = RandomStringUtils.randomAlphanumeric(63);
System.out.println(random);
System.out.println("09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7");
}
}
- AppSecret使用sha-256生成32位byte数组,基本很难重复,再转化为64位16进制数字字符串。
/**
* @author: huangyibo
* @Date: 2022/6/30 16:36
* @Description: AppSecret使用sha-256生成32位byte数组,基本很难重复,再转化为64位16进制数字字符串。
*/
public class AppUtils {
//某某服务 生成 app_secret 密钥
private final static String SERVER_NAME = "mazhq_abc123";
private final static String[] CHARS = new String[]{"a", "b", "c", "d", "e", "f",
"g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s",
"t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z"};
/**
* @Description: <p>
* 短8位UUID思想其实借鉴微博短域名的生成方式,但是其重复概率过高,而且每次生成4个,需要随即选取一个。
* 本算法利用62个可打印字符,通过随机生成32位UUID,由于UUID都为十六进制,所以将UUID分成8组,每4个为一组,然后通过模62操作,结果作为索引取出字符,
* 这样重复率大大降低。
* 经测试,在生成一千万个数据也没有出现重复,完全满足大部分需求。
* </p>
*/
public static String getAppId() {
StringBuilder shortBuffer = new StringBuilder();
String uuid = UUID.randomUUID().toString().replace("-", "");
for (int i = 0; i < 8; i++) {
String str = uuid.substring(i * 4, i * 4 + 4);
int x = Integer.parseInt(str, 16);
shortBuffer.append(CHARS[x % 0x3E]);
}
return shortBuffer.toString();
}
/**
* 通过appId和内置关键词生成APP Secret
* @param appId
* @return
*/
public static String getAppSecret(String appId) {
String[] array = new String[]{appId, SERVER_NAME};
StringBuilder stringBuilder = new StringBuilder();
// 字符串排序
Arrays.sort(array);
for (String str : array) {
stringBuilder.append(str);
}
String encodeStr = "";
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));
encodeStr = byte2Hex(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return encodeStr;
}
private static String byte2Hex(byte[] bytes) {
System.out.println(bytes.length);
StringBuilder stringBuilder = new StringBuilder();
String temp = null;
for (int i = 0; i < bytes.length; i++) {
temp = Integer.toHexString(bytes[i] & 0xFF);
if (temp.length() == 1) {
// 1得到一位的进行补0操作
stringBuilder.append(0);
}
stringBuilder.append(temp);
}
return stringBuilder.toString();
}
public static void main(String[] args) {
System.out.println("appId: " + getAppId());
System.out.println("appSecret: " + getAppSecret("130"));
System.out.println(getAppSecret("13034234324weweasxwsqszASxsadreqqscdzsd"));
System.out.println(getAppSecret("13034234324weweasxwsqszASxsadreq{ww=bb,see=2ss}"));
}
}
三、API 接口开发安全性
接口的安全性主要围绕token、timestamp和sign三个机制展开设计,保证接口的数据不会被篡改和重复调用。
在代码层面,对接口进行安全设计
- 1、使用token进行用户身份认证
- 2、使用sign防止传入参数被篡改
- 3、使用timestamp时间戳防止暴力请求
3.1 使用token进行用户身份认证授权
具体说明如下:
- 1、 用户登录时,客户端请求接口,传入用户名和密文的密码
- 2、 后台服务对用户身份进行验证。若验证失败,则返回错误结果;若验证通过,则生成一个随机不重复的token(可以是UUID),并将其存储在redis中,设置一个过期时间。
- 其中,redis的key为token,value为验证通过后获得的用户信息
- 3、 用户身份校验通过后,后台服务将生成的token返回客户端。
- 客户端请求后续其他接口时,需要带上这个token。后台服务会统一拦截接口请求,进行token有效性校验,并从中获取用户信息,供后续业务逻辑使用,Token是客户端访问服务端的凭证。
3.2 使用sign防止传入参数被篡改
为了防止中间人攻击(客户端发来的请求被第三方拦截篡改),引入参数的签名机制。
- 1、客户端和服务端约定一个加密算法(MD5或SHA-1算法(可根据情况加点盐)), 客户端发起请求时,将所有的非空参数按升序拼在一起,通过加密算法形成一个sign,将其放在请求头中传递给后端服务。
- 2、后端服务统一拦截接口请求,用接收到的非可空参数根据约定好的规则进行加密,和传入的sign值进行比较。若一致则予以放行,不一致则拒绝请求。
由于中间人不知道加密方法,也就不能伪造一个有效的sign。从而防止了中间人对请求参数的篡改。
3.3 用时间戳防止暴力请求
时间戳超时机制
用户每次请求都带上当前时间的时间戳timestamp,服务端接收到timestamp后跟当前时间进行比对,如果时间差大于一定时间(比如5分钟),则认为该请求失效。时间戳超时机制是防御DOS攻击的有效手段。
sign机制可以防止参数被篡改,但无法防dos攻击(第三方使用正确的参数,不停请求服务器,使之无法正常提供服务)。因此,还需要引入时间戳机制。
具体的操作为:
客户端在形成sign值时,除了使用所有参数和token外,再加一个发起请求时的时间戳。即 sign值来源 = 所有非空参数升序排序+token+timestamp
而服务端则需要根据当前时间和sign值的时间戳进行比较,差值超过一段时间则不予放行。
若要求不高,则客户端和服务端可以仅仅使用精确到秒或分钟的时间戳,据此形成sign值来校验有效性。这样可以使一秒或一分钟内的请求是有效的。
若要求较高,则还需要约定一个解密算法,使后端服务可以从sign值中解析出发起请求的时间戳。
总结后的流程图如下:
3.4 拒绝重复调用(非必须)
客户端第一次访问时,将签名sign存放到缓存服务器中,超时时间设定为跟时间戳的超时时间一致,二者时间一致可以保证无论在timestamp限定时间内还是外 URL都只能访问一次。如果有人使用同一个URL再次访问,如果发现缓存服务器中已经存在了本次签名,则拒绝服务。如果在缓存中的签名失效的情况下,有人使用同一个URL再次访问,则会被时间戳超时机制拦截。这就是为什么要求时间戳的超时时间要设定为跟时间戳的超时时间一致。拒绝重复调用机制确保URL被别人截获了也无法使用(如抓取数据)。
在以上三种机制的保护下,如果有人劫持了请求,并对请求中的参数进行了修改,签名就无法通过;
如果有人使用已经劫持的URL进行DOS攻击,服务器则会因为缓存服务器中已经存在签名或时间戳超时而拒绝服务,所以DOS攻击也是不可能的;
所有的安全措施都用上的话有时候难免太过复杂,在实际项目中需要根据自身情况作出裁剪,比如可以只使用签名机制就可以保证信息不会被篡改,或者定向提供服务的时候只用Token机制就可以了。如何裁剪,全看项目实际情况和对接口安全性的要求。
四、基于AccessToken方式实现API设计
需求:
- A、B机构需要调用X服务器的接口,那么X服务器就需要提供开放的外网访问接口。
分析:
- 1、开放平台提供者X,为每一个合作机构提供对应的appid、app_secret。
- 2、appid是唯一的(不能改变),表示对应的第三方合作机构,用来区分不同机构的。
- 3、app_secret在传输中实现加密功能(秘钥),该秘钥可以发生改变的。
- 4、为什么app_secret是可以改变的?调用接口需要appid+app_secret生成对应的access_token(临时性),如果appid和app_secret被泄密,产生安全性问题,如果一但发现被泄密,可以重新生成一个app_secret。
原理:为每个合作机构创建对应的appid、app_secret,生成对应的access_token(有效期2小时),在调用外网开放接口的时候,必须传递有效的access_token。
4.1 开发步骤
4.1.1、使用appid+app_secret生成对应的access_token
- 1、获取生成的AppId和appSecret,并验证是否可用
- 2、删除之前的accessToken
- 3、AppId和appSecret保证生成对应唯一的accessToken
- 注意:以上第二步必须保证在同一事务中
- 4、返回最新的accessToken
4.1.2、使用accessToken调用第三方接口
- 1、获取对应的accessToken
- 2、使用AccessToken查询redis对应的value(appId)
- 3、如果没有获取到对应的appid,直接返回错误提示
- 4、如果能获取到对应的appid,使用appid查询对应的APP信息
- 5、使用appId查询数据库app信息,获取is_flag状态,如果为1,则不能调用接口,否则正常执行
- 6、直接调用接口业务
五、常见问题总结
做API接口,为什么access_token要放在Header头里传递?
如果是OAuth2, 使用 Header传递token是属于规范的一种,Header中有一个Authorization头专门用于存放认证信息每一次登录,会生成一个新的Token, 此时旧的token并不会立即失效(取决于该token生成时,设置的失效时间)
六、代码实现
服务提供方
- 处理无法重复读取stream流,使之可以在一个stream流中多次读取同一个request值
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @Author: huangyibo
* @Date: 2023/1/6 14:43
* @Description: 处理无法重复读取stream流,使之可以在一个stream流中多次读取同一个request值
*/
public class RequestWrapper extends HttpServletRequestWrapper {
//参数字节数组
private byte[] requestBody;
//Http请求对象
private HttpServletRequest request;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.request = request;
}
/**
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream() throws IOException {
/**
* 每次调用此方法时将数据流中的数据读取出来,然后再回填到InputStream之中
* 解决通过@RequestBody和@RequestParam(POST方式)读取一次后控制器拿不到参数问题
*/
if (null == this.requestBody) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtils.copy(request.getInputStream(), baos);
this.requestBody = baos.toByteArray();
}
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() {
return bais.read();
}
};
}
public byte[] getRequestBody() {
return requestBody;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
- 拦截所有请求过滤器,并将请求类型是HttpServletRequest类型的请求替换为自定义{@link RequestWrapper}
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @Author: huangyibo
* @Date: 2023/1/5 15:36
* @Description: 拦截所有请求过滤器,并将请求类型是HttpServletRequest类型的请求替换为自定义{@link RequestWrapper}
*/
@Component
@WebFilter(filterName = "ChannelFilter", urlPatterns = {"/*"})
public class ChannelFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) request);
}
if (requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
}
- 商户鉴权拦截器-TenantAuthInterceptor
/**
* @Author: huangyibo
* @Date: 2023/1/3 18:29
* @Description: 商户对外开放鉴权拦截器
*/
@Component
@Slf4j
public class TenantAuthInterceptor extends HandlerInterceptorAdapter {
@Resource
private RedisUtil redisUtil;
@Resource
private SysTenantAppFeign sysTenantAppFeign;
/**
* 单次请求timestamp参数过期时间为5分钟
*/
private static final long TIMESTAMP_EXPIRE = 5 * 60 * 1000;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从header中获取接口签名
String sign = request.getHeader(JwtConstants.ACCESS_OPEN_API_SIGN_HEADER);
if(StringUtils.isEmpty(sign)){
log.error("商户接口签名为空, url={}", request.getRequestURL());
ResultData<String> resultData = ResultData.fail(SysResultEnum.TENANT_SIGN_EMPTY.getStatus(), SysResultEnum.TENANT_SIGN_EMPTY.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
Map paramMap = new TreeMap<>();
if(HttpMethod.GET.name().equals(request.getMethod())){
queryGetParamterMap(request,paramMap);
}else {
//获取请求body
byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
String body = new String(bodyBytes, request.getCharacterEncoding());
paramMap = JSONObject.parseObject(body, TreeMap.class);
}
String timestampStr = String.valueOf(paramMap.get("timestamp"));
if(StringUtils.isEmpty(timestampStr)){
log.error("商户接口请求参数时间戳为空, url={}", request.getRequestURL());
ResultData<String> resultData = ResultData.fail(SysResultEnum.TENANT_TIMESTAMP_EMPTY.getStatus(), SysResultEnum.TENANT_TIMESTAMP_EMPTY.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
long timestamp = Long.parseLong(timestampStr);
if((System.currentTimeMillis() - timestamp) >= TIMESTAMP_EXPIRE){
log.error("商户接口单次请求timestamp参数已失效, url={}", request.getRequestURL());
ResultData<String> resultData = ResultData.fail(SysResultEnum.TENANT_TIMESTAMP_EXPIRE.getStatus(), SysResultEnum.TENANT_TIMESTAMP_EXPIRE.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
String appId = (String)paramMap.get("appId");
SysTenantAppRedisVo tenantAppRedisVo = (SysTenantAppRedisVo) redisUtil.get(RedisKeyConstant.SYS_TENANT_APP_INFO + appId);
if(tenantAppRedisVo == null){
tenantAppRedisVo = sysTenantAppFeign.selectTenantByAppId(appId).pickBody();
if (Objects.nonNull(tenantAppRedisVo.getSysResultEnum())) {
log.error("商户鉴权拦截校验, 验证不通过,appId={}, message={}", appId, tenantAppRedisVo.getSysResultEnum().getMessage());
ResultData<String> resultData = ResultData.fail(tenantAppRedisVo.getSysResultEnum().getStatus(), tenantAppRedisVo.getSysResultEnum().getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
//保存20分钟
redisUtil.set(RedisKeyConstant.SYS_TENANT_APP_INFO + appId, tenantAppRedisVo, 1200);
log.info("商户鉴权拦截校验, 商户信息获取成功, 保存redis20分钟, appId={}, tenantAppRedisVo={}",
appId, JSON.toJSONString(tenantAppRedisVo));
}
StringBuilder stringBuilder = new StringBuilder();
paramMap.forEach((key, value) -> {
if(!StringUtils.isEmpty(value)){
stringBuilder.append(value);
}
});
stringBuilder.append(tenantAppRedisVo.getSecurityKey());
String localSign = DigestUtils.md5DigestAsHex(stringBuilder.toString().getBytes()).toUpperCase();
if(!sign.equals(localSign)){
log.error("商户接口签名异常, appId={}, url={}", appId, request.getRequestURL());
ResultData<String> resultData = ResultData.fail(SysResultEnum.TENANT_SIGN_ILLEGAL.getStatus(), SysResultEnum.TENANT_SIGN_ILLEGAL.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
TenantAuthAnnotation auth = handlerMethod.getMethod().getAnnotation(TenantAuthAnnotation.class);
if (auth == null) {
// 如果注解为null, 说明不需要拦截, 直接放过
return Boolean.TRUE;
}
//校验用户是否有权限
if (!hasAuth(tenantAppRedisVo.getInterfaceCodeList(), auth)) {
log.error("商户权限拦截校验, 接口权限不通过, appId={}, auth={}", appId, JSON.toJSONString(auth.value()));
ResultData<String> resultData = ResultData.fail(SysResultEnum.UNAUTHORIZED.getStatus(), SysResultEnum.UNAUTHORIZED.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
log.info("商户鉴权成功, appId={}, url={}", appId, request.getRequestURL());
return Boolean.TRUE;
}
/**
* 获取Get请求参数
* @param request
* @param reqMap
*/
private void queryGetParamterMap(HttpServletRequest request, Map reqMap) {
Map parameterMap = request.getParameterMap();
Set<Map.Entry<String,String[]>> entry = parameterMap.entrySet();
Iterator<Map.Entry<String,String[]>> it = entry.iterator();
while (it.hasNext()){
Map.Entry<String,String[]> me = it.next();
String key = me.getKey();
String value = me.getValue()[0];
reqMap.put(key,value);
}
}
/**
* 校验权限是否匹配
* @param authList
* @param auth
* @return
*/
private boolean hasAuth(List<InterfaceCodeEnum> authList, TenantAuthAnnotation auth) {
if (!CollectionUtils.isEmpty(authList)) {
for (InterfaceCodeEnum authEnum : auth.value()) {
if (authList.contains(authEnum)) {
return true;
}
}
}
return false;
}
}
接入方
- 接入方OpenApiFeign
@FeignClient(name = "OpenApiFeign", url= "${openapi.url}", configuration = FeignSSLConfiguration.class)
public interface OpenApiFeign {
@GetMapping(value = "/security/open/out/demo/queryGetDemo", produces = "application/json;charset=utf-8")
ResultBody<OpenDemo> queryGetDemo(OpenDemo demo);
@PostMapping(value = "/security/open/out/demo/queryPostDemo", produces = "application/json;charset=utf-8")
ResultBody<OpenDemo> queryPostDemo(OpenDemo demo);
}
- feign client配置, 调用https接口时绕过SSL证书验证
import feign.Client;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
/**
* @Author: huangyibo
* @Date: 2023/1/6 17:49
* @Description: feign client配置, 调用https接口时绕过SSL证书验证
*/
public class FeignSSLConfiguration {
@Bean
public CachingSpringLoadBalancerFactory cachingFactory(SpringClientFactory clientFactory) {
return new CachingSpringLoadBalancerFactory(clientFactory);
}
/**
* 调用https接口时绕过SSL证书验证
* @param cachingFactory
* @param clientFactory
* @return
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
*/
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) throws NoSuchAlgorithmException, KeyManagementException {
SSLContext ctx = SSLContext.getInstance("TLSv1.2");
X509TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
ctx.init(null, new TrustManager[]{tm}, null);
return new LoadBalancerFeignClient(new Client.Default(ctx.getSocketFactory(),
(hostname, session) -> true),
cachingFactory, clientFactory);
}
}
- feign请求参数拦截器
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.TreeMap;
/**
* @Author: huangyibo
* @Date: 2023/1/6 17:57
* @Description: feign请求参数拦截器
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(FeignRequestInterceptor.class);
@Value("${openapi.appId}")
private String appId;
@Value("${openapi.securityKey}")
private String securityKey;
@Value("${openapi.version}")
private String version;
private static final String KFANG_PRICE_OPEN_URL = "/security/open/out";
public static final String OPEN_API_SIGN_HEADER = "Sign";
@Override
public void apply(RequestTemplate requestTemplate) {
String url = requestTemplate.url();
if (url.contains(KFANG_PRICE_OPEN_URL)) {
//获取请求体
byte[] bodyBytes = requestTemplate.body();
Map paramMap = new TreeMap<>();
try {
String body = new String(bodyBytes, requestTemplate.requestCharset() == null ? "utf-8": requestTemplate.requestCharset().name());
paramMap = JSONObject.parseObject(body, TreeMap.class);
paramMap.put("appId", appId);
if (HttpMethod.GET.name().equals(requestTemplate.method())) {
paramMap.forEach((key, value) -> {
if(!StringUtils.isEmpty(value)){
// 将body的参数写入queries
requestTemplate.query(String.valueOf(key), String.valueOf(value));
}
});
}else {
//POST设置请求体
requestTemplate.body(JSON.toJSONString(paramMap));
}
StringBuilder stringBuilder = new StringBuilder();
paramMap.forEach((key, value) -> {
if(!StringUtils.isEmpty(value)){
stringBuilder.append(value);
}
});
stringBuilder.append(securityKey);
String sign = DigestUtils.md5DigestAsHex(stringBuilder.toString().getBytes()).toUpperCase();
requestTemplate.header(OPEN_API_SIGN_HEADER, sign);
requestTemplate.header("Content-Type", "application/json;charset=utf-8");
} catch (Exception e) {
logger.error("feign参数拦截, 添加接口签名异常, url:{}", url, e);
}
}
}
}
- HttpContextUtils
public class HttpContextUtils {
/**
* 获取query参数
* @param request
* @return
*/
public static Map<String, String> getParameterMapAll(HttpServletRequest request) {
Enumeration<String> parameters = request.getParameterNames();
Map<String, String> params = new HashMap<>();
while (parameters.hasMoreElements()) {
String parameter = parameters.nextElement();
String value = request.getParameter(parameter);
params.put(parameter, value);
}
return params;
}
/**
* 获取请求Body
* @param request
* @return
* @throws Exception
*/
public Map<String, Object> parsePostBodyToMap(HttpServletRequest request) throws Exception {
StringBuilder sb = new StringBuilder();
String line;
BufferedReader reader = request.getReader();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String body = sb.toString();
return JSON.parseObject(body);
// 这里使用Jackson进行转换,确保项目中已经引入了Jackson依赖
/*ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(body, new TypeReference<Map<String, Object>>(){});*/
}
}
参考:
https://docker.blog.csdn.net/article/details/103140515
https://www.cnblogs.com/owenma/p/11419341.html
https://www.cnblogs.com/yaoyu1983/p/12267809.html
https://blog.csdn.net/wjg8209/article/details/118806853
https://www.cnblogs.com/kevin-ying/p/10800934.html
https://www.jb51.net/article/239665.htm