From 14b09f0b26d931ea2631fd5cb62e61288de7202b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E6=98=9F=E4=B8=8D=E6=98=AF=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E5=91=98?= <1031900093@qq.com> Date: Sat, 25 Apr 2026 14:50:55 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=92=8C=E7=BA=BF=E4=B8=8A=E7=8E=AF=E5=A2=83=E6=BC=94=E7=A4=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 + .../ai/auth/config/AdminAuthProperties.java | 32 +++++++ .../auth/config/AdminWebMvcConfiguration.java | 35 +++++++ .../ai/auth/config/PreviewModeProperties.java | 22 +++++ .../auth/controller/AdminAuthController.java | 43 +++++++++ .../javaup/ai/auth/dto/AdminLoginRequest.java | 31 +++++++ .../ai/auth/service/AdminAuthService.java | 16 ++++ .../service/impl/AdminAuthServiceImpl.java | 49 ++++++++++ .../ai/auth/support/AdminAuthInterceptor.java | 74 +++++++++++++++ .../ai/auth/support/AdminJwtTokenService.java | 53 +++++++++++ .../ai/auth/support/AdminRequestContext.java | 23 +++++ .../auth/support/PreviewModeInterceptor.java | 91 +++++++++++++++++++ .../org/javaup/ai/auth/vo/AdminLoginVo.java | 43 +++++++++ .../org/javaup/ai/auth/vo/AdminProfileVo.java | 21 +++++ .../src/main/resources/application.yaml | 8 ++ .../super-agent-common-frame/pom.xml | 15 +++ vue/src/api/api.js | 65 ++++++++++++- vue/src/utils/adminAuth.js | 71 ++++++++++++--- vue/src/views/AdminLoginView.vue | 62 +++++++------ vue/src/views/BusinessChatView.vue | 5 +- vue/src/views/admin/AdminLayoutView.vue | 17 +++- vue/vite.config.js | 4 + 22 files changed, 732 insertions(+), 50 deletions(-) create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/AdminAuthProperties.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/AdminWebMvcConfiguration.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/PreviewModeProperties.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/controller/AdminAuthController.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/dto/AdminLoginRequest.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/service/AdminAuthService.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/service/impl/AdminAuthServiceImpl.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminAuthInterceptor.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminJwtTokenService.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminRequestContext.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/PreviewModeInterceptor.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/vo/AdminLoginVo.java create mode 100644 super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/vo/AdminProfileVo.java diff --git a/pom.xml b/pom.xml index 7469bb4..ab45218 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,8 @@ 1.1.2.0 2.6 0.9.1 + 2.3.1 + 2.3.9 5.8.25 1.18.20 3.5.7 diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/AdminAuthProperties.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/AdminAuthProperties.java new file mode 100644 index 0000000..d67fe07 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/AdminAuthProperties.java @@ -0,0 +1,32 @@ +package org.javaup.ai.auth.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 后台管理登录配置。 + */ +@Data +@ConfigurationProperties(prefix = "app.admin-auth") +public class AdminAuthProperties { + + /** + * 后台登录用户名。 + */ + private String username = "admin"; + + /** + * 后台登录密码。 + */ + private String password = "admin123456"; + + /** + * JWT 签名密钥。 + */ + private String tokenSecret = "super-agent-admin-token-secret-change-me"; + + /** + * token 有效期,单位分钟。 + */ + private Long tokenExpireMinutes = 720L; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/AdminWebMvcConfiguration.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/AdminWebMvcConfiguration.java new file mode 100644 index 0000000..5ae3e32 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/AdminWebMvcConfiguration.java @@ -0,0 +1,35 @@ +package org.javaup.ai.auth.config; + +import org.javaup.ai.auth.support.AdminAuthInterceptor; +import org.javaup.ai.auth.support.PreviewModeInterceptor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 后台管理登录与预览模式的 MVC 配置。 + */ +@Configuration +@EnableConfigurationProperties({AdminAuthProperties.class, PreviewModeProperties.class}) +public class AdminWebMvcConfiguration implements WebMvcConfigurer { + + private final AdminAuthInterceptor adminAuthInterceptor; + + private final PreviewModeInterceptor previewModeInterceptor; + + public AdminWebMvcConfiguration(AdminAuthInterceptor adminAuthInterceptor, + PreviewModeInterceptor previewModeInterceptor) { + this.adminAuthInterceptor = adminAuthInterceptor; + this.previewModeInterceptor = previewModeInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminAuthInterceptor) + .addPathPatterns("/manage/**", "/admin/auth/me"); + + registry.addInterceptor(previewModeInterceptor) + .addPathPatterns("/**"); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/PreviewModeProperties.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/PreviewModeProperties.java new file mode 100644 index 0000000..06631fd --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/config/PreviewModeProperties.java @@ -0,0 +1,22 @@ +package org.javaup.ai.auth.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 线上演示只读模式配置。 + */ +@Data +@ConfigurationProperties(prefix = "app.preview-mode") +public class PreviewModeProperties { + + /** + * 是否开启只读展示模式。 + */ + private Boolean enabled = Boolean.FALSE; + + /** + * 只读模式提示语。 + */ + private String message = "当前环境为只读展示模式,仅开放浏览与检索能力"; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/controller/AdminAuthController.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/controller/AdminAuthController.java new file mode 100644 index 0000000..94ee65c --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/controller/AdminAuthController.java @@ -0,0 +1,43 @@ +package org.javaup.ai.auth.controller; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.javaup.ai.auth.dto.AdminLoginRequest; +import org.javaup.ai.auth.service.AdminAuthService; +import org.javaup.ai.auth.vo.AdminLoginVo; +import org.javaup.ai.auth.vo.AdminProfileVo; +import org.javaup.common.ApiResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 后台登录认证接口。 + */ +@RestController +@RequestMapping("/admin/auth") +public class AdminAuthController { + + private final AdminAuthService adminAuthService; + + public AdminAuthController(AdminAuthService adminAuthService) { + this.adminAuthService = adminAuthService; + } + + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody AdminLoginRequest request) { + return ApiResponse.ok(adminAuthService.login(request)); + } + + @PostMapping("/logout") + public ApiResponse logout() { + return ApiResponse.ok(); + } + + @GetMapping("/me") + public ApiResponse me(HttpServletRequest request) { + return ApiResponse.ok(adminAuthService.currentProfile(request)); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/dto/AdminLoginRequest.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/dto/AdminLoginRequest.java new file mode 100644 index 0000000..2599628 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/dto/AdminLoginRequest.java @@ -0,0 +1,31 @@ +package org.javaup.ai.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +/** + * 后台登录请求。 + */ +public class AdminLoginRequest { + + @NotBlank(message = "请输入账号") + private String username; + + @NotBlank(message = "请输入密码") + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/service/AdminAuthService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/service/AdminAuthService.java new file mode 100644 index 0000000..1bda520 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/service/AdminAuthService.java @@ -0,0 +1,16 @@ +package org.javaup.ai.auth.service; + +import jakarta.servlet.http.HttpServletRequest; +import org.javaup.ai.auth.dto.AdminLoginRequest; +import org.javaup.ai.auth.vo.AdminLoginVo; +import org.javaup.ai.auth.vo.AdminProfileVo; + +/** + * 后台登录认证服务。 + */ +public interface AdminAuthService { + + AdminLoginVo login(AdminLoginRequest request); + + AdminProfileVo currentProfile(HttpServletRequest request); +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/service/impl/AdminAuthServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/service/impl/AdminAuthServiceImpl.java new file mode 100644 index 0000000..cdb474a --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/service/impl/AdminAuthServiceImpl.java @@ -0,0 +1,49 @@ +package org.javaup.ai.auth.service.impl; + +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.javaup.ai.auth.config.AdminAuthProperties; +import org.javaup.ai.auth.dto.AdminLoginRequest; +import org.javaup.ai.auth.service.AdminAuthService; +import org.javaup.ai.auth.support.AdminJwtTokenService; +import org.javaup.ai.auth.support.AdminRequestContext; +import org.javaup.ai.auth.vo.AdminLoginVo; +import org.javaup.ai.auth.vo.AdminProfileVo; +import org.javaup.exception.SuperAgentFrameException; +import org.springframework.stereotype.Service; + +/** + * 后台登录认证实现。 + */ +@Service +public class AdminAuthServiceImpl implements AdminAuthService { + + private final AdminAuthProperties adminAuthProperties; + + private final AdminJwtTokenService adminJwtTokenService; + + public AdminAuthServiceImpl(AdminAuthProperties adminAuthProperties, + AdminJwtTokenService adminJwtTokenService) { + this.adminAuthProperties = adminAuthProperties; + this.adminJwtTokenService = adminJwtTokenService; + } + + @Override + public AdminLoginVo login(AdminLoginRequest request) { + String username = StrUtil.trim(request.getUsername()); + String password = StrUtil.trim(request.getPassword()); + if (!StrUtil.equals(username, adminAuthProperties.getUsername()) + || !StrUtil.equals(password, adminAuthProperties.getPassword())) { + throw new SuperAgentFrameException(401, "账号或密码不正确"); + } + + String token = adminJwtTokenService.generateToken(username); + return new AdminLoginVo(username, token, adminAuthProperties.getTokenExpireMinutes()); + } + + @Override + public AdminProfileVo currentProfile(HttpServletRequest request) { + String username = AdminRequestContext.resolveUsername(request); + return new AdminProfileVo(username); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminAuthInterceptor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminAuthInterceptor.java new file mode 100644 index 0000000..cbe7b42 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminAuthInterceptor.java @@ -0,0 +1,74 @@ +package org.javaup.ai.auth.support; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import org.javaup.common.ApiResponse; +import org.javaup.exception.SuperAgentFrameException; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * 后台管理接口鉴权拦截器。 + */ +@Component +public class AdminAuthInterceptor implements HandlerInterceptor { + + private final AdminJwtTokenService adminJwtTokenService; + + private final ObjectMapper objectMapper; + + public AdminAuthInterceptor(AdminJwtTokenService adminJwtTokenService, + ObjectMapper objectMapper) { + this.adminJwtTokenService = adminJwtTokenService; + this.objectMapper = objectMapper; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } + + String authorization = request.getHeader("Authorization"); + String token = resolveToken(authorization); + if (StrUtil.isBlank(token)) { + writeUnauthorized(response, "请先登录后台管理台"); + return false; + } + + try { + Claims claims = adminJwtTokenService.parseToken(token); + String username = claims.getSubject(); + if (StrUtil.isBlank(username)) { + writeUnauthorized(response, "后台登录无效,请重新登录"); + return false; + } + AdminRequestContext.storeUsername(request, username); + return true; + } catch (SuperAgentFrameException exception) { + writeUnauthorized(response, exception.getMessage()); + return false; + } + } + + private String resolveToken(String authorization) { + if (StrUtil.isBlank(authorization)) { + return null; + } + if (StrUtil.startWithIgnoreCase(authorization, "Bearer ")) { + return StrUtil.trim(authorization.substring(7)); + } + return StrUtil.trim(authorization); + } + + private void writeUnauthorized(HttpServletResponse response, String message) throws Exception { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.error(401, message))); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminJwtTokenService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminJwtTokenService.java new file mode 100644 index 0000000..d200b71 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminJwtTokenService.java @@ -0,0 +1,53 @@ +package org.javaup.ai.auth.support; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; +import org.javaup.ai.auth.config.AdminAuthProperties; +import org.javaup.exception.SuperAgentFrameException; +import org.springframework.stereotype.Component; + +/** + * 后台登录 JWT 服务。 + */ +@Component +public class AdminJwtTokenService { + + private final AdminAuthProperties adminAuthProperties; + + public AdminJwtTokenService(AdminAuthProperties adminAuthProperties) { + this.adminAuthProperties = adminAuthProperties; + } + + public String generateToken(String username) { + Instant now = Instant.now(); + Instant expireAt = now.plusSeconds(adminAuthProperties.getTokenExpireMinutes() * 60); + return Jwts.builder() + .setSubject(username) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(expireAt)) + .signWith( + SignatureAlgorithm.HS256, + adminAuthProperties.getTokenSecret().getBytes(StandardCharsets.UTF_8) + ) + .compact(); + } + + public Claims parseToken(String token) { + try { + return Jwts.parser() + .setSigningKey(adminAuthProperties.getTokenSecret().getBytes(StandardCharsets.UTF_8)) + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException exception) { + throw new SuperAgentFrameException(401, "后台登录已过期,请重新登录", exception); + } catch (JwtException | IllegalArgumentException exception) { + throw new SuperAgentFrameException(401, "后台登录无效,请重新登录", exception); + } + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminRequestContext.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminRequestContext.java new file mode 100644 index 0000000..b07d36d --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/AdminRequestContext.java @@ -0,0 +1,23 @@ +package org.javaup.ai.auth.support; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 当前请求中的后台管理员上下文。 + */ +public final class AdminRequestContext { + + public static final String ADMIN_USERNAME_ATTRIBUTE = "super.agent.admin.username"; + + private AdminRequestContext() { + } + + public static void storeUsername(HttpServletRequest request, String username) { + request.setAttribute(ADMIN_USERNAME_ATTRIBUTE, username); + } + + public static String resolveUsername(HttpServletRequest request) { + Object username = request.getAttribute(ADMIN_USERNAME_ATTRIBUTE); + return username == null ? "" : String.valueOf(username); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/PreviewModeInterceptor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/PreviewModeInterceptor.java new file mode 100644 index 0000000..c1cfbd6 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/support/PreviewModeInterceptor.java @@ -0,0 +1,91 @@ +package org.javaup.ai.auth.support; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.util.Set; +import org.javaup.ai.auth.config.PreviewModeProperties; +import org.javaup.ai.chatagent.support.StreamEventWriter; +import org.javaup.common.ApiResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * 线上只读展示模式拦截器。 + */ +@Component +public class PreviewModeInterceptor implements HandlerInterceptor { + + private static final Set BLOCKED_PATHS = Set.of( + "/api/chat/stream", + "/api/chat/session/stop", + "/api/chat/session/reset", + "/api/chat/session/summary/rebuild", + "/manage/document/upload", + "/manage/document/delete", + "/manage/document/strategy/confirm", + "/manage/document/index/build", + "/manage/knowledge/scope/save", + "/manage/knowledge/scope/delete", + "/manage/knowledge/topic/save", + "/manage/knowledge/topic/delete", + "/manage/knowledge/document/profile/regenerate", + "/manage/knowledge/document/profile/batch/regenerate", + "/manage/knowledge/topic/document/save", + "/manage/knowledge/topic/document/remove" + ); + + private final PreviewModeProperties previewModeProperties; + + private final ObjectMapper objectMapper; + + private final StreamEventWriter streamEventWriter; + + public PreviewModeInterceptor(PreviewModeProperties previewModeProperties, + ObjectMapper objectMapper, + StreamEventWriter streamEventWriter) { + this.previewModeProperties = previewModeProperties; + this.objectMapper = objectMapper; + this.streamEventWriter = streamEventWriter; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (!Boolean.TRUE.equals(previewModeProperties.getEnabled())) { + return true; + } + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } + + String path = request.getRequestURI().substring(request.getContextPath().length()); + if (!BLOCKED_PATHS.contains(path)) { + return true; + } + + if ("/api/chat/stream".equals(path)) { + writeStreamReject(response); + } else { + writeJsonReject(response); + } + return false; + } + + private void writeStreamReject(HttpServletResponse response) throws Exception { + response.setStatus(HttpServletResponse.SC_OK); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType("text/event-stream;charset=UTF-8"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Connection", "keep-alive"); + response.getWriter().write("data: " + streamEventWriter.error(previewModeProperties.getMessage()) + "\n\n"); + response.getWriter().flush(); + } + + private void writeJsonReject(HttpServletResponse response) throws Exception { + response.setStatus(HttpServletResponse.SC_OK); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.error(previewModeProperties.getMessage()))); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/vo/AdminLoginVo.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/vo/AdminLoginVo.java new file mode 100644 index 0000000..fc6a36e --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/vo/AdminLoginVo.java @@ -0,0 +1,43 @@ +package org.javaup.ai.auth.vo; + +/** + * 后台登录返回值。 + */ +public class AdminLoginVo { + + private String username; + + private String token; + + private Long expireMinutes; + + public AdminLoginVo(String username, String token, Long expireMinutes) { + this.username = username; + this.token = token; + this.expireMinutes = expireMinutes; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Long getExpireMinutes() { + return expireMinutes; + } + + public void setExpireMinutes(Long expireMinutes) { + this.expireMinutes = expireMinutes; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/vo/AdminProfileVo.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/vo/AdminProfileVo.java new file mode 100644 index 0000000..f455a5b --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/auth/vo/AdminProfileVo.java @@ -0,0 +1,21 @@ +package org.javaup.ai.auth.vo; + +/** + * 当前后台管理员信息。 + */ +public class AdminProfileVo { + + private String username; + + public AdminProfileVo(String username) { + this.username = username; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/resources/application.yaml b/super-agent-business/super-agent-business-chat/src/main/resources/application.yaml index 0ddf13c..9bb89f8 100644 --- a/super-agent-business/super-agent-business-chat/src/main/resources/application.yaml +++ b/super-agent-business/super-agent-business-chat/src/main/resources/application.yaml @@ -78,6 +78,14 @@ mybatis-plus: local-cache-scope: statement app: + admin-auth: + username: ${SUPER_AGENT_ADMIN_USERNAME:admin} + password: ${SUPER_AGENT_ADMIN_PASSWORD:admin123456} + token-secret: ${SUPER_AGENT_ADMIN_TOKEN_SECRET:super-agent-admin-token-secret-change-me} + token-expire-minutes: ${SUPER_AGENT_ADMIN_TOKEN_EXPIRE_MINUTES:720} + preview-mode: + enabled: ${SUPER_AGENT_PREVIEW_MODE_ENABLED:false} + message: ${SUPER_AGENT_PREVIEW_MODE_MESSAGE:线上环境为只读展示模式,仅开放浏览与检索能力} chat: # 是否开启“推荐追问问题”能力。 # 开启后,主回答生成完成后会额外调用一次模型,产出最多 3 个可继续追问的问题。 diff --git a/super-agent-common/super-agent-common-frame/pom.xml b/super-agent-common/super-agent-common-frame/pom.xml index 21f7ce6..62e2fba 100644 --- a/super-agent-common/super-agent-common-frame/pom.xml +++ b/super-agent-common/super-agent-common-frame/pom.xml @@ -37,6 +37,21 @@ jjwt ${jjwt.version} + + + javax.xml.bind + jaxb-api + ${jaxb.api.version} + + + org.glassfish.jaxb + jaxb-runtime + ${jaxb.runtime.version} + cn.hutool hutool-all diff --git a/vue/src/api/api.js b/vue/src/api/api.js index 4af475e..625e9fc 100644 --- a/vue/src/api/api.js +++ b/vue/src/api/api.js @@ -1,5 +1,7 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '' const REQUEST_TIMEOUT = 30000 +const ADMIN_TOKEN_KEY = 'super-agent-admin-token' +const ADMIN_USER_KEY = 'super-agent-admin-user' export class APIError extends Error { constructor(message, status, cause) { @@ -14,6 +16,36 @@ function buildApiUrl(path) { return API_BASE_URL ? new URL(path, API_BASE_URL).toString() : path } +function getAdminToken() { + return window.localStorage.getItem(ADMIN_TOKEN_KEY) || '' +} + +function clearAdminAuth() { + window.localStorage.removeItem(ADMIN_TOKEN_KEY) + window.localStorage.removeItem(ADMIN_USER_KEY) +} + +function buildAuthHeaders(headers = {}) { + const token = getAdminToken() + if (!token) { + return headers + } + return { + Authorization: `Bearer ${token}`, + ...headers + } +} + +function handleUnauthorized(response) { + if (response.status !== 401) { + return + } + clearAdminAuth() + if (window.location.pathname.startsWith('/admin') && window.location.pathname !== '/admin/login') { + window.location.href = '/admin/login' + } +} + function stringifyManageValue(value) { if (Array.isArray(value)) { return value.map((item) => stringifyManageValue(item)) @@ -68,13 +100,14 @@ async function requestJson(path, options = {}) { method: options.method || 'GET', headers: { 'Content-Type': 'application/json', - ...(options.headers || {}) + ...buildAuthHeaders(options.headers || {}) }, body: options.body ? JSON.stringify(options.body) : undefined, signal: controller.signal }) if (!response.ok) { + handleUnauthorized(response) throw new APIError(await readResponseMessage(response), response.status) } @@ -105,13 +138,14 @@ async function requestApiEnvelope(path, options = {}) { method: options.method || 'POST', headers: { 'Content-Type': 'application/json', - ...(options.headers || {}) + ...buildAuthHeaders(options.headers || {}) }, body: options.body ? JSON.stringify(options.body) : undefined, signal: controller.signal }) if (!response.ok) { + handleUnauthorized(response) throw new APIError(await readResponseMessage(response), response.status) } @@ -130,13 +164,14 @@ async function requestMultipartApiEnvelope(path, formData, options = {}) { const response = await fetch(buildApiUrl(path), { method: options.method || 'POST', headers: { - ...(options.headers || {}) + ...buildAuthHeaders(options.headers || {}) }, body: formData, signal: controller.signal }) if (!response.ok) { + handleUnauthorized(response) throw new APIError(await readResponseMessage(response), response.status) } @@ -355,7 +390,8 @@ export const chatApi = { method: 'POST', headers: { 'Content-Type': 'application/json', - Accept: 'text/event-stream' + Accept: 'text/event-stream', + ...buildAuthHeaders() }, body: JSON.stringify(payload), signal: controller.signal @@ -379,6 +415,27 @@ export const chatApi = { } } +export const adminAuthApi = { + login(payload) { + return requestApiEnvelope('/admin/auth/login', { + method: 'POST', + body: payload + }) + }, + + logout() { + return requestApiEnvelope('/admin/auth/logout', { + method: 'POST', + body: {} + }) + }, + + currentUser() { + return requestJson('/admin/auth/me') + .then((payload) => unwrapApiResponse(payload)) + } +} + export const manageApi = { uploadDocument({ file, documentName, operatorId, knowledgeScopeCode, knowledgeScopeName, businessCategory, documentTags }) { const formData = new FormData() diff --git a/vue/src/utils/adminAuth.js b/vue/src/utils/adminAuth.js index 356bb74..30bb191 100644 --- a/vue/src/utils/adminAuth.js +++ b/vue/src/utils/adminAuth.js @@ -1,34 +1,77 @@ -const ADMIN_AUTH_KEY = 'super-agent-admin-auth' +const ADMIN_TOKEN_KEY = 'super-agent-admin-token' const ADMIN_USER_KEY = 'super-agent-admin-user' +function decodeBase64Url(value) { + const normalized = value.replace(/-/g, '+').replace(/_/g, '/') + const padding = normalized.length % 4 + const base64 = padding ? normalized + '='.repeat(4 - padding) : normalized + return window.atob(base64) +} + +function parseTokenPayload(token) { + if (!token) { + return null + } + const parts = token.split('.') + if (parts.length < 2) { + return null + } + try { + return JSON.parse(decodeBase64Url(parts[1])) + } catch { + return null + } +} + +function isTokenExpired(token) { + const payload = parseTokenPayload(token) + if (!payload?.exp) { + return true + } + return Date.now() >= Number(payload.exp) * 1000 +} + +/** + * 读取当前后台 token。 + */ +export function getAdminToken() { + return window.localStorage.getItem(ADMIN_TOKEN_KEY) || '' +} + /** - * 判断当前是否已进入后台管理台演示态。 - * - *

这里故意不走真实登录接口,而是用本地存储模拟一个轻量登录态, - * 方便学员直接体验管理端功能,不引入额外鉴权复杂度。

+ * 判断当前后台 token 是否存在且仍有效。 */ export function isAdminAuthenticated() { - return window.localStorage.getItem(ADMIN_AUTH_KEY) === '1' + const token = getAdminToken() + if (!token || isTokenExpired(token)) { + clearAdminAuth() + return false + } + return true } /** - * 写入一个假的后台登录态。 + * 写入后台登录态。 */ -export function loginAdminDemo(username) { - window.localStorage.setItem(ADMIN_AUTH_KEY, '1') - window.localStorage.setItem(ADMIN_USER_KEY, username || 'admin') +export function saveAdminAuth(payload = {}) { + if (payload.token) { + window.localStorage.setItem(ADMIN_TOKEN_KEY, payload.token) + } + if (payload.username) { + window.localStorage.setItem(ADMIN_USER_KEY, payload.username) + } } /** - * 清理假的后台登录态。 + * 清理后台登录态。 */ -export function logoutAdminDemo() { - window.localStorage.removeItem(ADMIN_AUTH_KEY) +export function clearAdminAuth() { + window.localStorage.removeItem(ADMIN_TOKEN_KEY) window.localStorage.removeItem(ADMIN_USER_KEY) } /** - * 获取当前演示用户名称。 + * 获取当前后台用户名。 */ export function getAdminUsername() { return window.localStorage.getItem(ADMIN_USER_KEY) || 'admin' diff --git a/vue/src/views/AdminLoginView.vue b/vue/src/views/AdminLoginView.vue index ea91013..a99b2ed 100644 --- a/vue/src/views/AdminLoginView.vue +++ b/vue/src/views/AdminLoginView.vue @@ -4,7 +4,7 @@ @@ -16,19 +16,21 @@

{{ errorMessage }}

- +
@@ -36,46 +38,54 @@