Ver código fonte

统一Cookie构建逻辑:登录和登出使用相同的 buildCookie() 方法
增强本地环境判断:多维度检测,避免Origin头缺失导致的问题
支持代理场景:正确识别HTTPS请求,设置正确的secure和sameSite属性
CORS放宽限制:允许所有来源,确保Set-Cookie响应头能被浏览器正确处理

liyanbo 7 horas atrás
pai
commit
9640c67c95

+ 102 - 5
byzs-web/src/main/java/cn/iocoder/byzs/module/web/controller/admin/login/WebLoginController.java

@@ -30,10 +30,16 @@ import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
 import jakarta.annotation.security.PermitAll;
+import jakarta.servlet.http.Cookie;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.validation.Valid;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseCookie;
 import org.springframework.web.bind.annotation.*;
 
+import jakarta.servlet.http.HttpServletResponse;
+import java.time.Duration;
+import java.time.LocalDateTime;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -83,16 +89,20 @@ public class WebLoginController {
     @PermitAll
     @TenantIgnore
     @Operation(summary = "使用账号密码登录")
-    public CommonResult<WebLoginVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
-        return success(buildWebLoginVO(authService.login(reqVO)));
+    public CommonResult<WebLoginVO> login(@RequestBody @Valid AuthLoginReqVO reqVO, HttpServletRequest request, HttpServletResponse response) {
+        WebLoginVO loginVO = buildWebLoginVO(authService.login(reqVO));
+        setAuthCookie(request, response, loginVO.getAccessToken(), loginVO.getExpiresTime());
+        return success(loginVO);
     }
 
     @PostMapping("/sms-login")
     @PermitAll
     @TenantIgnore
     @Operation(summary = "使用短信验证码登录")
-    public CommonResult<WebLoginVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) {
-        return success(buildWebLoginVO(authService.smsLogin(reqVO)));
+    public CommonResult<WebLoginVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO, HttpServletRequest request, HttpServletResponse response) {
+        WebLoginVO loginVO = buildWebLoginVO(authService.smsLogin(reqVO));
+        setAuthCookie(request, response, loginVO.getAccessToken(), loginVO.getExpiresTime());
+        return success(loginVO);
     }
 
     @PostMapping("/send-sms-code")
@@ -107,7 +117,11 @@ public class WebLoginController {
     @PostMapping("/logout")
     @PermitAll
     @Operation(summary = "登出系统")
-    public CommonResult<Boolean> logout(HttpServletRequest request) {
+    public CommonResult<Boolean> logout(HttpServletRequest request, HttpServletResponse response) {
+        // 复用登录时的Cookie设置方法,确保domain等参数完全一致
+        clearAuthCookie(request, response);
+
+        // 执行token作废业务逻辑
         String token = SecurityFrameworkUtils.obtainAuthorization(request,
                 securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
         if (StrUtil.isNotBlank(token)) {
@@ -144,4 +158,87 @@ public class WebLoginController {
 
         return webLoginVO;
     }
+
+    /**
+     * 设置认证Cookie
+     */
+    private void setAuthCookie(HttpServletRequest request, HttpServletResponse response, String token, LocalDateTime expiresTime) {
+        long maxAgeSeconds = 0;
+        if (expiresTime != null) {
+            Duration duration = Duration.between(LocalDateTime.now(), expiresTime);
+            maxAgeSeconds = duration.getSeconds();
+            if (maxAgeSeconds < 0) {
+                maxAgeSeconds = 0;
+            }
+        }
+        buildCookie(request, response, token, maxAgeSeconds);
+    }
+
+    /**
+     * 清除认证Cookie(登出时调用)
+     */
+    private void clearAuthCookie(HttpServletRequest request, HttpServletResponse response) {
+        buildCookie(request, response, "", 0);
+    }
+
+    /**
+     * 构建并设置Cookie(统一登录和登出的Cookie逻辑)
+     */
+    private void buildCookie(HttpServletRequest request, HttpServletResponse response, String token, long maxAgeSeconds) {
+        // 判断是否为HTTPS(考虑代理场景,检查X-Forwarded-Proto头)
+        boolean isHttps = isHttpsRequest(request);
+        String sameSiteVal = isHttps ? "None" : "Lax";
+
+        ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie.from("access_token", token)
+                .path("/")
+                .maxAge(maxAgeSeconds)
+                .httpOnly(true)
+                .secure(isHttps)
+                .sameSite(sameSiteVal);
+
+        // 判断是否为本地环境:检查Origin、Host、RemoteAddr
+        if (!isLocalEnvironment(request)) {
+            cookieBuilder.domain(".learn-ai.com.cn");
+        }
+
+        response.addHeader(HttpHeaders.SET_COOKIE, cookieBuilder.build().toString());
+    }
+
+    /**
+     * 判断请求是否为HTTPS(考虑代理场景)
+     */
+    private boolean isHttpsRequest(HttpServletRequest request) {
+        // 直接检查请求scheme
+        if ("https".equalsIgnoreCase(request.getScheme())) {
+            return true;
+        }
+        // 检查代理头(Nginx等代理场景)
+        String forwardedProto = request.getHeader("X-Forwarded-Proto");
+        if ("https".equalsIgnoreCase(forwardedProto)) {
+            return true;
+        }
+        // 检查X-Forwarded-Ssl头
+        String forwardedSsl = request.getHeader("X-Forwarded-Ssl");
+        return "on".equalsIgnoreCase(forwardedSsl);
+    }
+
+    /**
+     * 判断是否为本地开发环境
+     * 不依赖单一的Origin头,增加Host和RemoteAddr的判断
+     */
+    private boolean isLocalEnvironment(HttpServletRequest request) {
+        // 检查Origin头
+        String origin = request.getHeader("Origin");
+        if (origin != null && origin.contains("localhost")) {
+            return true;
+        }
+        // 检查Host头
+        String host = request.getHeader("Host");
+        if (host != null && (host.contains("localhost") || host.contains("127.0.0.1"))) {
+            return true;
+        }
+        // 检查远程IP
+        String remoteAddr = request.getRemoteAddr();
+        return "127.0.0.1".equals(remoteAddr) || "localhost".equals(remoteAddr) || "0:0:0:0:0:0:0:1".equals(remoteAddr);
+    }
 }