feat: 增加开发模式 URL 免登录
- 新增 dev-only 且仅限本机访问的 admin 免登入口 - 管理端支持通过 ?devLogin=admin 自动换取登录态并清理 URL 参数 - 删除未受保护的临时 token 接口并补充关键单测
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
package tech.easyflow.admin.controller.auth;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.auth.config.DevLoginGuard;
|
||||||
|
import tech.easyflow.auth.entity.LoginVO;
|
||||||
|
import tech.easyflow.auth.service.AuthService;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
|
@Profile("dev")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/auth/")
|
||||||
|
@ConditionalOnProperty(prefix = "easyflow.login.dev-bypass", name = "enabled", havingValue = "true")
|
||||||
|
public class DevLoginController {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
private final DevLoginGuard devLoginGuard;
|
||||||
|
|
||||||
|
public DevLoginController(AuthService authService, DevLoginGuard devLoginGuard) {
|
||||||
|
this.authService = authService;
|
||||||
|
this.devLoginGuard = devLoginGuard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SaIgnore
|
||||||
|
@PostMapping("dev-login")
|
||||||
|
public Result<LoginVO> devLogin(HttpServletRequest request,
|
||||||
|
@JsonBody(value = "account", required = true) String account) {
|
||||||
|
devLoginGuard.checkAccess(request, account);
|
||||||
|
return Result.ok(authService.devLogin(account));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package tech.easyflow.admin.controller.system;
|
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaIgnore;
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import tech.easyflow.common.constant.Constants;
|
|
||||||
import tech.easyflow.common.domain.Result;
|
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/temp-token")
|
|
||||||
public class SysTempTokenController {
|
|
||||||
|
|
||||||
@GetMapping("/create")
|
|
||||||
@SaIgnore
|
|
||||||
public Result<String> createTempToken() {
|
|
||||||
|
|
||||||
StpUtil.login(0);
|
|
||||||
String tokenValue = StpUtil.getTokenValue();
|
|
||||||
LoginAccount loginAccount = new LoginAccount();
|
|
||||||
loginAccount.setId(BigInteger.valueOf(0));
|
|
||||||
loginAccount.setLoginName("匿名用户");
|
|
||||||
loginAccount.setTenantId(BigInteger.ZERO);
|
|
||||||
loginAccount.setDeptId(BigInteger.ZERO);
|
|
||||||
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);
|
|
||||||
|
|
||||||
return Result.ok("", tokenValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,5 +24,11 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-system</artifactId>
|
<artifactId>easyflow-module-system</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package tech.easyflow.auth.config;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class DevLoginGuard {
|
||||||
|
|
||||||
|
private final LoginProperties properties;
|
||||||
|
|
||||||
|
public DevLoginGuard(LoginProperties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkAccess(HttpServletRequest request, String account) {
|
||||||
|
if (!isAllowedAccount(account)) {
|
||||||
|
throw new BusinessException("仅允许使用 admin 账号进行开发免登");
|
||||||
|
}
|
||||||
|
if (properties.getDevBypass().isLoopbackOnly() && !isLoopbackRequest(request)) {
|
||||||
|
throw new BusinessException("开发免登仅允许本机访问");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isAllowedAccount(String account) {
|
||||||
|
return StringUtils.hasText(account)
|
||||||
|
&& account.equals(properties.getDevBypass().getAccount());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isLoopbackRequest(HttpServletRequest request) {
|
||||||
|
return request != null && isLoopbackAddress(request.getRemoteAddr());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isLoopbackAddress(String remoteAddr) {
|
||||||
|
if (!StringUtils.hasText(remoteAddr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return InetAddress.getByName(remoteAddr).isLoopbackAddress();
|
||||||
|
} catch (UnknownHostException ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
public class LoginProperties {
|
public class LoginProperties {
|
||||||
|
|
||||||
private String[] excludes;
|
private String[] excludes;
|
||||||
|
private DevBypassProperties devBypass = new DevBypassProperties();
|
||||||
|
|
||||||
public String[] getExcludes() {
|
public String[] getExcludes() {
|
||||||
return excludes;
|
return excludes;
|
||||||
@@ -20,4 +21,43 @@ public class LoginProperties {
|
|||||||
public void setExcludes(String[] excludes) {
|
public void setExcludes(String[] excludes) {
|
||||||
this.excludes = excludes;
|
this.excludes = excludes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DevBypassProperties getDevBypass() {
|
||||||
|
return devBypass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDevBypass(DevBypassProperties devBypass) {
|
||||||
|
this.devBypass = devBypass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DevBypassProperties {
|
||||||
|
|
||||||
|
private String account = "admin";
|
||||||
|
private boolean enabled = false;
|
||||||
|
private boolean loopbackOnly = true;
|
||||||
|
|
||||||
|
public String getAccount() {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccount(String account) {
|
||||||
|
this.account = account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLoopbackOnly() {
|
||||||
|
return loopbackOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoopbackOnly(boolean loopbackOnly) {
|
||||||
|
this.loopbackOnly = loopbackOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,9 @@ public interface AuthService {
|
|||||||
* 登录
|
* 登录
|
||||||
*/
|
*/
|
||||||
LoginVO login(LoginDTO loginDTO);
|
LoginVO login(LoginDTO loginDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发模式免登录
|
||||||
|
*/
|
||||||
|
LoginVO devLogin(String account);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,35 +38,29 @@ public class AuthServiceImpl implements AuthService, StpInterface {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginVO login(LoginDTO loginDTO) {
|
public LoginVO login(LoginDTO loginDTO) {
|
||||||
LoginVO res = new LoginVO();
|
|
||||||
try {
|
try {
|
||||||
TenantManager.ignoreTenantCondition();
|
TenantManager.ignoreTenantCondition();
|
||||||
String pwd = loginDTO.getPassword();
|
String pwd = loginDTO.getPassword();
|
||||||
QueryWrapper w = QueryWrapper.create();
|
SysAccount record = getAvailableAccount(loginDTO.getAccount(), "用户名/密码错误");
|
||||||
w.eq(SysAccount::getLoginName, loginDTO.getAccount());
|
|
||||||
SysAccount record = sysAccountService.getOne(w);
|
|
||||||
if (record == null) {
|
|
||||||
throw new BusinessException("用户名/密码错误");
|
|
||||||
}
|
|
||||||
if (EnumDataStatus.UNAVAILABLE.getCode().equals(record.getStatus())) {
|
|
||||||
throw new BusinessException("账号未启用,请联系管理员");
|
|
||||||
}
|
|
||||||
String pwdDb = record.getPassword();
|
String pwdDb = record.getPassword();
|
||||||
if (!BCrypt.checkpw(pwd, pwdDb)) {
|
if (!BCrypt.checkpw(pwd, pwdDb)) {
|
||||||
throw new BusinessException("用户名/密码错误");
|
throw new BusinessException("用户名/密码错误");
|
||||||
}
|
}
|
||||||
StpUtil.login(record.getId());
|
return createLoginVO(record);
|
||||||
LoginAccount loginAccount = new LoginAccount();
|
} finally {
|
||||||
BeanUtil.copyProperties(record, loginAccount);
|
TenantManager.restoreTenantCondition();
|
||||||
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);
|
}
|
||||||
String tokenValue = StpUtil.getTokenValue();
|
}
|
||||||
res.setToken(tokenValue);
|
|
||||||
res.setNickname(record.getNickname());
|
@Override
|
||||||
res.setAvatar(record.getAvatar());
|
public LoginVO devLogin(String account) {
|
||||||
|
try {
|
||||||
|
TenantManager.ignoreTenantCondition();
|
||||||
|
SysAccount record = getAvailableAccount(account, "开发免登账号不存在");
|
||||||
|
return createLoginVO(record);
|
||||||
} finally {
|
} finally {
|
||||||
TenantManager.restoreTenantCondition();
|
TenantManager.restoreTenantCondition();
|
||||||
}
|
}
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -83,4 +77,30 @@ public class AuthServiceImpl implements AuthService, StpInterface {
|
|||||||
List<SysRole> roles = sysRoleService.getRolesByAccountId(BigInteger.valueOf(Long.parseLong(loginId.toString())));
|
List<SysRole> roles = sysRoleService.getRolesByAccountId(BigInteger.valueOf(Long.parseLong(loginId.toString())));
|
||||||
return roles.stream().map(SysRole::getRoleKey).collect(Collectors.toList());
|
return roles.stream().map(SysRole::getRoleKey).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoginVO createLoginVO(SysAccount record) {
|
||||||
|
StpUtil.login(record.getId());
|
||||||
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
|
BeanUtil.copyProperties(record, loginAccount);
|
||||||
|
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);
|
||||||
|
|
||||||
|
LoginVO res = new LoginVO();
|
||||||
|
res.setToken(StpUtil.getTokenValue());
|
||||||
|
res.setNickname(record.getNickname());
|
||||||
|
res.setAvatar(record.getAvatar());
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysAccount getAvailableAccount(String account, String accountNotFoundMessage) {
|
||||||
|
QueryWrapper w = QueryWrapper.create();
|
||||||
|
w.eq(SysAccount::getLoginName, account);
|
||||||
|
SysAccount record = sysAccountService.getOne(w);
|
||||||
|
if (record == null) {
|
||||||
|
throw new BusinessException(accountNotFoundMessage);
|
||||||
|
}
|
||||||
|
if (EnumDataStatus.UNAVAILABLE.getCode().equals(record.getStatus())) {
|
||||||
|
throw new BusinessException("账号未启用,请联系管理员");
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package tech.easyflow.auth.config;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class DevLoginGuardTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldAcceptConfiguredAdminAccount() {
|
||||||
|
DevLoginGuard guard = new DevLoginGuard(createProperties());
|
||||||
|
Assert.assertTrue(guard.isAllowedAccount("admin"));
|
||||||
|
Assert.assertFalse(guard.isAllowedAccount("guest"));
|
||||||
|
Assert.assertFalse(guard.isAllowedAccount(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRecognizeLoopbackAddresses() {
|
||||||
|
DevLoginGuard guard = new DevLoginGuard(createProperties());
|
||||||
|
Assert.assertTrue(guard.isLoopbackAddress("127.0.0.1"));
|
||||||
|
Assert.assertTrue(guard.isLoopbackAddress("::1"));
|
||||||
|
Assert.assertFalse(guard.isLoopbackAddress("192.168.1.10"));
|
||||||
|
Assert.assertFalse(guard.isLoopbackAddress("not-an-ip"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginProperties createProperties() {
|
||||||
|
LoginProperties properties = new LoginProperties();
|
||||||
|
LoginProperties.DevBypassProperties devBypass = new LoginProperties.DevBypassProperties();
|
||||||
|
devBypass.setEnabled(true);
|
||||||
|
devBypass.setAccount("admin");
|
||||||
|
devBypass.setLoopbackOnly(true);
|
||||||
|
properties.setDevBypass(devBypass);
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,10 @@ easyflow:
|
|||||||
login:
|
login:
|
||||||
# 放行接口路径
|
# 放行接口路径
|
||||||
excludes: /api/v1/auth/**, /static/**, /userCenter/auth/**, /userCenter/public/**
|
excludes: /api/v1/auth/**, /static/**, /userCenter/auth/**, /userCenter/public/**
|
||||||
|
dev-bypass:
|
||||||
|
enabled: true
|
||||||
|
account: admin
|
||||||
|
loopback-only: true
|
||||||
storage:
|
storage:
|
||||||
# local / xFileStorage
|
# local / xFileStorage
|
||||||
type: xFileStorage
|
type: xFileStorage
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ import { baseRequestClient, requestClient } from '#/api/request';
|
|||||||
export namespace AuthApi {
|
export namespace AuthApi {
|
||||||
/** 登录接口参数 */
|
/** 登录接口参数 */
|
||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
|
account?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DevLoginParams {
|
||||||
|
account: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 登录接口返回值 */
|
/** 登录接口返回值 */
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@@ -26,6 +31,16 @@ export async function loginApi(data: AuthApi.LoginParams) {
|
|||||||
return requestClient.post<AuthApi.LoginResult>('/api/v1/auth/login', data);
|
return requestClient.post<AuthApi.LoginResult>('/api/v1/auth/login', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发模式免登录
|
||||||
|
*/
|
||||||
|
export async function devLoginApi(data: AuthApi.DevLoginParams) {
|
||||||
|
return requestClient.post<AuthApi.LoginResult>(
|
||||||
|
'/api/v1/auth/dev-login',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新accessToken
|
* 刷新accessToken
|
||||||
*/
|
*/
|
||||||
|
|||||||
57
easyflow-ui-admin/app/src/router/__tests__/dev-login.test.ts
Normal file
57
easyflow-ui-admin/app/src/router/__tests__/dev-login.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDevLoginAccount,
|
||||||
|
removeDevLoginQuery,
|
||||||
|
shouldAttemptDevLogin,
|
||||||
|
} from '../dev-login';
|
||||||
|
|
||||||
|
describe('dev-login route helpers', () => {
|
||||||
|
it('reads the admin account from the query string', () => {
|
||||||
|
expect(getDevLoginAccount({ devLogin: 'admin' })).toBe('admin');
|
||||||
|
expect(getDevLoginAccount({ devLogin: ['admin', 'other'] })).toBe('admin');
|
||||||
|
expect(getDevLoginAccount({ devLogin: ' ' })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes only the devLogin query parameter', () => {
|
||||||
|
expect(
|
||||||
|
removeDevLoginQuery({
|
||||||
|
devLogin: 'admin',
|
||||||
|
redirect: '/ai/workflow',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
redirect: '/ai/workflow',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('attempts dev login only in dev mode and without an existing token', () => {
|
||||||
|
expect(
|
||||||
|
shouldAttemptDevLogin({
|
||||||
|
account: 'admin',
|
||||||
|
hasAccessToken: false,
|
||||||
|
isDev: true,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldAttemptDevLogin({
|
||||||
|
account: 'admin',
|
||||||
|
hasAccessToken: true,
|
||||||
|
isDev: true,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldAttemptDevLogin({
|
||||||
|
account: 'guest',
|
||||||
|
hasAccessToken: false,
|
||||||
|
isDev: true,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldAttemptDevLogin({
|
||||||
|
account: 'admin',
|
||||||
|
hasAccessToken: false,
|
||||||
|
isDev: false,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
39
easyflow-ui-admin/app/src/router/dev-login.ts
Normal file
39
easyflow-ui-admin/app/src/router/dev-login.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { LocationQuery, LocationQueryRaw } from 'vue-router';
|
||||||
|
|
||||||
|
const DEV_LOGIN_ACCOUNT = 'admin';
|
||||||
|
const DEV_LOGIN_QUERY_KEY = 'devLogin';
|
||||||
|
|
||||||
|
function normalizeQueryValue(
|
||||||
|
value: LocationQuery[string] | LocationQueryRaw[string],
|
||||||
|
) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value[0] ?? null;
|
||||||
|
}
|
||||||
|
return value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDevLoginAccount(query: LocationQuery | LocationQueryRaw) {
|
||||||
|
const value = normalizeQueryValue(query[DEV_LOGIN_QUERY_KEY]);
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const account = value.trim();
|
||||||
|
return account.length > 0 ? account : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeDevLoginQuery(query: LocationQuery | LocationQueryRaw) {
|
||||||
|
const { [DEV_LOGIN_QUERY_KEY]: _ignored, ...nextQuery } = query;
|
||||||
|
return nextQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldAttemptDevLogin(params: {
|
||||||
|
account: null | string;
|
||||||
|
hasAccessToken: boolean;
|
||||||
|
isDev: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
params.isDev &&
|
||||||
|
!params.hasAccessToken &&
|
||||||
|
params.account === DEV_LOGIN_ACCOUNT
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
import type {Router} from 'vue-router';
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
import {LOGIN_PATH} from '@easyflow/constants';
|
import { LOGIN_PATH } from '@easyflow/constants';
|
||||||
import {preferences} from '@easyflow/preferences';
|
import { preferences } from '@easyflow/preferences';
|
||||||
import {useAccessStore, useUserStore} from '@easyflow/stores';
|
import { useAccessStore, useUserStore } from '@easyflow/stores';
|
||||||
import {startProgress, stopProgress} from '@easyflow/utils';
|
import { startProgress, stopProgress } from '@easyflow/utils';
|
||||||
|
|
||||||
import {accessRoutes, coreRouteNames} from '#/router/routes';
|
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
||||||
import {useAuthStore} from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
import {generateAccess} from './access';
|
import { generateAccess } from './access';
|
||||||
|
import {
|
||||||
|
getDevLoginAccount,
|
||||||
|
removeDevLoginQuery,
|
||||||
|
shouldAttemptDevLogin,
|
||||||
|
} from './dev-login';
|
||||||
|
|
||||||
interface NetworkConnectionLike {
|
interface NetworkConnectionLike {
|
||||||
effectiveType?: string;
|
effectiveType?: string;
|
||||||
@@ -121,10 +126,59 @@ function setupCommonGuard(router: Router) {
|
|||||||
* @param router
|
* @param router
|
||||||
*/
|
*/
|
||||||
function setupAccessGuard(router: Router) {
|
function setupAccessGuard(router: Router) {
|
||||||
|
let devLoginPromise: null | Promise<void> = null;
|
||||||
|
|
||||||
router.beforeEach(async (to, from) => {
|
router.beforeEach(async (to, from) => {
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const devLoginAccount = getDevLoginAccount(to.query);
|
||||||
|
|
||||||
|
const resolveCleanFullPath = () =>
|
||||||
|
router.resolve({
|
||||||
|
hash: to.hash,
|
||||||
|
path: to.path,
|
||||||
|
query: removeDevLoginQuery(to.query),
|
||||||
|
}).fullPath;
|
||||||
|
|
||||||
|
if (devLoginAccount && import.meta.env.DEV && accessStore.accessToken) {
|
||||||
|
return resolveCleanFullPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
shouldAttemptDevLogin({
|
||||||
|
account: devLoginAccount,
|
||||||
|
hasAccessToken: !!accessStore.accessToken,
|
||||||
|
isDev: import.meta.env.DEV,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
const account = devLoginAccount;
|
||||||
|
try {
|
||||||
|
devLoginPromise ??= authStore
|
||||||
|
.authDevLogin(account ?? '')
|
||||||
|
.then(() => undefined)
|
||||||
|
.finally(() => {
|
||||||
|
devLoginPromise = null;
|
||||||
|
});
|
||||||
|
await devLoginPromise;
|
||||||
|
const cleanFullPath = resolveCleanFullPath();
|
||||||
|
if (window.location.search.includes('devLogin=')) {
|
||||||
|
window.location.replace(cleanFullPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return cleanFullPath;
|
||||||
|
} catch {
|
||||||
|
const cleanFullPath = resolveCleanFullPath();
|
||||||
|
return {
|
||||||
|
path: LOGIN_PATH,
|
||||||
|
query:
|
||||||
|
cleanFullPath === preferences.app.defaultHomePath
|
||||||
|
? {}
|
||||||
|
: { redirect: encodeURIComponent(cleanFullPath) },
|
||||||
|
replace: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 基本路由,这些路由不需要进入权限拦截
|
// 基本路由,这些路由不需要进入权限拦截
|
||||||
if (coreRouteNames.includes(to.name as string)) {
|
if (coreRouteNames.includes(to.name as string)) {
|
||||||
@@ -147,13 +201,17 @@ function setupAccessGuard(router: Router) {
|
|||||||
|
|
||||||
// 没有访问权限,跳转登录页面
|
// 没有访问权限,跳转登录页面
|
||||||
if (to.fullPath !== LOGIN_PATH) {
|
if (to.fullPath !== LOGIN_PATH) {
|
||||||
|
const cleanFullPath =
|
||||||
|
devLoginAccount && import.meta.env.DEV
|
||||||
|
? resolveCleanFullPath()
|
||||||
|
: to.fullPath;
|
||||||
return {
|
return {
|
||||||
path: LOGIN_PATH,
|
path: LOGIN_PATH,
|
||||||
// 如不需要,直接删除 query
|
// 如不需要,直接删除 query
|
||||||
query:
|
query:
|
||||||
to.fullPath === preferences.app.defaultHomePath
|
cleanFullPath === preferences.app.defaultHomePath
|
||||||
? {}
|
? {}
|
||||||
: { redirect: encodeURIComponent(to.fullPath) },
|
: { redirect: encodeURIComponent(cleanFullPath) },
|
||||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
replace: true,
|
replace: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ import { resetAllStores, useAccessStore, useUserStore } from '@easyflow/stores';
|
|||||||
import { ElNotification } from 'element-plus';
|
import { ElNotification } from 'element-plus';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
import {
|
||||||
|
devLoginApi,
|
||||||
|
getAccessCodesApi,
|
||||||
|
getUserInfoApi,
|
||||||
|
loginApi,
|
||||||
|
logoutApi,
|
||||||
|
} from '#/api';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
@@ -20,6 +26,50 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|
|
||||||
const loginLoading = ref(false);
|
const loginLoading = ref(false);
|
||||||
|
|
||||||
|
async function finalizeLogin(
|
||||||
|
accessToken: string,
|
||||||
|
options: {
|
||||||
|
notify?: boolean;
|
||||||
|
onSuccess?: () => Promise<void> | void;
|
||||||
|
skipRedirect?: boolean;
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
let userInfo: null | UserInfo = null;
|
||||||
|
accessStore.setAccessToken(accessToken);
|
||||||
|
|
||||||
|
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||||
|
fetchUserInfo(),
|
||||||
|
getAccessCodesApi(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
userInfo = fetchUserInfoResult;
|
||||||
|
|
||||||
|
userStore.setUserInfo(userInfo);
|
||||||
|
accessStore.setAccessCodes(accessCodes);
|
||||||
|
|
||||||
|
if (accessStore.loginExpired) {
|
||||||
|
accessStore.setLoginExpired(false);
|
||||||
|
} else if (!options.skipRedirect) {
|
||||||
|
const homePath =
|
||||||
|
userInfo.homePath || preferences.app.defaultHomePath || '/';
|
||||||
|
options.onSuccess
|
||||||
|
? await options.onSuccess()
|
||||||
|
: await router.push(homePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.notify !== false && userInfo?.nickname) {
|
||||||
|
ElNotification({
|
||||||
|
message: `${$t('authentication.loginSuccessDesc')}:${userInfo.nickname}`,
|
||||||
|
title: $t('authentication.loginSuccess'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步处理登录操作
|
* 异步处理登录操作
|
||||||
* Asynchronously handle the login process
|
* Asynchronously handle the login process
|
||||||
@@ -29,53 +79,35 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
params: Recordable<any>,
|
params: Recordable<any>,
|
||||||
onSuccess?: () => Promise<void> | void,
|
onSuccess?: () => Promise<void> | void,
|
||||||
) {
|
) {
|
||||||
// 异步处理用户登录操作并获取 accessToken
|
|
||||||
let userInfo: null | UserInfo = null;
|
|
||||||
try {
|
try {
|
||||||
loginLoading.value = true;
|
loginLoading.value = true;
|
||||||
const { token: accessToken } = await loginApi(params);
|
const { token: accessToken } = await loginApi(params);
|
||||||
|
|
||||||
// 如果成功获取到 accessToken
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
// 将 accessToken 存储到 accessStore 中
|
return await finalizeLogin(accessToken, { onSuccess });
|
||||||
accessStore.setAccessToken(accessToken);
|
|
||||||
|
|
||||||
// 获取用户信息并存储到 accessStore 中
|
|
||||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
|
||||||
fetchUserInfo(),
|
|
||||||
getAccessCodesApi(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
userInfo = fetchUserInfoResult;
|
|
||||||
|
|
||||||
userStore.setUserInfo(userInfo);
|
|
||||||
accessStore.setAccessCodes(accessCodes);
|
|
||||||
|
|
||||||
if (accessStore.loginExpired) {
|
|
||||||
accessStore.setLoginExpired(false);
|
|
||||||
} else {
|
|
||||||
const homePath =
|
|
||||||
userInfo.homePath || preferences.app.defaultHomePath || '/';
|
|
||||||
onSuccess ? await onSuccess?.() : await router.push(homePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userInfo?.nickname) {
|
|
||||||
ElNotification({
|
|
||||||
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.nickname}`,
|
|
||||||
title: $t('authentication.loginSuccess'),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loginLoading.value = false;
|
loginLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userInfo,
|
userInfo: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function authDevLogin(account: string) {
|
||||||
|
const { token: accessToken } = await devLoginApi({ account });
|
||||||
|
if (!accessToken) {
|
||||||
|
return {
|
||||||
|
userInfo: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return finalizeLogin(accessToken, {
|
||||||
|
notify: false,
|
||||||
|
skipRedirect: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function logout(redirect: boolean = true) {
|
async function logout(redirect: boolean = true) {
|
||||||
try {
|
try {
|
||||||
await logoutApi();
|
await logoutApi();
|
||||||
@@ -109,6 +141,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
$reset,
|
$reset,
|
||||||
|
authDevLogin,
|
||||||
authLogin,
|
authLogin,
|
||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
loginLoading,
|
loginLoading,
|
||||||
|
|||||||
Reference in New Issue
Block a user