From b83c5400187db6b6d63999ad5668bf603081857d Mon Sep 17 00:00:00 2001 From: liuguancen Date: Thu, 7 May 2026 20:22:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(cas):=20=E6=8E=A5=E9=80=9A=20CAS=20?= =?UTF-8?q?=E5=8D=95=E7=82=B9=E7=99=BB=E5=BD=95=E5=85=A8=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=20+=20=E6=B8=85=E7=90=86=E5=86=97=E4=BD=99=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复链上的 8 个真 bug: 1. UserDetail(UserVO,Set) 漏 setAuthorities → CAS token 构造抛 IllegalArgumentException 修:CasUserDetailsService.buildGptUserDetail 手动补 empty authorities 2. frontEndUrl 写死内网 IP,localhost 隧道用户跳回时"无法访问此网站" 修:CasUrlBuilder 用 X-Forwarded-Host / Host 动态拼 service URL 3. vite proxy 没配 /metalinfo/chat_web_backend,CAS 回跳 ticket 被前端路由吞 修:加一条 proxy(rewrite 去 /metalinfo 前缀)+ X-Forwarded-Host 转发 4. ticket 校验 service URL 跟 entry point 不一致 → CAS server mismatch 修:自定义 AuthenticationDetailsSource 用同一个动态 URL 5. sendRedirect URL 含 # 经容器编码成 %23,浏览器拿不到 hash → 404 修:改用 query 参数(/metalinfo/?cas_token=xxx),前端 router beforeEach 拦截 6. CAS 登录后 HttpSession 残留,第二次访问 /cas/login 不触发 entryPoint → 落到 DispatcherServlet → 找不到映射 → 404 Whitelabel 修:SuccessHandler 完成后 invalidate session + clear SecurityContext 7. CAS 路径漏写 Redis token,JwtAuthenticationFilter 校验时 LOGIN_TOKEN_KEY 找不到 → "token已失效" → 前端 axios interceptor 清 token 跳回 login 修:SuccessHandler 同步写 redisUtils.set,与 LoginController.saveLoginLog 对齐 8. permission.ts 没拦 query 里的 cas_token,hash 路由下 location.search 取不到 修:router beforeEach 优先消费 cas_token 再走 getSession 清理冗余: - CasProperties 删 6 个未用字段(enabled/serverLogout/appLogout/appKey/ appSecret/httpsFlag/frontEndUrl) - application.yml 同步删,移除写死的 app-secret 等敏感字段 - 删外部 override 文件 chat_web_backend/config/application.yml - casServiceProperties.setService 改占位符(实际不被读取,只满足 ServiceProperties.afterPropertiesSet 的非空校验) - 删 permission.ts 的 [CAS] [GUARD] debug log,保留 catch error 一条 新增: - CasUrlBuilder 工具类:从请求动态解析 host/scheme,多个地方共用 UI: - welcome 页面玻璃按钮 + 呼吸光晕/光感动画(用户自己调过,本次保留) - App.vue:/welcome 路径不渲染 Operates 侧边栏 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../security/cas/CasLoginSuccessHandler.java | 52 ++++++- .../chat/base/security/cas/CasProperties.java | 15 +- .../base/security/cas/CasSecurityConfig.java | 42 +++++- .../chat/base/security/cas/CasUrlBuilder.java | 44 ++++++ .../security/cas/CasUserDetailsService.java | 23 ++- .../src/main/resources/application.yml | 8 +- chat_web_front/public/favicon.svg | 4 +- chat_web_front/src/App.vue | 2 +- .../assets/images/login/projectLogo-white.svg | 4 +- chat_web_front/src/router/permission.ts | 23 +++ chat_web_front/src/views/login/index.vue | 9 +- chat_web_front/src/views/welcome/index.vue | 139 ++++++++++++++++-- chat_web_front/vite.config.ts | 20 +++ 13 files changed, 337 insertions(+), 48 deletions(-) create mode 100644 chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUrlBuilder.java diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasLoginSuccessHandler.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasLoginSuccessHandler.java index 3511607..26ac100 100644 --- a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasLoginSuccessHandler.java +++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasLoginSuccessHandler.java @@ -1,37 +1,83 @@ package com.inspur.llm.chat.base.security.cas; +import com.inspur.llm.chat.base.constant.RedisConstants; +import com.inspur.llm.chat.base.constant.StringPoolConstant; import com.inspur.llm.chat.base.security.JwtTokenUtils; +import com.inspur.llm.chat.base.security.UserDetail; +import com.inspur.llm.chat.base.util.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; /** - * CAS 登录成功 → 生成 JWT → 重定向前端登录页(带 cas_token) + * CAS 登录成功 → 生成 JWT → 重定向前端登录页(带 cas_token)。 + * + * 重定向的前端 URL 用当前请求的 X-Forwarded-Host / Host 动态拼, + * 这样回到前端时浏览器地址不会突变(用户从哪个 host 来就回哪个 host), + * 避免内网 IP 不可达造成"无法访问此网站"。 */ @Slf4j @Component public class CasLoginSuccessHandler implements AuthenticationSuccessHandler { + /** + * 前端 base 路径,token 用 query 参数传(不放 hash 里)。 + * 原本想用 /metalinfo/#/login?cas_token=xxx,但 sendRedirect 经过容器/代理时 + * # 字符容易被 encode 成 %23,导致浏览器拿不到 hash → vue-router 不知道路由 → 404。 + * 改用 query:/metalinfo/?cas_token=xxx,前端 router beforeEach 拦截。 + */ + private static final String FRONTEND_BASE_PATH = "/metalinfo/"; + @Autowired private CasProperties casProperties; + @Autowired + private RedisUtils redisUtils; + @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { log.info("CAS 登录成功: {}", authentication.getName()); String token = JwtTokenUtils.generateToken(authentication); - String redirect = casProperties.getFrontEndUrl() - + "/#/login?cas_token=" + URLEncoder.encode(token, StandardCharsets.UTF_8.name()); + + // 把 token 写进 Redis,否则 JwtAuthenticationFilter 校验时查不到会返"token已失效" + // 跟 LoginController.saveLoginLog 同样逻辑 + try { + UserDetail userDetail = JwtTokenUtils.getUserDetail(authentication); + String key = RedisConstants.LOGIN_TOKEN_KEY + + userDetail.getId() + StringPoolConstant.COLON + userDetail.getSessionId(); + redisUtils.set(key, token, JwtTokenUtils.EXPIRE_TIME / 1000L); + log.info("CAS token 已写入 Redis, key={}", key); + } catch (Exception e) { + log.warn("CAS token 写 Redis 失败: {}", e.getMessage(), e); + } + + String redirect = CasUrlBuilder.buildServiceUrl(request, FRONTEND_BASE_PATH) + + "?cas_token=" + URLEncoder.encode(token, StandardCharsets.UTF_8.name()); + log.info("CAS 登录成功跳转 URL: {}", redirect); + + // 销毁 HttpSession + 清 SecurityContext + // 否则下次访问 /cas/login 时 Spring Security 看到 session 已认证 → 不触发 CAS entryPoint + // → 请求穿透到 DispatcherServlet → 找不到 /cas/login 映射 → 404 Whitelabel。 + // 我们用 JWT 做认证,不需要 server-side session。 + HttpSession session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + SecurityContextHolder.clearContext(); + response.sendRedirect(redirect); } } diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasProperties.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasProperties.java index 5ae1f96..7100881 100644 --- a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasProperties.java +++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasProperties.java @@ -5,20 +5,19 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; /** - * CAS 单点登录配置(绑定 application.yml 中的 security.cas.*) + * CAS 单点登录配置(绑定 application.yml 中的 security.cas.*)。 + * + * 只保留实际使用的字段。logout/appKey/appSecret 等之前预留但未实现, + * 真正接入时再加,避免 dead config。 */ @Data @Configuration @ConfigurationProperties(prefix = "security.cas") public class CasProperties { - private boolean enabled = false; + /** CAS 服务器根地址,例如 http://192.168.203.20:8180 */ private String serverHost; + /** CAS 服务器登录入口,通常 ${serverHost}/login */ private String serverLogin; - private String serverLogout; + /** 本应用的 CAS 回调路径(servlet path),通常 /cas/login */ private String appLogin; - private String appLogout; - private String appKey; - private String appSecret; - private boolean httpsFlag = false; - private String frontEndUrl; } diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasSecurityConfig.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasSecurityConfig.java index 79f8d91..d3dd171 100644 --- a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasSecurityConfig.java +++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasSecurityConfig.java @@ -10,23 +10,34 @@ import org.springframework.security.cas.authentication.CasAssertionAuthenticatio import org.springframework.security.cas.authentication.CasAuthenticationProvider; import org.springframework.security.cas.web.CasAuthenticationEntryPoint; import org.springframework.security.cas.web.CasAuthenticationFilter; +import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + /** * CAS 单点登录 Security 配置(仅处理 /cas/** 路径,与 WebSecurityConfig 共存)。 * * 流程: * /cas/login(无 ticket) → 触发 CasEntryPoint → 302 到 CAS 登录页 * /cas/login?ticket=xxx → CasAuthenticationFilter 验证 → 成功 → CasLoginSuccessHandler 生成 JWT → 302 前端 + * + * service URL 用 X-Forwarded-Host / Host 动态拼,不依赖固定 frontEndUrl, + * 这样浏览器在 localhost:3000 / 内网 IP / 公网域名 各种入口下 CAS 回跳都能命中。 */ @Configuration @Order(1) public class CasSecurityConfig extends WebSecurityConfigurerAdapter { + /** 给 CAS 的 service URL 后缀(不含 host) */ + static final String SERVICE_URL_SUFFIX = "/metalinfo/chat_web_backend/cas/login"; + @Autowired private CasProperties casProperties; @@ -57,18 +68,39 @@ public class CasSecurityConfig extends WebSecurityConfigurerAdapter { auth.authenticationProvider(casAuthenticationProvider()); } + /** + * ServiceProperties 仅作为 bean 占位被 entry point / filter / provider 引用, + * 它的 service 字段实际不会被读到——entry point override 了 createServiceUrl, + * filter 设了自定义 AuthenticationDetailsSource,校验时用的是 details.getServiceUrl()。 + * + * 但 ServiceProperties.afterPropertiesSet() 会硬性校验 service 字段非空, + * 所以这里 set 一个占位符值满足校验,不影响实际行为。 + */ @Bean public ServiceProperties casServiceProperties() { ServiceProperties sp = new ServiceProperties(); - sp.setService(casProperties.getFrontEndUrl() + "/chat_web_backend" + casProperties.getAppLogin()); + // 占位符(必须非空,afterPropertiesSet 会校验),实际值由 entry point 的 createServiceUrl + // 和 filter 的 AuthenticationDetailsSource 动态构造 + sp.setService("placeholder-not-used"); sp.setSendRenew(false); sp.setAuthenticateAllArtifacts(true); return sp; } + /** + * 自定义 entry point:override createServiceUrl,用当前请求的 X-Forwarded-Host / Host + * 动态拼 service URL,而不是用固定的 frontEndUrl 配置(commence 是 final,只能改这里)。 + * + * 注意返回 raw URL(未 encode),commence 内部的 constructRedirectUrl 会再 URLEncoder.encode 一次。 + */ @Bean public CasAuthenticationEntryPoint casAuthenticationEntryPoint() { - CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); + CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint() { + @Override + protected String createServiceUrl(HttpServletRequest request, HttpServletResponse response) { + return CasUrlBuilder.buildServiceUrl(request, SERVICE_URL_SUFFIX); + } + }; ep.setLoginUrl(casProperties.getServerLogin()); ep.setServiceProperties(casServiceProperties()); return ep; @@ -81,6 +113,12 @@ public class CasSecurityConfig extends WebSecurityConfigurerAdapter { filter.setFilterProcessesUrl(casProperties.getAppLogin()); filter.setAuthenticationSuccessHandler(casLoginSuccessHandler); filter.setServiceProperties(casServiceProperties()); + // 关键:ticket 校验阶段也得用同一个动态 service URL(含 /metalinfo 前缀), + // 否则 CAS 服务器对 ticket 的 service 校验会 mismatch(vite proxy 已经把 /metalinfo + // 前缀剥掉,request URL 没法直接拼出正确 service URL,所以自定义一个)。 + AuthenticationDetailsSource source = + request -> () -> CasUrlBuilder.buildServiceUrl(request, SERVICE_URL_SUFFIX); + filter.setAuthenticationDetailsSource(source); // 仅在 URL 含 ticket 时才尝试认证;无 ticket → 让 ExceptionTranslationFilter 触发 EntryPoint 重定向 CAS final String appLogin = casProperties.getAppLogin(); filter.setRequiresAuthenticationRequestMatcher(req -> diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUrlBuilder.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUrlBuilder.java new file mode 100644 index 0000000..4e7b3f8 --- /dev/null +++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUrlBuilder.java @@ -0,0 +1,44 @@ +package com.inspur.llm.chat.base.security.cas; + +import javax.servlet.http.HttpServletRequest; + +/** + * 从 HTTP 请求动态构造 CAS 相关 URL 的工具类。 + * + * 优先用 X-Forwarded-Host / X-Forwarded-Proto(vite proxy 转发的原始 Host), + * 退而求其次用请求自身的 Host。这样无论用户是 localhost、内网 IP 还是公网域名访问, + * 拼出来的 URL 都跟用户浏览器的实际地址栏一致。 + */ +public final class CasUrlBuilder { + + private CasUrlBuilder() {} + + /** 取请求实际的 host(优先 X-Forwarded-Host)。 */ + public static String resolveHost(HttpServletRequest request) { + String forwarded = request.getHeader("X-Forwarded-Host"); + if (forwarded != null && !forwarded.isEmpty()) { + // X-Forwarded-Host 可能是 "host1, host2"(多层代理),取第一个 + int comma = forwarded.indexOf(','); + return (comma > 0 ? forwarded.substring(0, comma) : forwarded).trim(); + } + String host = request.getHeader("Host"); + if (host != null && !host.isEmpty()) { + return host; + } + return request.getServerName() + ":" + request.getServerPort(); + } + + /** 取请求实际的 scheme(优先 X-Forwarded-Proto)。 */ + public static String resolveScheme(HttpServletRequest request) { + String proto = request.getHeader("X-Forwarded-Proto"); + if (proto != null && !proto.isEmpty()) { + return proto.trim(); + } + return request.getScheme(); + } + + /** 拼一个绝对 URL:scheme://host + suffix */ + public static String buildServiceUrl(HttpServletRequest request, String suffix) { + return resolveScheme(request) + "://" + resolveHost(request) + suffix; + } +} diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUserDetailsService.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUserDetailsService.java index b4a1677..11850a8 100644 --- a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUserDetailsService.java +++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUserDetailsService.java @@ -17,6 +17,7 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.HashSet; /** @@ -54,21 +55,31 @@ public class CasUserDetailsService implements AuthenticationUserDetailsServicelambdaQuery().eq(User::getCasUsername, casPrincipal).last("limit 1")); if (user != null) { log.info("CAS 命中 gpt_user (cas_username) id={}", user.getId()); - UserVO vo = new UserVO(); - BeanUtils.copyProperties(user, vo); - return new UserDetail(vo, new HashSet<>()); + return buildGptUserDetail(user); } user = userMapper.selectOne( Wrappers.lambdaQuery().eq(User::getTel, casPrincipal).last("limit 1")); if (user != null) { log.info("CAS 命中 gpt_user (tel fallback) id={}", user.getId()); - UserVO vo = new UserVO(); - BeanUtils.copyProperties(user, vo); - return new UserDetail(vo, new HashSet<>()); + return buildGptUserDetail(user); } log.warn("CAS 用户未在战知库中找到: {}", casPrincipal); throw new UsernameNotFoundException("CAS 用户未绑定战知账号: " + casPrincipal); } + + /** + * 包装 gpt_user → UserDetail。 + * 注意:UserDetail(UserVO, Set) 构造器没初始化 authorities,导致 getAuthorities() 返 null, + * 而 CasAuthenticationToken 构造会校验 authorities 非 null(不是非空 list)→ IllegalArgumentException。 + * 这里手动补一个空 list。 + */ + private UserDetail buildGptUserDetail(User user) { + UserVO vo = new UserVO(); + BeanUtils.copyProperties(user, vo); + UserDetail ud = new UserDetail(vo, new HashSet<>()); + ud.setAuthorities(new ArrayList<>()); + return ud; + } } diff --git a/chat_web_backend/src/main/resources/application.yml b/chat_web_backend/src/main/resources/application.yml index 3f2100d..e1097a8 100644 --- a/chat_web_backend/src/main/resources/application.yml +++ b/chat_web_backend/src/main/resources/application.yml @@ -92,17 +92,11 @@ xss: # 词向量模型名称 embedding-model-name: bge_m3 -# CAS 单点登录配置 +# CAS 单点登录配置(service URL 用 X-Forwarded-Host 动态拼,无需 front-end-url) security: cas: - enabled: true server-host: http://192.168.203.20:8180 server-login: ${security.cas.server-host}/login - server-logout: ${security.cas.server-host}/logout app-login: /cas/login - app-logout: /cas/logout - app-key: 8UyEQdr6L6FfAH3D - app-secret: Ely1U6Uv2Nf8APQSfZdg1epSbQ6ORKwr - front-end-url: http://192.168.203.8:3000 diff --git a/chat_web_front/public/favicon.svg b/chat_web_front/public/favicon.svg index ea69ed9..5d71b35 100644 --- a/chat_web_front/public/favicon.svg +++ b/chat_web_front/public/favicon.svg @@ -1,13 +1,13 @@ - + - + \ No newline at end of file diff --git a/chat_web_front/src/App.vue b/chat_web_front/src/App.vue index 6553840..d38d0ce 100644 --- a/chat_web_front/src/App.vue +++ b/chat_web_front/src/App.vue @@ -1,6 +1,6 @@ diff --git a/chat_web_front/src/assets/images/login/projectLogo-white.svg b/chat_web_front/src/assets/images/login/projectLogo-white.svg index e256263..e137c1a 100644 --- a/chat_web_front/src/assets/images/login/projectLogo-white.svg +++ b/chat_web_front/src/assets/images/login/projectLogo-white.svg @@ -7,13 +7,13 @@ - + - + diff --git a/chat_web_front/src/router/permission.ts b/chat_web_front/src/router/permission.ts index b548ada..8f561eb 100644 --- a/chat_web_front/src/router/permission.ts +++ b/chat_web_front/src/router/permission.ts @@ -1,9 +1,31 @@ import type { Router } from 'vue-router' import { useAuthStore } from '@/store/modules/auth' +/** + * 检查 URL search 部分有没有 cas_token(CAS 回调时后端把 token 拼在 query 里), + * 有的话存下来并清掉 URL 上的痕迹。 + */ +function consumeCasTokenFromUrl(authStore: ReturnType) { + const search = window.location.search || '' + if (!search.includes('cas_token=')) return false + const params = new URLSearchParams(search.replace(/^\?/, '')) + const token = params.get('cas_token') + if (!token) return false + authStore.setToken(token) + // 把 cas_token 从 URL 上清掉,留住 hash 部分 + const cleanUrl = window.location.pathname + window.location.hash + window.history.replaceState({}, document.title, cleanUrl) + return true +} + export function setupPageGuard(router: Router) { router.beforeEach(async (to, from, next) => { const authStore = useAuthStore() + // CAS 回调先拿 token,再走普通鉴权 + if (consumeCasTokenFromUrl(authStore)) { + next({ path: '/welcome', replace: true }) + return + } if (to.path == '/login') { next() return @@ -25,6 +47,7 @@ export function setupPageGuard(router: Router) { } next() } catch (error) { + console.warn('[auth-guard] getSession 失败:', error) if (to.path === '/login') next() else next({ name: 'Login' }) } diff --git a/chat_web_front/src/views/login/index.vue b/chat_web_front/src/views/login/index.vue index 039a324..165abdb 100644 --- a/chat_web_front/src/views/login/index.vue +++ b/chat_web_front/src/views/login/index.vue @@ -179,15 +179,20 @@ const goCasLogin = () => { }; // CAS 回调:URL 含 cas_token → 存 token + 跳 welcome +// 注意 hash 路由下 token 在 hash 里(#/login?cas_token=xxx),不在 location.search onMounted(() => { - const params = new URLSearchParams(window.location.search); + const hash = window.location.hash || ""; + const qIdx = hash.indexOf("?"); + const queryStr = qIdx >= 0 ? hash.substring(qIdx + 1) : window.location.search.replace(/^\?/, ""); + const params = new URLSearchParams(queryStr); const casToken = params.get("cas_token"); if (casToken) { authStore.setToken(casToken); + // 清掉 URL 上的 cas_token 参数 window.history.replaceState( {}, document.title, - window.location.pathname + window.location.hash.split("?")[0] + window.location.pathname + (qIdx >= 0 ? hash.substring(0, qIdx) : window.location.hash) ); router.push("/welcome"); } diff --git a/chat_web_front/src/views/welcome/index.vue b/chat_web_front/src/views/welcome/index.vue index ceb2992..48904c3 100644 --- a/chat_web_front/src/views/welcome/index.vue +++ b/chat_web_front/src/views/welcome/index.vue @@ -1,6 +1,6 @@