新增 RequireAuthAndProxyAspect 切片, 用于确保一些需要鉴权的方法有合法鉴权

This commit is contained in:
2026-02-26 19:24:30 +08:00
parent 8f6c9af00f
commit c73b4e47cd
10 changed files with 102 additions and 29 deletions

View File

@@ -11,7 +11,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@EnableFeignClients
@SpringBootApplication
@EnableCaching(proxyTargetClass=true)
public class EmoneyAutoApplication {
public static void main(String[] args) {

View File

@@ -1,7 +1,7 @@
package quant.rich.emoney.component;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
@@ -20,9 +20,19 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
/**
* 注解在受 Spring 管理的类的成员方法上,使其在执行前校验代理状态、鉴权状态
* <p>需要开启 AOP在任意配置类上增加注解<i><code>@EnableAspectJAutoProxy</code></i>
*/
@Component
@Aspect
public class RequireAuthAndProxyAspect {
/**
* 调用链深度,用以判断同线程同类型切点命中的次数
*/
private static final ThreadLocal<Integer> DEPTH =
ThreadLocal.withInitial(() -> 0);
@Autowired
RequestInfoService requestInfoService;
@@ -30,9 +40,30 @@ public class RequireAuthAndProxyAspect {
@Autowired
ProxySettingService proxySettingService;
@Around("@annotation(quant.rich.emoney.component.RequireAuthAndProxyAspect.RequireAuthAndProxy)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
@Pointcut("@annotation(quant.rich.emoney.component.RequireAuthAndProxyAspect.RequireAuthAndProxy)")
public void pointCut() {}
@Before("pointCut()")
public void before(JoinPoint jp) throws Throwable {
int depth = DEPTH.get();
if (depth++ == 0) {
beforeRoot(jp);
}
DEPTH.set(depth);
}
@After("pointCut()")
public void after() {
int depth = DEPTH.get() - 1;
if (depth == 0) {
DEPTH.remove();
} else {
DEPTH.set(depth);
}
}
public void beforeRoot(JoinPoint jp) throws Throwable {
ProxySetting defualtProxySetting = proxySettingService.getDefaultProxySetting();
if (defualtProxySetting == null) {
@@ -45,7 +76,7 @@ public class RequireAuthAndProxyAspect {
throw new RuntimeException("需要配置默认请求信息");
}
MethodSignature signature = (MethodSignature) pjp.getSignature();
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
RequireAuthAndProxy annotation = method.getAnnotation(RequireAuthAndProxy.class);
@@ -60,13 +91,21 @@ public class RequireAuthAndProxyAspect {
else if (!EmoneyClient.reloginCheck()) {
throw new RuntimeException("检查重鉴权失败");
}
return pjp.proceed();
}
/**
* 在方法上添加此注解,则进入该方法前先校验 defaultRequestInfo 已鉴权、代理已配置,否则不允许进入方法
* <p>需要开启 AOP在任意配置类上增加注解<i><code>@EnableAspectJAutoProxy</code></i>
* <p>
* <b><font color="red">由于 AOP 特性使然, 必须为 public 方法才能生效</font></b>
* <p>
* 为防止反复重登录验证, 在同一调用链中的校验, 仅在首次调用时进行检查, 如:
* A、B 和 C 方法都添加了本注解, A 中调用 B, B 中调用 C, 则仅在 A 方法执行时才进行检查。
* <p>
* 为了安全起见, 无论 autoLogin 是否为 true, 只要添加了本注解, 在执行方法前, 最终都会进行
* EmoneyClient.reloginCheck(), 如果检查重鉴权失败, 被注解的方法将不会执行,
* 并由切片方法抛出异常。如果某个方法并不需要严格控制鉴权的, 可以不用本注解,
* 而是由在方法内自行编写逻辑来替代。
*
* @see RequireAuthAndProxyAspect
*/
@Target(ElementType.METHOD)
@@ -74,7 +113,8 @@ public class RequireAuthAndProxyAspect {
@Documented
public static @interface RequireAuthAndProxy {
/**
* 当存在默认请求配置但未鉴权时,是否自动鉴权
* 当存在默认请求配置但未鉴权时,是否自动鉴权。由于某些场景下,
* 鉴权信息需要系统统一维护,故此区分
* @return
*/
boolean autoLogin() default false;

View File

@@ -1,9 +1,11 @@
package quant.rich.emoney.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -17,8 +19,10 @@ import quant.rich.emoney.service.ConfigService;
* @author Doghole
*
*/
@EnableAspectJAutoProxy
@Configuration
@EnableAsync(proxyTargetClass=true)
@EnableCaching(proxyTargetClass=true)
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
@Import(ConfigAutoRegistrar.class)
public class EmoneyAutoConfig implements WebMvcConfigurer {

View File

@@ -1,4 +1,4 @@
package quant.rich.emoney.pojo;
package quant.rich.emoney.pojo.dto;
import java.util.ArrayList;
import java.util.Collections;
@@ -8,10 +8,14 @@ import java.util.Map;
import java.util.Set;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.experimental.Accessors;
@Validated
@Data
@Accessors(chain=true)
public class MultiIndexPlanDetail {
@@ -19,11 +23,14 @@ public class MultiIndexPlanDetail {
/**
* 指标
*/
@Valid
@NotEmpty
List<MultiIndexPlanPart> indexes;
/**
* 抓取的 K 线粒度
*/
@NotEmpty
List<Integer> periods;
/**
@@ -55,6 +62,7 @@ public class MultiIndexPlanDetail {
return periods;
}
@Validated
@Data
@Accessors(chain=true)
public static class MultiIndexPlanPart {

View File

@@ -1,4 +1,4 @@
package quant.rich.emoney.pojo;
package quant.rich.emoney.pojo.dto;
import java.util.ArrayList;
import java.util.Collections;

View File

@@ -23,6 +23,7 @@ import org.jsoup.nodes.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -71,6 +72,10 @@ public class IndexDetailService {
@Autowired
RequestInfoService requestInfoService;
@Lazy
@Autowired
IndexDetailService self;
static final String filePath = "./conf/extra/indexDetail/";
static final ObjectMapper mapper = new ObjectMapper();
static final Context jsContext = Context.create("js");
@@ -86,25 +91,24 @@ public class IndexDetailService {
* @return
*/
@CacheEvict(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()")
@RequireAuthAndProxy(autoLogin = true)
public IndexDetail forceRefreshAndGetIndexDetail(Serializable indexCode) {
// 刷新的本质就是从网络获取,因为此处已经清理了缓存,所以直接从网络获取后再
// 走一次 getIndexDetail获取到的就是从网络保存到了本地的此时缓存也更新了
if (!hasParams(indexCode)) {
getNonParamsIndexDetailOnline(indexCode);
self.getNonParamsIndexDetailOnline(indexCode);
}
else {
getParamsIndexDetailOnline(indexCode);
self.getParamsIndexDetailOnline(indexCode);
}
return getIndexDetail(indexCode);
// 此处用 self 是为了在本次调用时就设置缓存
return self.getIndexDetail(indexCode);
}
@Cacheable(cacheNames="@indexDetailService.getIndexDetail(Serializable)", key="#indexCode.toString()")
@RequireAuthAndProxy(autoLogin = true)
public IndexDetail getIndexDetail(Serializable indexCode) {
if (indexCode == null) {
@@ -136,7 +140,7 @@ public class IndexDetailService {
}
}
// 从网络获取
return getParamsIndexDetailOnline(indexCode);
return self.getParamsIndexDetailOnline(indexCode);
}
/**
@@ -146,7 +150,8 @@ public class IndexDetailService {
* @see RequestInfoService#getDefaultRequestInfo()
* @return
*/
private ParamsIndexDetail getParamsIndexDetailOnline(Serializable indexCode) {
@RequireAuthAndProxy(autoLogin = true)
public ParamsIndexDetail getParamsIndexDetailOnline(Serializable indexCode) {
RequestInfo requestInfo = requestInfoService.getDefaultRequestInfo();
if (requestInfo == null || StringUtils.isBlank(requestInfo.getAuthorization())) {
throw new RuntimeException("无法获取已鉴权的 RequestInfo");
@@ -221,26 +226,29 @@ public class IndexDetailService {
log.warn("无法获取本地无参数指标说明,将尝试重新从网络获取 indexCode: {}", indexCode, e);
}
if (detail != null) {
loadImages(detail);
self.loadImages(detail);
saveIndexDetail(detail);
return detail;
}
}
// 从网络获取
return getNonParamsIndexDetailOnline(indexCode);
return self.getNonParamsIndexDetailOnline(indexCode);
}
/**
* 从网络获取指定 indexCode 的无参指标详情
* <p>本例用到的 requestInfo 不需要 PatchOkHttp 覆写,但要求鉴权参数拼接到 url 中,故要求鉴权
* <p>会一并尝试获取其他在本地未有的无参指标</p>
* <p>由于本例需要鉴权, 故在方法上开启了 {@code @RequireAuthAndProxy}, 而本方法被类内其他方法调用, 需要 proxy 才能使 AOP 生效,
* 为了使 self 调用成功, 需要在任意配置类上添加 {@code @EnableAspectJAutoProxy(exposeProxy = true)}
* @param indexCode
* @see RequestInfo#getWebviewUserAgent()
* @see RequestInfoService#getDefaultRequestInfo()
* @return
*/
private NonParamsIndexDetail getNonParamsIndexDetailOnline(Serializable indexCode) {
String url = buildNonParamsIndexUrl(indexCode);
@RequireAuthAndProxy(autoLogin = true)
public NonParamsIndexDetail getNonParamsIndexDetailOnline(Serializable indexCode) {
String url = self.buildNonParamsIndexUrl(indexCode);
Request request = new Request.Builder()
.url(url)
.header("Host", "appstatic.emoney.cn")
@@ -370,7 +378,7 @@ public class IndexDetailService {
String path = getIndexDetailPath(detail);
// 判断是否是需求的 detail
if (indexCode.toString().equals(detail.getIndexCode())) {
loadImages(detail);
self.loadImages(detail);
targetDetail = detail;
}
// 清洗内容:凡是文本类型的内容的,都要清洗一遍,判断是否有脚本、
@@ -434,7 +442,8 @@ public class IndexDetailService {
* @return
* @see RequestInfo
*/
private NonParamsIndexDetail loadImages(NonParamsIndexDetail detail) {
@RequireAuthAndProxy(autoLogin = true)
public NonParamsIndexDetail loadImages(NonParamsIndexDetail detail) {
OkHttpClient client = OkHttpClientProvider.getInstance();
for (NonParamsIndexDetailData data : detail.getData()) {
String imageUrl = data.getImage();
@@ -465,7 +474,7 @@ public class IndexDetailService {
.header("Sec-Fetch-Mode", "no-cors")
.header("Sec-Fetch-Dest", "image")
.header("Accept-Encoding", "gzip, deflate")
.header("Referer", buildNonParamsIndexUrl(detail.getIndexCode()))
.header("Referer", self.buildNonParamsIndexUrl(detail.getIndexCode()))
.header("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7")
.build();
@@ -567,7 +576,8 @@ public class IndexDetailService {
* @return
* @see RequestInfo
*/
private String buildNonParamsIndexUrl(Serializable indexCode) {
@RequireAuthAndProxy(autoLogin = true)
public String buildNonParamsIndexUrl(Serializable indexCode) {
RequestInfo requestInfo = requestInfoService.getDefaultRequestInfo();
if (requestInfo == null || StringUtils.isBlank(requestInfo.getAuthorization())) {

View File

@@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import quant.rich.emoney.entity.sqlite.ProxySetting;
import quant.rich.emoney.interfaces.IValidator;
public class ProxySettingValidator implements IValidator, ConstraintValidator<ProxySettingValid, ProxySetting> {

View File

@@ -8,6 +8,7 @@ import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import quant.rich.emoney.entity.config.DeviceInfoConfig;
import quant.rich.emoney.entity.sqlite.RequestInfo;
import quant.rich.emoney.interfaces.IValidator;
public class RequestInfoValidator implements IValidator, ConstraintValidator<RequestInfoValid, RequestInfo> {

View File

@@ -1 +1,11 @@
{"id":"46","name":"DKBY","nameCode":"10008500","data":[{"title":"DKBY指标说明:","items":["多空博弈","1.当“多方”线向上与“空方”线“金叉”时为买点。","2.当“多方”线向下与“空方”线“死叉”时为卖点。"],"image":null}],"original":"{\"id\":46,\"data\":[{\"title\":\"DKBY指标说明:\",\"items\":[\"多空博弈\",\"1.当“多方”线向上与“空方”线“金叉”时为买点。\",\"2.当“多方”线向下与“空方”线“死叉”时为卖点。\"]}],\"name\":\"DKBY\",\"nameCode\":\"10008500\"}","indexCode":"10008500","indexName":"DKBY","details":[{"content":"DKBY指标说明:","type":"TITLE"},{"content":"多空博弈","type":"TEXT"},{"content":"1.当“多方”线向上与“空方”线“金叉”时为买点。","type":"TEXT"},{"content":"2.当“多方”线向下与“空方”线“死叉”时为卖点。","type":"TEXT"}]}
{
"id" : "46",
"name" : "DKBY",
"nameCode" : "10008500",
"data" : [ {
"title" : "DKBY指标说明:",
"items" : [ "多空博弈", "1.当“多方”线向上与“空方”线“金叉”时为买点。", "2.当“多方”线向下与“空方”线“死叉”时为卖点。" ],
"image" : null
} ],
"original" : "{\"id\":46,\"data\":[{\"title\":\"DKBY指标说明:\",\"items\":[\"多空博弈\",\"1.当“多方”线向上与“空方”线“金叉”时为买点。\",\"2.当“多方”线向下与“空方”线“死叉”时为卖点。\"]}],\"name\":\"DKBY\",\"nameCode\":\"10008500\"}"
}

Binary file not shown.