feat: 增加开发模式 URL 免登录

- 新增 dev-only 且仅限本机访问的 admin 免登入口

- 管理端支持通过 ?devLogin=admin 自动换取登录态并清理 URL 参数

- 删除未受保护的临时 token 接口并补充关键单测
This commit is contained in:
2026-03-07 18:16:42 +08:00
parent 37e185e74a
commit a93f7ca216
14 changed files with 459 additions and 96 deletions

View File

@@ -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;
}
}
}

View File

@@ -8,6 +8,7 @@ import org.springframework.context.annotation.Configuration;
public class LoginProperties {
private String[] excludes;
private DevBypassProperties devBypass = new DevBypassProperties();
public String[] getExcludes() {
return excludes;
@@ -20,4 +21,43 @@ public class LoginProperties {
public void setExcludes(String[] 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;
}
}
}

View File

@@ -8,4 +8,9 @@ public interface AuthService {
* 登录
*/
LoginVO login(LoginDTO loginDTO);
/**
* 开发模式免登录
*/
LoginVO devLogin(String account);
}

View File

@@ -38,35 +38,29 @@ public class AuthServiceImpl implements AuthService, StpInterface {
@Override
public LoginVO login(LoginDTO loginDTO) {
LoginVO res = new LoginVO();
try {
TenantManager.ignoreTenantCondition();
String pwd = loginDTO.getPassword();
QueryWrapper w = QueryWrapper.create();
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("账号未启用,请联系管理员");
}
SysAccount record = getAvailableAccount(loginDTO.getAccount(), "用户名/密码错误");
String pwdDb = record.getPassword();
if (!BCrypt.checkpw(pwd, pwdDb)) {
throw new BusinessException("用户名/密码错误");
}
StpUtil.login(record.getId());
LoginAccount loginAccount = new LoginAccount();
BeanUtil.copyProperties(record, loginAccount);
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);
String tokenValue = StpUtil.getTokenValue();
res.setToken(tokenValue);
res.setNickname(record.getNickname());
res.setAvatar(record.getAvatar());
return createLoginVO(record);
} finally {
TenantManager.restoreTenantCondition();
}
}
@Override
public LoginVO devLogin(String account) {
try {
TenantManager.ignoreTenantCondition();
SysAccount record = getAvailableAccount(account, "开发免登账号不存在");
return createLoginVO(record);
} finally {
TenantManager.restoreTenantCondition();
}
return res;
}
@Override
@@ -83,4 +77,30 @@ public class AuthServiceImpl implements AuthService, StpInterface {
List<SysRole> roles = sysRoleService.getRolesByAccountId(BigInteger.valueOf(Long.parseLong(loginId.toString())));
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;
}
}

View File

@@ -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;
}
}