feat: 额外对国培AI对话接口的鉴权

11111
winter 9 months ago
parent 9aec66d1a7
commit acf448e980

@ -32,6 +32,11 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId> <artifactId>spring-boot-starter-aop</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>4.6.0</version>
</dependency>
<!-- Spring Security OAuth2 resource server --> <!-- Spring Security OAuth2 resource server -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

@ -2,7 +2,7 @@ package cn.teammodel.config.exception;
import cn.teammodel.common.ErrorCode; import cn.teammodel.common.ErrorCode;
import cn.teammodel.common.R; import cn.teammodel.common.R;
import cn.teammodel.manager.NotificationService; import cn.teammodel.manager.notification.NotificationService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;

@ -3,7 +3,7 @@ package cn.teammodel.config.intercepter;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import cn.hutool.extra.servlet.ServletUtil; import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.jwt.JWTUtil; import cn.hutool.jwt.JWTUtil;
import cn.teammodel.manager.NotificationService; import cn.teammodel.manager.notification.NotificationService;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
@ -11,6 +11,8 @@ import lombok.extern.slf4j.Slf4j;
import okhttp3.*; import okhttp3.*;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
@ -32,13 +34,22 @@ import java.util.List;
@Component @Component
@Slf4j @Slf4j
public class UploadApiLogInterceptor implements HandlerInterceptor { public class UploadApiLogInterceptor implements HandlerInterceptor {
@Value("${spring.env}")
private String env;
@Resource @Resource
private NotificationService notificationService; private NotificationService notificationService;
private final ObjectMapper mapper = new ObjectMapper(); private final ObjectMapper mapper = new ObjectMapper();
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
if (!env.equals("dev")) {
doUploadLog(request);
}
return true;
}
private void doUploadLog(@NotNull HttpServletRequest request) throws IOException {
String ip = ServletUtil.getClientIP(request); String ip = ServletUtil.getClientIP(request);
String client = ""; String client = "";
String tokenSha = ""; String tokenSha = "";
@ -115,7 +126,7 @@ public class UploadApiLogInterceptor implements HandlerInterceptor {
okHttpClient.newCall(okRequest).enqueue(new Callback() { okHttpClient.newCall(okRequest).enqueue(new Callback() {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(Call call, IOException e) {
log.error("UploadApiLogIntercepter error" ) ; log.error("UploadApiLogIntercepter error") ;
notificationService.send("日志上传告警: 请求发送失败,检查请求发送客户端"); notificationService.send("日志上传告警: 请求发送失败,检查请求发送客户端");
} }
@ -129,6 +140,5 @@ public class UploadApiLogInterceptor implements HandlerInterceptor {
} }
} }
}); });
return true;
} }
} }

