diff --git a/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/ApiConfig.java b/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/ApiConfig.java index 8196286..71aa01b 100644 --- a/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/ApiConfig.java +++ b/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/ApiConfig.java @@ -1,5 +1,6 @@ package com.techsor.datacenter.business.configurator; +import com.techsor.datacenter.business.configurator.interceptor.ApiTokenInterceptor; import org.springframework.boot.web.servlet.MultipartConfigFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -24,6 +25,11 @@ public class ApiConfig implements WebMvcConfigurer { public AccessApiInterceptor accessApiInterceptor(){ return new AccessApiInterceptor(); } + + @Bean + public ApiTokenInterceptor apiTokenInterceptor(){ + return new ApiTokenInterceptor(); + } /** * 加了配置spring.web.resources.add-mappings=false @@ -42,6 +48,7 @@ public class ApiConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(accessApiInterceptor()); + registry.addInterceptor(apiTokenInterceptor()); } @Bean diff --git a/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/interceptor/ApiTokenInterceptor.java b/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/interceptor/ApiTokenInterceptor.java new file mode 100644 index 0000000..ab64ee4 --- /dev/null +++ b/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/interceptor/ApiTokenInterceptor.java @@ -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(); + } +} + diff --git a/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/interceptor/ApiTokenRequired.java b/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/interceptor/ApiTokenRequired.java new file mode 100644 index 0000000..98490af --- /dev/null +++ b/data-center-business-controller/src/main/java/com/techsor/datacenter/business/configurator/interceptor/ApiTokenRequired.java @@ -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 { +} + diff --git a/data-center-business-controller/src/main/java/com/techsor/datacenter/business/controller/CommonController.java b/data-center-business-controller/src/main/java/com/techsor/datacenter/business/controller/CommonController.java index 4566baa..dd4bfc4 100644 --- a/data-center-business-controller/src/main/java/com/techsor/datacenter/business/controller/CommonController.java +++ b/data-center-business-controller/src/main/java/com/techsor/datacenter/business/controller/CommonController.java @@ -6,6 +6,8 @@ import com.techsor.datacenter.business.configurator.interceptor.AccessRequired; import com.techsor.datacenter.business.dto.common.RepostRoidParams; import com.techsor.datacenter.business.dto.common.api.*; import com.techsor.datacenter.business.dto.common.roidproblemreport.ProblemReportsSummariesSearchParams; +import com.techsor.datacenter.business.service.ApiAuthService; +import com.techsor.datacenter.business.vo.common.ApiTokenVO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -60,6 +62,8 @@ public class CommonController{ @Autowired private CommonService commonService; + @Autowired + private ApiAuthService apiAuthService; @Value("${open.api.mock}") private String openApiMock; @@ -393,4 +397,10 @@ public class CommonController{ @Parameter(name = "Apikey", description = "API key", required = false, schema = @Schema(defaultValue = "123456")) @RequestHeader(required = true) String Apikey) { return commonService.queryCancelAlarmDevice(null, Apikey, apiAlarmDeviceSearchParams); } + + @PostMapping("/auth/token") + public SimpleDataResponse getToken( + @RequestHeader("X-API-KEY") String apiKey) { + return apiAuthService.generateToken(apiKey); + } } diff --git a/data-center-business-controller/src/main/resources/config/application.properties b/data-center-business-controller/src/main/resources/config/application.properties index 737b7f9..ad911dc 100644 --- a/data-center-business-controller/src/main/resources/config/application.properties +++ b/data-center-business-controller/src/main/resources/config/application.properties @@ -33,7 +33,7 @@ logging_appender=${loggingAppender:STDOUT} logging_maxHistory=30 logging_maxFileSize=100MB -user.login.keytimeout=3600 +user.login.keytimeout=43200 #集群模式cluster spring.redis.cluster.nodes=192.168.0.30:7000,192.168.0.30:7001 diff --git a/data-center-business-model/src/main/java/com/techsor/datacenter/business/vo/common/ApiTokenVO.java b/data-center-business-model/src/main/java/com/techsor/datacenter/business/vo/common/ApiTokenVO.java new file mode 100644 index 0000000..54cd5b2 --- /dev/null +++ b/data-center-business-model/src/main/java/com/techsor/datacenter/business/vo/common/ApiTokenVO.java @@ -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; +} + diff --git a/data-center-business-model/src/main/java/com/techsor/datacenter/business/vo/common/RedisApiTokenInfo.java b/data-center-business-model/src/main/java/com/techsor/datacenter/business/vo/common/RedisApiTokenInfo.java new file mode 100644 index 0000000..bd21924 --- /dev/null +++ b/data-center-business-model/src/main/java/com/techsor/datacenter/business/vo/common/RedisApiTokenInfo.java @@ -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; +} + diff --git a/data-center-business-service/src/main/java/com/techsor/datacenter/business/service/ApiAuthService.java b/data-center-business-service/src/main/java/com/techsor/datacenter/business/service/ApiAuthService.java new file mode 100644 index 0000000..de0e57b --- /dev/null +++ b/data-center-business-service/src/main/java/com/techsor/datacenter/business/service/ApiAuthService.java @@ -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 generateToken(String apiKey); + +} diff --git a/data-center-business-service/src/main/java/com/techsor/datacenter/business/service/impl/ApiAuthServiceImpl.java b/data-center-business-service/src/main/java/com/techsor/datacenter/business/service/impl/ApiAuthServiceImpl.java new file mode 100644 index 0000000..2582e25 --- /dev/null +++ b/data-center-business-service/src/main/java/com/techsor/datacenter/business/service/impl/ApiAuthServiceImpl.java @@ -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 generateToken(String apiKey) { + + Map 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)); + } +} + diff --git a/data-center-business-util/src/main/java/com/techsor/datacenter/business/util/ApiContext.java b/data-center-business-util/src/main/java/com/techsor/datacenter/business/util/ApiContext.java new file mode 100644 index 0000000..60f292b --- /dev/null +++ b/data-center-business-util/src/main/java/com/techsor/datacenter/business/util/ApiContext.java @@ -0,0 +1,19 @@ +package com.techsor.datacenter.business.util; + +public class ApiContext { + + private static final ThreadLocal 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(); + } +} + diff --git a/data-center-business-util/src/main/java/com/techsor/datacenter/business/util/SimpleJwtTokenUtil.java b/data-center-business-util/src/main/java/com/techsor/datacenter/business/util/SimpleJwtTokenUtil.java new file mode 100644 index 0000000..508441f --- /dev/null +++ b/data-center-business-util/src/main/java/com/techsor/datacenter/business/util/SimpleJwtTokenUtil.java @@ -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()); + } +} +