diff --git a/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/pom.xml b/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/pom.xml
index 0cb7ea3..286f0f8 100644
--- a/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/pom.xml
+++ b/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/pom.xml
@@ -60,6 +60,12 @@
bizlog-sdk
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
co.yixiang.boot
diff --git a/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/config/SecurityProperties.java b/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/config/SecurityProperties.java
index 3f24798..e3cc886 100644
--- a/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/config/SecurityProperties.java
+++ b/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/config/SecurityProperties.java
@@ -48,4 +48,10 @@ public class SecurityProperties {
* PasswordEncoder 加密复杂度,越高开销越大
*/
private Integer passwordEncoderLength = 4;
+
+ /**
+ * 管理端认证 Cookie 是否仅通过 HTTPS 发送
+ */
+ @NotNull(message = "Cookie Secure 开关不能为空")
+ private Boolean cookieSecure = true;
}
diff --git a/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/util/SecurityFrameworkUtils.java b/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/util/SecurityFrameworkUtils.java
index b60b0c3..a7cfa8a 100644
--- a/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/util/SecurityFrameworkUtils.java
+++ b/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/util/SecurityFrameworkUtils.java
@@ -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;
}
diff --git a/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/test/java/co/yixiang/yshop/framework/security/core/util/SecurityFrameworkUtilsTest.java b/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/test/java/co/yixiang/yshop/framework/security/core/util/SecurityFrameworkUtilsTest.java
new file mode 100644
index 0000000..0fed7d7
--- /dev/null
+++ b/yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/test/java/co/yixiang/yshop/framework/security/core/util/SecurityFrameworkUtilsTest.java
@@ -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"));
+ }
+}
diff --git a/yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/AuthController.java b/yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/AuthController.java
index 5c9a3ff..5dbd34d 100644
--- a/yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/AuthController.java
+++ b/yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/AuthController.java
@@ -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 loginCookie(@RequestBody @Valid AuthLoginReqVO reqVO,
+ HttpServletResponse response) {
+ writeTokenCookies(response, authService.login(reqVO));
+ return success(true);
+ }
+
@PostMapping("/logout")
@PermitAll
@Operation(summary = "登出系统")
- public CommonResult logout(HttpServletRequest request) {
+ public CommonResult 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 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 getPermissionInfo() {
@@ -124,6 +155,15 @@ public class AuthController {
return success(authService.smsLogin(reqVO));
}
+ @PostMapping("/sms-login-cookie")
+ @PermitAll
+ @Operation(summary = "使用短信验证码登录(HttpOnly Cookie)")
+ public CommonResult 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 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("/");
+ }
+
}
diff --git a/yshop-drink-boot3/yshop-server/src/main/resources/application-local.yaml b/yshop-drink-boot3/yshop-server/src/main/resources/application-local.yaml
index 2935a8f..e89707c 100644
--- a/yshop-drink-boot3/yshop-server/src/main/resources/application-local.yaml
+++ b/yshop-drink-boot3/yshop-server/src/main/resources/application-local.yaml
@@ -192,6 +192,7 @@ yshop:
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
security:
mock-enable: true
+ cookie-secure: false
xss:
enable: false
exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系
diff --git a/yshop-drink-vue3/src/api/login/index.ts b/yshop-drink-vue3/src/api/login/index.ts
index ef86563..50eebf3 100644
--- a/yshop-drink-vue3/src/api/login/index.ts
+++ b/yshop-drink-vue3/src/api/login/index.ts
@@ -1,5 +1,4 @@
import request from '@/config/axios'
-import { getRefreshToken } from '@/utils/auth'
import type { UserLoginVO } from './types'
export interface SmsCodeVO {
@@ -14,12 +13,12 @@ export interface SmsLoginVO {
// 登录
export const login = (data: UserLoginVO) => {
- return request.post({ url: '/system/auth/login', data })
+ return request.post({ url: '/system/auth/login-cookie', data })
}
// 刷新访问令牌
export const refreshToken = () => {
- return request.post({ url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken() })
+ return request.post({ url: '/system/auth/refresh-token-cookie' })
}
// 使用租户名,获得租户编号
@@ -49,13 +48,13 @@ export const sendSmsCode = (data: SmsCodeVO) => {
// 短信验证码登录
export const smsLogin = (data: SmsLoginVO) => {
- return request.post({ url: '/system/auth/sms-login', data })
+ return request.post({ url: '/system/auth/sms-login-cookie', data })
}
// 社交快捷登录,使用 code 授权码
export function socialLogin(type: string, code: string, state: string) {
return request.post({
- url: '/system/auth/social-login',
+ url: '/system/auth/social-login-cookie',
data: {
type,
code,
diff --git a/yshop-drink-vue3/src/api/login/types.ts b/yshop-drink-vue3/src/api/login/types.ts
index fff8122..fbf1eb4 100644
--- a/yshop-drink-vue3/src/api/login/types.ts
+++ b/yshop-drink-vue3/src/api/login/types.ts
@@ -7,16 +7,6 @@ export type UserLoginVO = {
socialState?: string
}
-export type TokenType = {
- id: number // 编号
- accessToken: string // 访问令牌
- refreshToken: string // 刷新令牌
- userId: number // 用户编号
- userType: number //用户类型
- clientId: string //客户端编号
- expiresTime: number //过期时间
-}
-
export type UserVO = {
id: number
username: string
diff --git a/yshop-drink-vue3/src/components/Editor/src/Editor.vue b/yshop-drink-vue3/src/components/Editor/src/Editor.vue
index eff8274..d7705ba 100644
--- a/yshop-drink-vue3/src/components/Editor/src/Editor.vue
+++ b/yshop-drink-vue3/src/components/Editor/src/Editor.vue
@@ -6,7 +6,7 @@ import { propTypes } from '@/utils/propTypes'
import { isNumber } from '@/utils/is'
import { ElMessage } from 'element-plus'
import { useLocaleStore } from '@/store/modules/locale'
-import { getAccessToken, getTenantId } from '@/utils/auth'
+import { getTenantId } from '@/utils/auth'
defineOptions({ name: 'Editor' })
@@ -104,10 +104,8 @@ const editorConfig = computed((): IEditorConfig => {
// 自定义增加 http header
headers: {
Accept: '*',
- Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
},
-
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
diff --git a/yshop-drink-vue3/src/components/Materials/src/Materials.vue b/yshop-drink-vue3/src/components/Materials/src/Materials.vue
index 03a9379..43963f2 100644
--- a/yshop-drink-vue3/src/components/Materials/src/Materials.vue
+++ b/yshop-drink-vue3/src/components/Materials/src/Materials.vue
@@ -99,7 +99,7 @@
{
- // 是否需要设置 token
- let isToken = (config!.headers || {}).isToken === false
- whiteList.some((v) => {
- if (config.url) {
- config.url.indexOf(v) > -1
- return (isToken = false)
- }
- })
- if (getAccessToken() && !isToken) {
- ;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token
- }
// 设置租户
if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId()
@@ -117,16 +102,10 @@ service.interceptors.response.use(
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
if (!isRefreshToken) {
isRefreshToken = true
- // 1. 如果获取不到刷新令牌,则只能执行登出操作
- if (!getRefreshToken()) {
- return handleAuthorized()
- }
- // 2. 进行刷新访问令牌
+ // 使用 HttpOnly Cookie 刷新访问令牌
try {
- const refreshTokenRes = await refreshToken()
+ await refreshToken()
// 2.1 刷新成功,则回放队列的请求 + 当前请求
- setToken((await refreshTokenRes).data.data)
- config.headers!.Authorization = 'Bearer ' + getAccessToken()
requestList.forEach((cb: any) => {
cb()
})
@@ -148,7 +127,6 @@ service.interceptors.response.use(
// 添加到队列,等待刷新获取到新的令牌
return new Promise((resolve) => {
requestList.push(() => {
- config.headers!.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(service(config))
})
})
@@ -200,7 +178,12 @@ service.interceptors.response.use(
const refreshToken = async () => {
axios.defaults.headers.common['tenant-id'] = getTenantId()
- return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
+ const response = await axios.post(base_url + '/system/auth/refresh-token-cookie', null, {
+ withCredentials: true
+ })
+ if (response.data?.code !== result_code) {
+ throw new Error(response.data?.msg)
+ }
}
const handleAuthorized = () => {
const { t } = useI18n()
@@ -218,8 +201,7 @@ const handleAuthorized = () => {
type: 'warning'
}).then(() => {
resetRouter() // 重置静态路由表
- deleteUserCache() // 删除用户缓存
- removeToken()
+ removeAuthenticated()
isRelogin.show = false
// 干掉token后再走一次路由让它过router.beforeEach的校验
window.location.href = window.location.href
diff --git a/yshop-drink-vue3/src/directives/permission/hasPermi.ts b/yshop-drink-vue3/src/directives/permission/hasPermi.ts
index d86d2f5..3bdeed8 100644
--- a/yshop-drink-vue3/src/directives/permission/hasPermi.ts
+++ b/yshop-drink-vue3/src/directives/permission/hasPermi.ts
@@ -1,14 +1,13 @@
import type { App } from 'vue'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { useUserStoreWithOut } from '@/store/modules/user'
const { t } = useI18n() // 国际化
export function hasPermi(app: App) {
app.directive('hasPermi', (el, binding) => {
- const { wsCache } = useCache()
const { value } = binding
const all_permission = '*:*:*'
- const permissions = wsCache.get(CACHE_KEY.USER).permissions
+ const permissions = useUserStoreWithOut().getPermissions
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
diff --git a/yshop-drink-vue3/src/directives/permission/hasRole.ts b/yshop-drink-vue3/src/directives/permission/hasRole.ts
index 31a352a..a58774d 100644
--- a/yshop-drink-vue3/src/directives/permission/hasRole.ts
+++ b/yshop-drink-vue3/src/directives/permission/hasRole.ts
@@ -1,14 +1,13 @@
import type { App } from 'vue'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { useUserStoreWithOut } from '@/store/modules/user'
const { t } = useI18n() // 国际化
export function hasRole(app: App) {
app.directive('hasRole', (el, binding) => {
- const { wsCache } = useCache()
const { value } = binding
const super_admin = 'admin'
- const roles = wsCache.get(CACHE_KEY.USER).roles
+ const roles = useUserStoreWithOut().getRoles
if (value && value instanceof Array && value.length > 0) {
const roleFlag = value
diff --git a/yshop-drink-vue3/src/hooks/web/useCache.ts b/yshop-drink-vue3/src/hooks/web/useCache.ts
index 4f39f30..9573fd5 100644
--- a/yshop-drink-vue3/src/hooks/web/useCache.ts
+++ b/yshop-drink-vue3/src/hooks/web/useCache.ts
@@ -7,9 +7,6 @@ import WebStorageCache from 'web-storage-cache'
type CacheType = 'localStorage' | 'sessionStorage'
export const CACHE_KEY = {
- // 用户相关
- ROLE_ROUTERS: 'roleRouters',
- USER: 'user',
// 系统设置
IS_DARK: 'isDark',
LANG: 'lang',
@@ -30,10 +27,3 @@ export const useCache = (type: CacheType = 'localStorage') => {
wsCache
}
}
-
-export const deleteUserCache = () => {
- const { wsCache } = useCache()
- wsCache.delete(CACHE_KEY.USER)
- wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
- // 注意,不要清理 LoginForm 登录表单
-}
diff --git a/yshop-drink-vue3/src/layout/components/UserInfo/src/components/LockPage.vue b/yshop-drink-vue3/src/layout/components/UserInfo/src/components/LockPage.vue
index 497dd37..d1cdf47 100644
--- a/yshop-drink-vue3/src/layout/components/UserInfo/src/components/LockPage.vue
+++ b/yshop-drink-vue3/src/layout/components/UserInfo/src/components/LockPage.vue
@@ -1,6 +1,5 @@