@ -20,7 +20,7 @@ import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@RestController @RestController
@RequestMapping("public/ai") @RequestMapping("/ai/api")
@Api(tags = "AI 能力") @Api(tags = "AI 能力")
public class AiController { public class AiController {
@Resource @Resource

@ -1,44 +1,44 @@
package cn.teammodel.manager; package cn.teammodel.manager.notification;
import com.dingtalk.api.DefaultDingTalkClient; import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient; import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest; import com.dingtalk.api.request.OapiRobotSendRequest;
import com.taobao.api.ApiException; import com.taobao.api.ApiException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* *
* @author winter * @author winter
* @create 2023-11-14 10:11 * @create 2023-11-14 10:11
*/ */
@Component @Component
@Slf4j @Slf4j
public class DingAlertNotifier implements NotificationService{ public class DingAlertNotifier implements NotificationService{
private final DingTalkClient client; private final DingTalkClient client;
@Autowired @Autowired
public DingAlertNotifier(@Value("${ding.server-url}") String dingServerUrl) { public DingAlertNotifier(@Value("${ding.server-url}") String dingServerUrl) {
this.client = new DefaultDingTalkClient(dingServerUrl); this.client = new DefaultDingTalkClient(dingServerUrl);
} }
@Override @Override
public void send(String message) { public void send(String message) {
OapiRobotSendRequest request = new OapiRobotSendRequest(); OapiRobotSendRequest request = new OapiRobotSendRequest();
// 文本消息 // 文本消息
request.setMsgtype("text"); request.setMsgtype("text");
OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text(); OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
text.setContent(message); text.setContent(message);
request.setText(text); request.setText(text);
// at 管理员提醒异常 // at 管理员提醒异常
OapiRobotSendRequest.At atAll = new OapiRobotSendRequest.At(); OapiRobotSendRequest.At atAll = new OapiRobotSendRequest.At();
atAll.setIsAtAll(true); atAll.setIsAtAll(true);
request.setAt(atAll); request.setAt(atAll);
try { try {
client.execute(request); client.execute(request);
} catch (ApiException e) { } catch (ApiException e) {
log.error("钉钉 robot 推送消息渠道异常: {}", e.getMessage()); log.error("钉钉 robot 推送消息渠道异常: {}", e.getMessage());
} }
} }
} }

@ -1,16 +1,16 @@
package cn.teammodel.manager; package cn.teammodel.manager.notification;
/** /**
* *
* @author winter * @author winter
* @create 2023-11-14 10:08 * @create 2023-11-14 10:08
*/ */
public interface NotificationService { public interface NotificationService {
/** /**
* *
* @author: winter * @author: winter
* @date: 2023/11/14 10:09 * @date: 2023/11/14 10:09
*/ */
void send(String message); void send(String message);
} }

@ -0,0 +1,12 @@
package cn.teammodel.manager.wx;
import org.springframework.stereotype.Service;
/**
* @author winter
* @create 2024-03-26 11:08
*/
@Service
public class MiniProgramSevice {
}

@ -1,10 +1,12 @@
package cn.teammodel.security; package cn.teammodel.security;
import cn.teammodel.security.filter.ApiAuthTokenFilter;
import cn.teammodel.security.filter.AuthInnerTokenFilter; import cn.teammodel.security.filter.AuthInnerTokenFilter;
import cn.teammodel.security.handler.RestAccessDeniedHandler; import cn.teammodel.security.handler.RestAccessDeniedHandler;
import cn.teammodel.security.handler.RestAuthenticationEntryPoint; import cn.teammodel.security.handler.RestAuthenticationEntryPoint;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@ -31,8 +33,11 @@ public class SecurityConfiguration {
private RestAuthenticationEntryPoint restAuthenticationEntryPoint; private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Resource @Resource
private AuthInnerTokenFilter authInnerTokenFilter; private AuthInnerTokenFilter authInnerTokenFilter;
@Resource
private ApiAuthTokenFilter apiAuthTokenFilter;
@Bean @Bean
@Order(2)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http http
// CSRF禁用因为不使用session // CSRF禁用因为不使用session
@ -64,7 +69,18 @@ public class SecurityConfiguration {
.exceptionHandling() .exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint) .authenticationEntryPoint(restAuthenticationEntryPoint)
.accessDeniedHandler(restAccessDeniedHandler); .accessDeniedHandler(restAccessDeniedHandler);
return http.build();
}
@Bean
@Order(1)
public SecurityFilterChain outterApiFilterChain(HttpSecurity http) throws Exception {
http.
antMatcher("/ai/api/**")
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.addFilterAfter(apiAuthTokenFilter, BearerTokenAuthenticationFilter.class);
return http.build(); return http.build();
} }
@ -86,8 +102,8 @@ public class SecurityConfiguration {
} }
/** /**
* *
*/ */
@Bean @Bean
public CorsConfigurationSource corsConfigurationSource() { public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration(); CorsConfiguration configuration = new CorsConfiguration();

@ -0,0 +1,54 @@
package cn.teammodel.security.filter;
import cn.teammodel.security.utils.JwtTokenUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* AI chat
* @author winter
* @create 2023-11-09 10:43
*/
@Component
@Slf4j
public class ApiAuthTokenFilter extends OncePerRequestFilter {
@Resource
private JwtTokenUtil jwtTokenUtil;
// todo: 修改 context 的值 + 写一下多过滤器链的复盘
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
SecurityContext context = SecurityContextHolder.getContext();
// 验证 authToken 合法
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
filterChain.doFilter(request, response);
return;
}
Claims claims = jwtTokenUtil.validAndGetClaims(token, "fXO6ko/qyXeYrkecPeKdgXnuLXf9vMEtnBC9OB3s+aA=", 315360000);
if (claims == null) {
SecurityContextHolder.clearContext(); // 验证失败不应该在此处抛出异常,应该维护好 context 的值,以便整个过滤器链正常运行
filterChain.doFilter(request, response);
return;
}
// 组装 authToken 的 jwt 进 authentication
UsernamePasswordAuthenticationToken finalAuthentication = new UsernamePasswordAuthenticationToken(claims, null, null);
context.setAuthentication(finalAuthentication);
filterChain.doFilter(request, response);
}
}

