fix(auth): move admin session to HttpOnly cookies
This commit is contained in:
1 parent
0d2165cf80
commit
17b2ba49a8
39 files changed
+191
-219
No files matched your search
@@ -60,6 +60,12 @@
|
||||
<artifactId>bizlog-sdk</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>co.yixiang.boot</groupId>
|
||||
|
||||
+6
@@ -48,4 +48,10 @@ public class SecurityProperties {
|
||||
* PasswordEncoder 加密复杂度,越高开销越大
|
||||
*/
|
||||
private Integer passwordEncoderLength = 4;
|
||||
|
||||
/**
|
||||
* 管理端认证 Cookie 是否仅通过 HTTPS 发送
|
||||
*/
|
||||
@NotNull(message = "Cookie Secure 开关不能为空")
|
||||
private Boolean cookieSecure = true;
|
||||
}
|
||||
+8
-1
@@ -11,6 +11,7 @@ import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
@@ -22,6 +23,9 @@ import java.util.Collections;
|
||||
*/
|
||||
public class SecurityFrameworkUtils {
|
||||
|
||||
public static final String ACCESS_TOKEN_COOKIE = "yshop_admin_access_token";
|
||||
public static final String REFRESH_TOKEN_COOKIE = "yshop_admin_refresh_token";
|
||||
|
||||
/**
|
||||
* HEADER 认证头 value 的前缀
|
||||
*/
|
||||
@@ -39,11 +43,14 @@ public class SecurityFrameworkUtils {
|
||||
*/
|
||||
public static String obtainAuthorization(HttpServletRequest request,
|
||||
String headerName, String parameterName) {
|
||||
// 1. 获得 Token。优先级:Header > Parameter
|
||||
// 1. 获得 Token。优先级:Header > Parameter > HttpOnly Cookie
|
||||
String token = request.getHeader(headerName);
|
||||
if (StrUtil.isEmpty(token)) {
|
||||
token = request.getParameter(parameterName);
|
||||
}
|
||||
if (StrUtil.isEmpty(token) && WebUtils.getCookie(request, ACCESS_TOKEN_COOKIE) != null) {
|
||||
token = WebUtils.getCookie(request, ACCESS_TOKEN_COOKIE).getValue();
|
||||
}
|
||||
if (!StringUtils.hasText(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package co.yixiang.yshop.framework.security.core.util;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class SecurityFrameworkUtilsTest {
|
||||
|
||||
@Test
|
||||
void obtainAuthorizationUsesCookieWhenHeaderIsMissing() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setCookies(new Cookie(SecurityFrameworkUtils.ACCESS_TOKEN_COOKIE, "cookie-token"));
|
||||
|
||||
assertEquals("cookie-token", SecurityFrameworkUtils.obtainAuthorization(request, "Authorization", "token"));
|
||||
|
||||
request.addHeader("Authorization", "Bearer header-token");
|
||||
assertEquals("header-token", SecurityFrameworkUtils.obtainAuthorization(request, "Authorization", "token"));
|
||||
}
|
||||
}
|
||||
+65
-1
@@ -24,19 +24,28 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
|
||||
import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static co.yixiang.yshop.framework.common.pojo.CommonResult.success;
|
||||
import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet;
|
||||
import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.ACCESS_TOKEN_COOKIE;
|
||||
import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.REFRESH_TOKEN_COOKIE;
|
||||
import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
|
||||
@Tag(name = "管理后台 - 认证")
|
||||
@@ -69,15 +78,25 @@ public class AuthController {
|
||||
return success(authService.login(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/login-cookie")
|
||||
@PermitAll
|
||||
@Operation(summary = "使用账号密码登录(HttpOnly Cookie)")
|
||||
public CommonResult<Boolean> loginCookie(@RequestBody @Valid AuthLoginReqVO reqVO,
|
||||
HttpServletResponse response) {
|
||||
writeTokenCookies(response, authService.login(reqVO));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
@PermitAll
|
||||
@Operation(summary = "登出系统")
|
||||
public CommonResult<Boolean> logout(HttpServletRequest request) {
|
||||
public CommonResult<Boolean> logout(HttpServletRequest request, HttpServletResponse response) {
|
||||
String token = SecurityFrameworkUtils.obtainAuthorization(request,
|
||||
securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType());
|
||||
}
|
||||
clearTokenCookies(response);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@@ -89,6 +108,18 @@ public class AuthController {
|
||||
return success(authService.refreshToken(refreshToken));
|
||||
}
|
||||
|
||||
@PostMapping("/refresh-token-cookie")
|
||||
@PermitAll
|
||||
@Operation(summary = "刷新令牌(HttpOnly Cookie)")
|
||||
public CommonResult<Boolean> refreshTokenCookie(HttpServletRequest request, HttpServletResponse response) {
|
||||
if (WebUtils.getCookie(request, REFRESH_TOKEN_COOKIE) == null) {
|
||||
throw exception(UNAUTHORIZED);
|
||||
}
|
||||
writeTokenCookies(response,
|
||||
authService.refreshToken(WebUtils.getCookie(request, REFRESH_TOKEN_COOKIE).getValue()));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get-permission-info")
|
||||
@Operation(summary = "获取登录用户的权限信息")
|
||||
public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo() {
|
||||
@@ -124,6 +155,15 @@ public class AuthController {
|
||||
return success(authService.smsLogin(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/sms-login-cookie")
|
||||
@PermitAll
|
||||
@Operation(summary = "使用短信验证码登录(HttpOnly Cookie)")
|
||||
public CommonResult<Boolean> smsLoginCookie(@RequestBody @Valid AuthSmsLoginReqVO reqVO,
|
||||
HttpServletResponse response) {
|
||||
writeTokenCookies(response, authService.smsLogin(reqVO));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/send-sms-code")
|
||||
@PermitAll
|
||||
@Operation(summary = "发送手机验证码")
|
||||
@@ -154,4 +194,28 @@ public class AuthController {
|
||||
return success(authService.socialLogin(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/social-login-cookie")
|
||||
@PermitAll
|
||||
@Operation(summary = "社交快捷登录(HttpOnly Cookie)")
|
||||
public CommonResult<Boolean> socialQuickLoginCookie(@RequestBody @Valid AuthSocialLoginReqVO reqVO,
|
||||
HttpServletResponse response) {
|
||||
writeTokenCookies(response, authService.socialLogin(reqVO));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
private void writeTokenCookies(HttpServletResponse response, AuthLoginRespVO token) {
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, tokenCookie(ACCESS_TOKEN_COOKIE, token.getAccessToken()).toString());
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, tokenCookie(REFRESH_TOKEN_COOKIE, token.getRefreshToken()).toString());
|
||||
}
|
||||
|
||||
private void clearTokenCookies(HttpServletResponse response) {
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, tokenCookie(ACCESS_TOKEN_COOKIE, "").maxAge(Duration.ZERO).toString());
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, tokenCookie(REFRESH_TOKEN_COOKIE, "").maxAge(Duration.ZERO).toString());
|
||||
}
|
||||
|
||||
private ResponseCookie.ResponseCookieBuilder tokenCookie(String name, String value) {
|
||||
return ResponseCookie.from(name, value).httpOnly(true).secure(securityProperties.getCookieSecure())
|
||||
.sameSite("Strict").path("/");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -192,6 +192,7 @@ yshop:
|
||||
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
|
||||
security:
|
||||
mock-enable: true
|
||||
cookie-secure: false
|
||||
xss:
|
||||
enable: false
|
||||
exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系
|
||||
|
||||
Reference in new issue
Block a user