11 changed files with 305 additions and 1 deletions
@ -0,0 +1,57 @@ |
|||||
|
package com.techsor.datacenter.business.configurator.interceptor; |
||||
|
|
||||
|
import com.alibaba.fastjson2.JSON; |
||||
|
import com.techsor.datacenter.business.util.ApiContext; |
||||
|
import com.techsor.datacenter.business.util.redis.RedisUtil; |
||||
|
import com.techsor.datacenter.business.vo.common.RedisApiTokenInfo; |
||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.springframework.web.servlet.HandlerInterceptor; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Component |
||||
|
public class ApiTokenInterceptor implements HandlerInterceptor { |
||||
|
|
||||
|
@Autowired |
||||
|
private RedisUtil redisUtil; |
||||
|
|
||||
|
@Override |
||||
|
public boolean preHandle(HttpServletRequest request, |
||||
|
HttpServletResponse response, |
||||
|
Object handler) throws Exception { |
||||
|
|
||||
|
String auth = request.getHeader("Authorization"); |
||||
|
if (auth == null || !auth.startsWith("Bearer ")) { |
||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
String token = auth.substring(7); |
||||
|
String redisKey = "api:token:" + token; |
||||
|
|
||||
|
Object tokenInfo = redisUtil.get(redisKey); |
||||
|
if (null == tokenInfo){ |
||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
RedisApiTokenInfo redisApiTokenInfo = JSON.parseObject(tokenInfo.toString(), RedisApiTokenInfo.class); |
||||
|
|
||||
|
Long companyId = redisApiTokenInfo.getTopCompanyId(); |
||||
|
ApiContext.setCompanyId(companyId); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void afterCompletion(HttpServletRequest request, |
||||
|
HttpServletResponse response, |
||||
|
Object handler, |
||||
|
Exception ex) { |
||||
|
ApiContext.clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,9 @@ |
|||||
|
package com.techsor.datacenter.business.configurator.interceptor; |
||||
|
|
||||
|
import java.lang.annotation.*; |
||||
|
|
||||
|
@Target({ElementType.METHOD, ElementType.TYPE}) |
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
public @interface ApiTokenRequired { |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,12 @@ |
|||||
|
package com.techsor.datacenter.business.vo.common; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
public class ApiTokenVO { |
||||
|
private String token; |
||||
|
private Long expireTime; |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,12 @@ |
|||||
|
package com.techsor.datacenter.business.vo.common; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
public class RedisApiTokenInfo { |
||||
|
private String apiKey; |
||||
|
private Long topCompanyId; |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,10 @@ |
|||||
|
package com.techsor.datacenter.business.service; |
||||
|
|
||||
|
import com.techsor.datacenter.business.common.response.SimpleDataResponse; |
||||
|
import com.techsor.datacenter.business.vo.common.ApiTokenVO; |
||||
|
|
||||
|
public interface ApiAuthService { |
||||
|
|
||||
|
SimpleDataResponse<ApiTokenVO> generateToken(String apiKey); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
package com.techsor.datacenter.business.service.impl; |
||||
|
|
||||
|
import com.alibaba.fastjson2.JSON; |
||||
|
import com.techsor.datacenter.business.common.config.DataSourceInterceptor; |
||||
|
import com.techsor.datacenter.business.common.response.ResponseCode; |
||||
|
import com.techsor.datacenter.business.common.response.SimpleDataResponse; |
||||
|
import com.techsor.datacenter.business.dao.ex.BasicCompanyMapperExt; |
||||
|
import com.techsor.datacenter.business.service.ApiAuthService; |
||||
|
import com.techsor.datacenter.business.util.SimpleJwtTokenUtil; |
||||
|
import com.techsor.datacenter.business.util.redis.RedisUtil; |
||||
|
import com.techsor.datacenter.business.vo.common.ApiTokenVO; |
||||
|
import com.techsor.datacenter.business.vo.common.RedisApiTokenInfo; |
||||
|
import com.techsor.datacenter.business.vo.company.ApikeyInfo2; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Service |
||||
|
public class ApiAuthServiceImpl implements ApiAuthService { |
||||
|
|
||||
|
private static final long TOKEN_EXPIRE_SECONDS = 12 * 60 * 60; |
||||
|
|
||||
|
|
||||
|
@Autowired |
||||
|
RedisUtil redisUtil; |
||||
|
|
||||
|
@Autowired |
||||
|
private BasicCompanyMapperExt basicCompanyMapperExt; |
||||
|
@Autowired |
||||
|
private DataSourceInterceptor dataSourceInterceptor; |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public SimpleDataResponse<ApiTokenVO> generateToken(String apiKey) { |
||||
|
|
||||
|
Map<String, Object> paramMap = new HashMap<>(); |
||||
|
paramMap.put("apikey", apiKey); |
||||
|
ApikeyInfo2 selfInfo = basicCompanyMapperExt.getAuroraInfoByApikey(paramMap); |
||||
|
if (selfInfo == null) { |
||||
|
return SimpleDataResponse.fail(ResponseCode.MSG_ERROR, "Invalid API Key"); |
||||
|
} |
||||
|
Long companyId = selfInfo.getId(); |
||||
|
|
||||
|
Long topCompanyId = dataSourceInterceptor.getTopCompanyId(companyId+""); |
||||
|
|
||||
|
String token = "Bearer " + SimpleJwtTokenUtil.generate(); |
||||
|
|
||||
|
String redisKey = "api:token:" + token; |
||||
|
|
||||
|
redisUtil.set(redisKey, JSON.toJSONString(new RedisApiTokenInfo(apiKey, topCompanyId))); |
||||
|
redisUtil.expire(redisKey, TOKEN_EXPIRE_SECONDS); |
||||
|
|
||||
|
return SimpleDataResponse.success(new ApiTokenVO(token, TOKEN_EXPIRE_SECONDS)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.techsor.datacenter.business.util; |
||||
|
|
||||
|
public class ApiContext { |
||||
|
|
||||
|
private static final ThreadLocal<Long> COMPANY_ID_HOLDER = new ThreadLocal<>(); |
||||
|
|
||||
|
public static void setCompanyId(Long companyId) { |
||||
|
COMPANY_ID_HOLDER.set(companyId); |
||||
|
} |
||||
|
|
||||
|
public static Long getCompanyId() { |
||||
|
return COMPANY_ID_HOLDER.get(); |
||||
|
} |
||||
|
|
||||
|
public static void clear() { |
||||
|
COMPANY_ID_HOLDER.remove(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,109 @@ |
|||||
|
package com.techsor.datacenter.business.util; |
||||
|
|
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import javax.crypto.Mac; |
||||
|
import javax.crypto.spec.SecretKeySpec; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.security.MessageDigest; |
||||
|
import java.util.Base64; |
||||
|
|
||||
|
|
||||
|
public class SimpleJwtTokenUtil { |
||||
|
|
||||
|
private static final String HMAC_ALGO = "HmacSHA256"; |
||||
|
|
||||
|
private static final String subject = "company-from-Japan-aeon"; |
||||
|
|
||||
|
private static final String secret = "ci3b512jwy1949pla"; |
||||
|
|
||||
|
private SimpleJwtTokenUtil() { |
||||
|
} |
||||
|
|
||||
|
public static String generate() { |
||||
|
return generate(subject, secret); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成一个 JWT 格式的 token(无过期) |
||||
|
* |
||||
|
* @param subject 业务唯一标识(userId / clientId / appId) |
||||
|
* @param secret 签名密钥 |
||||
|
*/ |
||||
|
public static String generate(String subject, String secret) { |
||||
|
// header 固定
|
||||
|
String headerJson = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; |
||||
|
|
||||
|
// payload 只放你需要的最小信息
|
||||
|
String payloadJson = String.format("{\"sub\":\"%s\"}", subject); |
||||
|
|
||||
|
String header = base64Url(headerJson.getBytes(StandardCharsets.UTF_8)); |
||||
|
String payload = base64Url(payloadJson.getBytes(StandardCharsets.UTF_8)); |
||||
|
|
||||
|
String data = header + "." + payload; |
||||
|
String signature = base64Url(hmac(secret, data)); |
||||
|
|
||||
|
return data + "." + signature; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 校验 token 是否被伪造(只校验签名) |
||||
|
*/ |
||||
|
public static boolean verify(String token, String secret) { |
||||
|
String[] parts = token.split("\\."); |
||||
|
if (parts.length != 3) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
String data = parts[0] + "." + parts[1]; |
||||
|
String expectedSig = base64Url(hmac(secret, data)); |
||||
|
|
||||
|
return MessageDigest.isEqual( |
||||
|
expectedSig.getBytes(StandardCharsets.UTF_8), |
||||
|
parts[2].getBytes(StandardCharsets.UTF_8) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解析 subject(不做合法性校验) |
||||
|
*/ |
||||
|
public static String getSubject(String token) { |
||||
|
String[] parts = token.split("\\."); |
||||
|
if (parts.length != 3) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
String json = new String( |
||||
|
Base64.getUrlDecoder().decode(parts[1]), |
||||
|
StandardCharsets.UTF_8 |
||||
|
); |
||||
|
|
||||
|
// {"sub":"xxx"}
|
||||
|
int start = json.indexOf(":\"") + 2; |
||||
|
int end = json.lastIndexOf("\""); |
||||
|
return start > 1 && end > start ? json.substring(start, end) : null; |
||||
|
} |
||||
|
|
||||
|
/* ================= 内部方法 ================= */ |
||||
|
|
||||
|
private static byte[] hmac(String secret, String data) { |
||||
|
try { |
||||
|
Mac mac = Mac.getInstance(HMAC_ALGO); |
||||
|
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGO)); |
||||
|
return mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); |
||||
|
} catch (Exception e) { |
||||
|
throw new RuntimeException("HMAC error", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static String base64Url(byte[] bytes) { |
||||
|
return Base64.getUrlEncoder() |
||||
|
.withoutPadding() |
||||
|
.encodeToString(bytes); |
||||
|
} |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
System.out.println(SimpleJwtTokenUtil.generate()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
Loading…
Reference in new issue