@ -74,11 +74,15 @@ public class JwtTokenUtil {
* @return claimsnull * @return claimsnull
*/ */
private Claims getClaimsFromToken(String token) { private Claims getClaimsFromToken(String token) {
return validAndGetClaims(token, secret, NEVER_EXPIRE);
}
public Claims validAndGetClaims(String token, String key, long expire) {
Claims claims = null; Claims claims = null;
try { try {
claims = Jwts.parser() claims = Jwts.parser()
.setSigningKey(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")) .setSigningKey(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"))
.setAllowedClockSkewSeconds(NEVER_EXPIRE) .setAllowedClockSkewSeconds(expire)
.parseClaimsJws(token) .parseClaimsJws(token)
.getBody(); .getBody();
} catch (Exception e) { } catch (Exception e) {

@ -1,4 +1,5 @@
spring: spring:
env: dev
mvc: mvc:
pathmatch: pathmatch:
matching-strategy: ant_path_matcher matching-strategy: ant_path_matcher

@ -3,7 +3,7 @@ package cn.teammodel;
import cn.teammodel.common.PK; import cn.teammodel.common.PK;
import cn.teammodel.controller.admin.service.AdminAppraiseService; import cn.teammodel.controller.admin.service.AdminAppraiseService;
import cn.teammodel.repository.*; import cn.teammodel.repository.*;
import cn.teammodel.manager.DingAlertNotifier; import cn.teammodel.manager.notification.DingAlertNotifier;
import cn.teammodel.model.dto.admin.appraise.TimeRangeDto; import cn.teammodel.model.dto.admin.appraise.TimeRangeDto;
import cn.teammodel.model.dto.admin.appraise.UpdateAchievementRuleDto; import cn.teammodel.model.dto.admin.appraise.UpdateAchievementRuleDto;
import cn.teammodel.model.entity.appraise.*; import cn.teammodel.model.entity.appraise.*;

@ -1,6 +1,5 @@
package cn.teammodel; package cn.teammodel;
import cn.hutool.jwt.JWTUtil;
import cn.teammodel.common.FiveEducations; import cn.teammodel.common.FiveEducations;
import cn.teammodel.model.entity.appraise.Appraise; import cn.teammodel.model.entity.appraise.Appraise;
import cn.teammodel.model.entity.appraise.AppraiseTreeNode; import cn.teammodel.model.entity.appraise.AppraiseTreeNode;
@ -12,9 +11,13 @@ import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest; import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse; import com.dingtalk.api.response.OapiRobotSendResponse;
import com.taobao.api.ApiException; import com.taobao.api.ApiException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -33,8 +36,20 @@ public class TestWithoutSpring {
@Test @Test
public void testJwtDecode() { public void testJwtDecode() {
// 解析 JWT不验证签 // 解析 JWT不验证签
Object roles = JWTUtil.parseToken("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlR4OFo1d2lkT2VSdDVCSGdWX2lvdXpzbjd6OCJ9.eyJhdWQiOiI3MjY0MzcwNC1iMmU3LTRiMjYtYjg4MS1iZDU4NjVlN2E3YTUiLCJpc3MiOiJodHRwczovL2xvZ2luLnBhcnRuZXIubWljcm9zb2Z0b25saW5lLmNuLzQ4MDdlOWNmLTg3YjgtNDE3NC1hYTViLWU3NjQ5N2Q3MzkyYi92Mi4wIiwiaWF0IjoxNzEwNzQ0MzA4LCJuYmYiOjE3MTA3NDQzMDgsImV4cCI6MTcxMDc0ODIwOCwiYWlvIjoiNDJaZ1lPQS9KL0t0ZTIzdWFzN2dEMGFPdW80WHpmdjlmMXhUREp6ZlZsNzk0VWkwWndVQSIsImF6cCI6ImM3MzE3Zjg4LTdjZWEtNGU0OC1hYzU3LWExNjA3MWY3Yjg4NCIsImF6cGFjciI6IjEiLCJvaWQiOiJkNDliYjc4Mi02ZGMyLTQ5MzktODY3OC1kMzNjMjY3MTliZjkiLCJyaCI6IjAuREFJQXota0hTTGlIZEVHcVctZGtsOWM1S3dRM1pITG5zaVpMdUlHOVdHWG5wNlVCQUFBLiIsInJvbGVzIjpbIklFUyJdLCJzdWIiOiJkNDliYjc4Mi02ZGMyLTQ5MzktODY3OC1kMzNjMjY3MTliZjkiLCJ0aWQiOiI0ODA3ZTljZi04N2I4LTQxNzQtYWE1Yi1lNzY0OTdkNzM5MmIiLCJ1dGkiOiJ6MGhKdDg0c1UwMlpxNTJNdGNRS0FBIiwidmVyIjoiMi4wIn0.U9KWPccJQ4ZgfzHBNTK6swuEJvkZM8ebfrOpAl7uDoNynlVEsp8CcQ2a1PIO3V8X8tcNassCplc08qbCqcL49R3S5vP2bWTiucT7c_fyCogxMYpmRM0RmGz8RF1tFF3VAxG4_q1Gt9LgzLbNhxxCQcbgJuZhUMKAP246esHlJrkue88FwgmMxf_rwxDH8VwbAchHtSuIRbj3zYSzgKU0Lpmibrr88g_gQZ061f4RyRezTmKLoOqhoGjEosOOnXOdI67YyxpdMjm64uzc92fNUyC9PHvu8XbTTbdUlkU2lPgrk5xFfyVus9SdxWQXB4PCIDhcBsoVQQlWGmY-kvw_8w").getPayload("roles"); // Object roles = JWTUtil.parseToken("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlR4OFo1d2lkT2VSdDVCSGdWX2lvdXpzbjd6OCJ9.eyJhdWQiOiI3MjY0MzcwNC1iMmU3LTRiMjYtYjg4MS1iZDU4NjVlN2E3YTUiLCJpc3MiOiJodHRwczovL2xvZ2luLnBhcnRuZXIubWljcm9zb2Z0b25saW5lLmNuLzQ4MDdlOWNmLTg3YjgtNDE3NC1hYTViLWU3NjQ5N2Q3MzkyYi92Mi4wIiwiaWF0IjoxNzEwNzQ0MzA4LCJuYmYiOjE3MTA3NDQzMDgsImV4cCI6MTcxMDc0ODIwOCwiYWlvIjoiNDJaZ1lPQS9KL0t0ZTIzdWFzN2dEMGFPdW80WHpmdjlmMXhUREp6ZlZsNzk0VWkwWndVQSIsImF6cCI6ImM3MzE3Zjg4LTdjZWEtNGU0OC1hYzU3LWExNjA3MWY3Yjg4NCIsImF6cGFjciI6IjEiLCJvaWQiOiJkNDliYjc4Mi02ZGMyLTQ5MzktODY3OC1kMzNjMjY3MTliZjkiLCJyaCI6IjAuREFJQXota0hTTGlIZEVHcVctZGtsOWM1S3dRM1pITG5zaVpMdUlHOVdHWG5wNlVCQUFBLiIsInJvbGVzIjpbIklFUyJdLCJzdWIiOiJkNDliYjc4Mi02ZGMyLTQ5MzktODY3OC1kMzNjMjY3MTliZjkiLCJ0aWQiOiI0ODA3ZTljZi04N2I4LTQxNzQtYWE1Yi1lNzY0OTdkNzM5MmIiLCJ1dGkiOiJ6MGhKdDg0c1UwMlpxNTJNdGNRS0FBIiwidmVyIjoiMi4wIn0.U9KWPccJQ4ZgfzHBNTK6swuEJvkZM8ebfrOpAl7uDoNynlVEsp8CcQ2a1PIO3V8X8tcNassCplc08qbCqcL49R3S5vP2bWTiucT7c_fyCogxMYpmRM0RmGz8RF1tFF3VAxG4_q1Gt9LgzLbNhxxCQcbgJuZhUMKAP246esHlJrkue88FwgmMxf_rwxDH8VwbAchHtSuIRbj3zYSzgKU0Lpmibrr88g_gQZ061f4RyRezTmKLoOqhoGjEosOOnXOdI67YyxpdMjm64uzc92fNUyC9PHvu8XbTTbdUlkU2lPgrk5xFfyVus9SdxWQXB4PCIDhcBsoVQQlWGmY-kvw_8w").getPayload("roles");
System.out.println(roles); // System.out.println(roles);
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(new SecretKeySpec("fXO6ko/qyXeYrkecPeKdgXnuLXf9vMEtnBC9OB3s+aA=".getBytes(StandardCharsets.UTF_8), "HmacSHA256"))
.setAllowedClockSkewSeconds(315360000)
.parseClaimsJws("eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ3d3cud2ludGVhY2guY24iLCJzdWIiOiI1NTUzMzMwNzQ1OTU4NzY4NjQiLCJleHAiOiIxNzExNjE1MDIxIiwibmFtZSI6IuWRqOWTjeWGmyIsInBpY3R1cmUiOm51bGwsImNkanhqeSI6eyJuaWNrbmFtZSI6IuWRqOWTjeWGmyIsInBpY3R1cmUiOm51bGwsImJpbmRJZCI6IjE4NDgyMTMzMDk0Iiwib3BlbmlkIjpudWxsLCJ1bmlvbmlkIjoiMWE4ZWFkNDYtY2JkMS00Y2VlLTg2NzEtZTU0NmMwZmQ4NTMyIn19._6ltuihVcWopQQqfgpSLo3aMVKE3MJhb28NQBSL_w7s")
.getBody();
System.out.println(claims);
} catch (Exception e) {
log.warn("token 解析出错:{}",e.getMessage());
}
} }
@Test @Test

Loading…
Cancel
Save