接口鉴权
由于api接口直接暴露在服务器上容易被恶意攻击,所有使用接口鉴权来拦截恶意请求
使用时间戳+appId+secret+参数排序生成MD5加密传输.被调用api接口使用AOP切面添加注解鉴权,
text
MD5是一种不可逆的加密算法,这意味着一旦数据被MD5加密,就无法直接从加密后的结果还原出原始数据。尽管MD5加密本身是不可逆的,但它可以通过一些方法来实现特定的应用,如数据完整性校验和密码存储等。
1.数据完整性校验:通过将消息体排序、拼接后进行MD5加密,生成一个签名。发送方将原始消息体和签名一起发送给接收方。接收方收到消息后,用相同的方法计算签名,并与接收到的签名进行比较。如果两者匹配,说明消息在传输过程中没有被篡改,从而保证了数据的完整性
2.密码存储:虽然MD5不可逆,但它仍然被用于密码存储,尤其是在早期系统中。这是因为MD5加密速度快,适合大量数据的加密处理。然而,由于MD5的安全性较低,现代系统更倾向于使用更安全的哈希算法(如SHA-256)来存储密码哈希值2。
尽管MD5有上述应用,但需要注意的是,由于MD5算法的设计特性,它存在固有的安全缺陷,如哈希冲突、压缩性和预计算表等问题,这些都可能影响到其安全性。因此,对于需要高安全性的应用场景,建议使用更强大的加密算法来保护数据安全
此外,虽然存在一些尝试性的破解方法(如字典攻击、彩虹表攻击和穷举攻击),但这些方法的有效性取决于攻击者的资源和计算能力。因此,对于关键的应用和数据,应考虑使用更安全的加密算法来保护数据安全
一.调用接口方法
1.调用接口方法类-添加接口鉴权参数
java
public class AliHuiFeiRefundOrder {
private final static String DETAIL_URL = "http://192.168.0.66:12886/order/list";
public static void demo(){
Map<String, String> requestParams = getRequestParams("/order/list");
requestParams.put("startTime",startTime);
requestParams.put("endTime",endTime);
requestParams.put("siteName","2342as");
requestParams.put("status", "3");
String token = createToken(requestParams);
requestParams.put("token",token);
HttpResponse response = HttpRequest.get(DETAIL_URL)
.formStr(requestParams)
.execute();
if (HttpStatus.SUCCESS == response.getStatus()) {
String result = response.body();
log.info("拉取结果[{}--{}]==>{}",startTime,endTime,result);
}
}
}
2.封装接口鉴权公共请求参数ApiTokenReqParamUtil类
java
package com.bgn.common.util;
import com.bgn.common.core.utils.uuid.UUID;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: lxq
* @Date: 2024/06/24/15:17
* @Description: 封装接口鉴权公共请求参数
* 生成 appId + secret
*/
public class ApiTokenReqParamUtil {
//生成 app_secret 密钥
private final static String SERVER_NAME = "demo_api_123456";
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"};
public static Map<String, String> getRequestParams(String baseUrl) {
Map<String, String> params = new HashMap<>(1 << 2);
String appId = "7A***Nkp";
String secret = "73****************************55c4";
long currentTime = System.currentTimeMillis();
params.put("appId",appId);
params.put("baseUrl",baseUrl);
params.put("createTime", String.valueOf(currentTime));
params.put("secret",secret);
return params;
}
public static void main(String[] args) {
String appId = getAppId();
String appSecret = getAppSecret(appId);
System.out.println("appId: "+appId);
System.out.println("appSecret: "+appSecret);
}
/**
* 短8位UUID思想其实借鉴微博短域名的生成方式,但是其重复概率过高,而且每次生成4个,需要随即选取一个。
* 本算法利用62个可打印字符,通过随机生成32位UUID,由于UUID都为十六进制,所以将UUID分成8组,每4个为一组,然后通过模62操作,结果作为索引取出字符,
* 这样重复率大大降低。
* 经测试,在生成一千万个数据也没有出现重复,完全满足大部分需求。
* @return
*/
public static String getAppId() {
StringBuffer shortBuffer = new StringBuffer();
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) {
try {
String[] array = new String[]{appId, SERVER_NAME};
StringBuffer sb = new StringBuffer();
// 字符串排序
Arrays.sort(array);
for (int i = 0; i < array.length; i++) {
sb.append(array[i]);
}
String str = sb.toString();
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
StringBuffer hexstr = new StringBuffer();
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 (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
}
4.接口api鉴权-公共token工具类
java
package com.bgn.common.core.utils.sign;
import cn.hutool.core.map.MapUtil;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.crypto.digest.Digester;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* @Author: lxq
* @Date: 2024/06/24/14:02
* @Description: 接口api鉴权-公共token工具类
*/
public class ApiTokenUtils {
private static final Digester MD5 = new Digester(DigestAlgorithm.MD5);
private ApiTokenUtils() {
throw new IllegalStateException("Utility class");
}
public static String createToken(Map<String, String> params) {
String appId = params.get("appId");
String secret = params.get("secret");
String createTime = params.get("createTime");
String baseUrl = params.get("baseUrl");
String original = appId + secret + baseUrl + MapUtil.sortJoin(params, "&", "=", true) + createTime;
return MD5.digestHex(original, StandardCharsets.UTF_8);
}
}
二.添加api接口鉴权-使用AOP添加一个注解,便于操作
1.用于接口api鉴权AuthenticationAop
java
package com.bgn.fliggy.aop;
import com.bgn.common.core.exception.GlobalException;
import com.bgn.fliggy.utils.AuthToken;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import com.bgn.common.core.utils.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: lxq
* @Date: 2024/06/24/11:52
* @Description: 用于接口api鉴权
*/
@Slf4j
@Aspect
@Component
public class AuthenticationAop {
@Value("${apiAuthToken.secret}")
public String secret;
@Pointcut("@annotation(com.bgn.fliggy.aop.Authentication)")
public void pointCutMethodBefore() {
throw new UnsupportedOperationException();
}
@Before("pointCutMethodBefore()")
public void doBefore(JoinPoint point) {
MethodInvocationProceedingJoinPoint mjp = (MethodInvocationProceedingJoinPoint) point;
MethodSignature signature = (MethodSignature) mjp.getSignature();
Method method = signature.getMethod();
Authentication annotation = method.getAnnotation(Authentication.class);
if (annotation != null) {
HttpServletRequest request = getRequest(point.getArgs());
if (request == null) {
throw new GlobalException("【鉴权失败】,获取HttpServletRequest");
}
String appId = request.getParameter("appId");
if (StringUtils.isEmpty(appId)) {
throw new GlobalException("【鉴权失败】,appId不存在");
}
final String token = request.getParameter("token");
String createTime = request.getParameter("createTime");
if (StringUtils.isEmpty(createTime)){
throw new GlobalException("【鉴权失败】,createTime不存在");
}
Map<String, String> params = new HashMap<>(1 << 2);
final String baseUrl = request.getRequestURI();
request.getParameterMap().forEach((k, v) -> params.put(k, v[0]));
params.remove("token");
params.put("secret", secret);
AuthToken authToken = AuthToken.createToken(appId, secret, Long.parseLong(createTime), baseUrl, params);
log.info(authToken.getToken());
if (!authToken.isExpired()) {
throw new GlobalException("【鉴权失败】,token已过期");
}
if (!ObjectUtils.nullSafeEquals(token, authToken.getToken())) {
throw new GlobalException("【鉴权失败】,token错误");
}
}
}
private HttpServletRequest getRequest(Object[] args) {
for (Object o : args) {
if (o instanceof HttpServletRequest) {
return (HttpServletRequest) o;
}
}
return null;
}
}
2.用于api接口鉴权
java
package com.bgn.fliggy.aop;
import java.lang.annotation.*;
/**
* @Author: lxq
* @Date: 2024/06/24/11:51
* @Description: 用于api接口鉴权
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Authentication {
}
3. 获取参数生成token工具类
java
package com.bgn.fliggy.utils;
import cn.hutool.core.map.MapUtil;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.crypto.digest.Digester;
import lombok.Getter;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* @Author: lxq
* @Date: 2024/06/24/11:38
* @Description: 获取参数生成token
*/
public class AuthToken {
// 默认超时时间 3 分钟
private static final long DEFAULT_EXPIRE_TIME_INTERVAL = 3*60 * 1000;
@Getter
private String token;
private long createTime;
@Getter
private long expiredTimeInterval = DEFAULT_EXPIRE_TIME_INTERVAL;
private static final Digester MD5 = new Digester(DigestAlgorithm.MD5);
public AuthToken(String token, long createTime) {
this.token = token;
this.createTime = createTime;
}
public AuthToken(String token, long createTime, long expiredTimeInterval) {
this.token = token;
this.createTime = createTime;
this.expiredTimeInterval = expiredTimeInterval;
}
public static AuthToken createToken(String appId, String secret, long createTime, String baseUrl, Map<String, String> params) {
String original = appId + secret + baseUrl + MapUtil.sortJoin(params, "&", "=", true) + createTime;
String token = MD5.digestHex(original, StandardCharsets.UTF_8);
System.out.println(new AuthToken(token, createTime));
return new AuthToken(token, createTime);
}
public boolean isExpired() {
return System.currentTimeMillis() < this.createTime + DEFAULT_EXPIRE_TIME_INTERVAL;
}
}
4.yml文件
yml
server:
port: 12886
application:
name: bgn-demo-api
main:
allow-bean-definition-overriding: true
#接口鉴权密文
apiAuthToken:
secret: 73****************************55c4
5.使用的注解接口鉴权
java
@Slf4j
@RestController
@RequestMapping("/order")
public class DemoApiController {
@Resource
private DemoApiService demoApiService;
/**
* 查询出票订单列表
* @return
*/
@GetMapping(value = "/list")
@ApiOperation("出票订单列表接口")
@Authentication
public AjaxResult ticketingList(@RequestParam("siteName") String siteName,
@RequestParam("status") String status,
@RequestParam("startTime") String startTime,
@RequestParam("endTime") String endTime,
HttpServletRequest request)
{
return demoApiService.list(siteName, status, startTime, endTime);
}
}