对外提供API,通过appId、appSecret、sign秘钥对接口做鉴权
一、背景
在接口开发过程中,我们通常不能暴露一个接口给第三方随便调用,要对第三方发来参数进行校验,看是不是具有访问权限。
名词介绍:
1、appId: 应用id,用户自定义命名,如:*-access-token
2、appSecret:安全密钥,服务端通过uuid生成,然后发送给调用方
3、sign:开发生成签名的工具类,发送给调用方
鉴权步骤:
1、客户端使用提供的工具,传参appId、时间戳、appSecret生成sign签名
2、发送appId、13位系统时间戳、签名、请求参数给服务端
3、服务端收到appId、时间戳、签名参数后,根据appId查询数据库,获取用户appSecret安全密钥。同样根据appId、时间戳、appSecret生成sign,判断用户传参的sign是否相等。
二、具体代码
1、鉴权工具类
package com.test.utils;
import java.security.MessageDigest;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
/**
* @datetime 2022-11-02 上午11:00
* @desc 接口校验工具类
* 生成有序map,签名,验签
* 通过appId、timestamp、appSecret做签名
* @menu
*/
@Slf4j
public class SignUtil {
/**
* 生成签名sign
* 加密前:appId=wx123456789×tamp=1583332804914&key=7214fefff0cf47d7950cb2fc3b5d670a
* 加密后:E2B30D3A5DA59959FA98236944A7D9CA
*/
public static String createSign(SortedMap<String, String> params, String key){
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
Iterator<Map.Entry<String,String>> it = es.iterator();
//生成
while (it.hasNext()){
Map.Entry<String,String> entry = it.next();
String k = entry.getKey();
String v = entry.getValue();
if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
sb.append(k+"="+v+"&");
}
}
sb.append("key=").append(key);
String sign = MD5(sb.toString()).toUpperCase();
return sign;
}
/**
* 校验签名
*/
public static Boolean isCorrectSign(SortedMap<String, String> params, String key){
String sign = createSign(params,key);
String requestSign = params.get("sign").toUpperCase();
log.info("通过用户发送数据获取新签名:{}", sign);
return requestSign.equals(sign);
}
/**
* md5常用工具类
*/
public static String MD5(String data){
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte [] array = md5.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 生成uuid
*/
public static String generateUUID(){
String uuid = UUID.randomUUID().toString().replaceAll("-","").substring(0,32);
return uuid;
}
public static void main(String[] args) {
//第一步:生成uuid,用作appSecret
// System.out.println(SignUtil.generateUUID());
//第二步:用户端发起请求,生成签名后发送请求
String appSecret = "7214fefff0cf47d7950cb2fc3b5d670a";
String appId = "wx123456789";
String timestamp = "1583332804914";
//生成签名
SortedMap<String, String> sortedMap = new TreeMap<>();
sortedMap.put("appId", appId);
sortedMap.put("timestamp", timestamp);
System.out.println("签名:"+SignUtil.createSign(sortedMap, appSecret));
//第三步:校验签名
//1.校验时间戳
long requestTime = Long.valueOf(timestamp);
// 时间查过20秒,则认为接口为重复调用,返回错误信息
long nowTime = new Date().getTime();
int seconds = (int) ((nowTime - requestTime)/1000);
if(Math.abs(seconds) > 86400) {
System.out.println("访问已过期,请检查服务器时间!");
return;
}
//2.组装参数,
SortedMap<String, String> sortedMap12 = new TreeMap<>();
sortedMap12.put("appId", appId);
sortedMap12.put("timestamp", timestamp);
sortedMap12.put("sign", "");
//3.校验签名
Boolean flag = SignUtil.isCorrectSign(sortedMap12, appSecret);
if(flag){
System.out.println("签名验证通过");
}else {
System.out.println("签名验证未通过");
}
}
}
2、提供给用户的生成签名工具类
import java.security.MessageDigest;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import 上面的工具类包下.SignUtil;
/**
* @datetime 2022-11-02 下午2:45
* @desc
* @menu
*/
public class SignUtilTest {
/**
* 生成签名sign
* 加密前:appId=wx123456789×tamp=1583332804914&key=7214fefff0cf47d7950cb2fc3b5d670a
* 加密后:E2B30D3A5DA59959FA98236944A7D9CA
*/
public static String createSign(SortedMap<String, String> params, String key){
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
Iterator<Map.Entry<String,String>> it = es.iterator();
//生成
while (it.hasNext()){
Map.Entry<String,String> entry = it.next();
String k = entry.getKey();
String v = entry.getValue();
if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
sb.append(k+"="+v+"&");
}
}
sb.append("key=").append(key);
String sign = MD5(sb.toString()).toUpperCase();
return sign;
}
/**
* md5常用工具类
*/
public static String MD5(String data){
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte [] array = md5.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
//第二步:用户端发起请求,生成签名后发送请求
String appSecret = "";
String appId = "";
long nowTime = new Date().getTime();
//生成签名
SortedMap<String, String> sortedMap = new TreeMap<>();
sortedMap.put("appId", appId);
sortedMap.put("timestamp", String.valueOf(nowTime));
System.out.println("appId:"+appId+" 时间戳:"+nowTime+" 签名:"+ SignUtil.createSign(sortedMap, appSecret));
}
}
3、服务端系统对控制层做切面,拦截部分url请求
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
//省略内部使用包
@Slf4j
@Aspect
@Component
public class ExtApiAspect {
/**
* 系统接入管理
*/
@Resource
SystemAccessMapper systemAccessMapper;
/**
* 不拦截路径
*/
private static final Set<String> FILTER_PATHS = Collections.unmodifiableSet(new HashSet<>(
Arrays.asList("/api/extApi/test")));
/**
* 匹配ExtApiController下所有方法
*/
@Pointcut("execution(public * com.test.ExtApiController.*(..))")
public void webLog() {
}
/**
* 1、控制层处理完后返回给用户前,记录日志
* 3、调用处理方法获取结果后,再调用本方法proceed
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印下返回给用户数据
log.info("RequestId={}, Response Args={}, Time-Consuming={} ms", RequestThreadLocal.getRequestId(),
JsonUtil.toJSON(result), System.currentTimeMillis() - startTime);
return result;
}
/**
* 2、调用控制层方法前执行
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
RequestThreadLocal.setRequestId(getUUID());
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//不拦截打印日志的方法
String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", "");
if(FILTER_PATHS.contains(path)){
return;
}
//请求参数
Object[] args = joinPoint.getArgs();
//请求的方法参数值 JSON 格式 null不显示
if (args.length > 0) {
for (int i = 0; i < args.length; i++) {
//请求参数类型判断过滤,防止JSON转换报错
if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse || args[i] instanceof MultipartFile) {
continue;
}
StringBuilder logInfo = new StringBuilder();
logInfo.append("RequestId=").append(RequestThreadLocal.getRequestId()).append(SystemConstants.LOG_SEPARATOR)
.append(" RequestMethod=").append(request.getRequestURI()).append(SystemConstants.LOG_SEPARATOR)
.append(" Args=").append(args[i].toString());
log.info(logInfo.toString());
JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(args[i]));
if (jsonObject != null) {
String appId = jsonObject.getString("appId");
String timestamp = jsonObject.getString("timestamp");
String sign = jsonObject.getString("sign");
if(StringUtils.isEmpty(appId) || StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(sign)){
throw new CustomException(CodeEnum.EXT_API_PARAMETER_ERROR);
}
//1.时间戳校验,大于一天不处理
long requestTime = Long.valueOf(timestamp);
long nowTime = new Date().getTime();
int seconds = (int) ((nowTime - requestTime)/1000);
if(Math.abs(seconds) > 86400) {
throw new CustomException(CodeEnum.EXT_API_TIMEOUT);
}
LambdaQueryWrapper<SystemAccessDTO> queryWrapper = new LambdaQueryWrapper<>();
//!!!!!安全考虑,省略查询条件
queryWrapper.last(" limit 1");
SystemAccessDTO systemAccessDTO = systemAccessMapper.selectOne(queryWrapper);
if(systemAccessDTO == null){
throw new CustomException(CodeEnum.EXT_API_AUTH_ERROR);
}
//3.校验签名
SortedMap<String, String> sortedMap = new TreeMap<>();
sortedMap.put("appId", appId);
sortedMap.put("timestamp", timestamp);
sortedMap.put("sign", sign);
Boolean flag = SignUtil.isCorrectSign(sortedMap, systemAccessDTO.getAppSecret());
if(flag){
log.info("外部系统接入,信息认证正确"+appId);
}else{
throw new CustomException(CodeEnum.EXT_API_AUTH_ERROR);
}
}
}
}else{
throw new CustomException(CodeEnum.PARAMETER_ERROR);
}
}
/**
* 4、调用控制层方法后,说明校验通过不处理
*/
@After("webLog()")
public void doAfter(JoinPoint joinPoint) throws Throwable {
log.info("外部接口调用鉴权成功" + RequestThreadLocal.getRequestId());
}
/**
* 5、处理完后
*/
@AfterReturning(pointcut = "webLog()", returning = "result")
public void AfterReturning(JoinPoint joinPoint, RestResponse result) {
result.success(RequestThreadLocal.getRequestId());
RequestThreadLocal.remove();
}
/**
* 异常处理
*/
@AfterThrowing("webLog()")
public void afterThrowing() {
RequestThreadLocal.remove();
}
/**
* 生成uuid
*/
public static String getUUID() {
return UUID.randomUUID().toString().trim();
}
}
三、模拟请求
请求post:http://127.0.0.1:8080/test/test
请求json
{"appId":"appid","timestamp":"1667384136520","sign":"sign","ids":[接口其它请求]}
参考文档:https://blog.csdn.net/it1993/article/details/104682832/