From 69dc3e67886fcc7e3f2ef4d07b2bf087ff8a6716 Mon Sep 17 00:00:00 2001 From: chenhao Date: Thu, 12 Feb 2026 15:51:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=B7=BB=E5=8A=A0=E5=A4=9A?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E7=99=BB=E5=BD=95=E6=94=AF=E6=8C=81=E5=92=8C?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在登录接口中添加租户编码参数支持 - 实现租户隔离的用户认证逻辑 - 添加平台管理员和租户用户的区分处理 - 集成 MyBatis Plus 多租户插件实现数据隔离 - 在 JWT Token 中添加租户 ID 信息 - 实现前端登录页面租户编码输入字段 - 添加 401 认证失败时的自动登出处理 - 优化权限缓存机制并集成 Redis - 添加租户状态和过期时间验证 --- .../auth/JwtAuthenticationFilter.java | 75 ++++++++++++++++++- .../com/imeeting/auth/dto/LoginRequest.java | 1 + .../imeeting/config/MybatisPlusConfig.java | 50 +++++++++++++ .../com/imeeting/mapper/SysTenantMapper.java | 6 ++ .../com/imeeting/mapper/SysUserMapper.java | 20 +++++ .../java/com/imeeting/security/LoginUser.java | 2 + .../service/impl/AuthServiceImpl.java | 52 +++++++++---- .../service/impl/SysParamServiceImpl.java | 1 + .../impl/SysPermissionServiceImpl.java | 18 +++++ .../service/impl/SysUserServiceImpl.java | 41 +++++++--- frontend/src/api/auth.ts | 1 + frontend/src/api/http.ts | 12 ++- frontend/src/pages/Login.tsx | 15 +++- 13 files changed, 262 insertions(+), 32 deletions(-) diff --git a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java b/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java index a0c9e90..35ccff5 100644 --- a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java +++ b/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java @@ -1,12 +1,19 @@ package com.imeeting.auth; +import com.imeeting.entity.SysTenant; +import com.imeeting.entity.SysUser; import com.imeeting.security.LoginUser; +import com.imeeting.service.SysParamService; import com.imeeting.service.SysPermissionService; +import com.imeeting.mapper.SysTenantMapper; +import com.imeeting.mapper.SysUserMapper; import io.jsonwebtoken.Claims; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -14,21 +21,42 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Set; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; private final SysPermissionService sysPermissionService; + private final SysTenantMapper sysTenantMapper; + private final SysUserMapper sysUserMapper; + private final SysParamService sysParamService; + private final StringRedisTemplate redisTemplate; - public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, SysPermissionService sysPermissionService) { + public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, + @Lazy SysPermissionService sysPermissionService, + SysTenantMapper sysTenantMapper, + SysUserMapper sysUserMapper, + @Lazy SysParamService sysParamService, + StringRedisTemplate redisTemplate) { this.jwtTokenProvider = jwtTokenProvider; this.sysPermissionService = sysPermissionService; + this.sysTenantMapper = sysTenantMapper; + this.sysUserMapper = sysUserMapper; + this.sysParamService = sysParamService; + this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String uri = request.getRequestURI(); + // Skip filter for public endpoints + if (uri.startsWith("/auth/") || uri.equals("/api/params/value")) { + filterChain.doFilter(request, response); + return; + } + String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); @@ -36,10 +64,51 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { Claims claims = jwtTokenProvider.parseToken(token); String username = claims.get("username", String.class); Long userId = claims.get("userId", Long.class); + Long tenantId = claims.get("tenantId", Long.class); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - Set permissions = sysPermissionService.listPermissionCodesByUserId(userId); - LoginUser loginUser = new LoginUser(userId, username, permissions); + // 1. Validate User Status (Ignore Tenant isolation here) + SysUser user = sysUserMapper.selectByIdIgnoreTenant(userId); + if (user == null || user.getStatus() != 1 || user.getIsDeleted() == 1) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User account is disabled or deleted"); + return; + } + + // 2. Validate Tenant Status & Grace Period + if (tenantId != null) { + SysTenant tenant = sysTenantMapper.selectByIdIgnoreTenant(tenantId); + if (tenant == null || tenant.getStatus() != 1) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Tenant is disabled"); + return; + } + + if (tenant.getExpireTime() != null) { + LocalDateTime now = LocalDateTime.now(); + if (now.isAfter(tenant.getExpireTime())) { + String graceDaysStr = sysParamService.getParamValue("sys.tenant.grace_period_days", "0"); + int graceDays = Integer.parseInt(graceDaysStr); + if (now.isAfter(tenant.getExpireTime().plusDays(graceDays))) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Tenant subscription expired"); + return; + } + } + } + } + + // 3. Get Permissions (With Redis Cache) + String permKey = "sys:auth:perm:" + userId; + Set permissions; + String cachedPerms = redisTemplate.opsForValue().get(permKey); + if (cachedPerms != null) { + permissions = Set.of(cachedPerms.split(",")); + } else { + permissions = sysPermissionService.listPermissionCodesByUserId(userId); + if (permissions != null && !permissions.isEmpty()) { + redisTemplate.opsForValue().set(permKey, String.join(",", permissions), java.time.Duration.ofHours(2)); + } + } + + LoginUser loginUser = new LoginUser(userId, tenantId, username, user.getIsPlatformAdmin(), permissions); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); diff --git a/backend/src/main/java/com/imeeting/auth/dto/LoginRequest.java b/backend/src/main/java/com/imeeting/auth/dto/LoginRequest.java index 9162a45..43e8a6f 100644 --- a/backend/src/main/java/com/imeeting/auth/dto/LoginRequest.java +++ b/backend/src/main/java/com/imeeting/auth/dto/LoginRequest.java @@ -5,6 +5,7 @@ import lombok.Data; @Data public class LoginRequest { + private String tenantCode; @NotBlank private String username; @NotBlank diff --git a/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java b/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java index ec50eed..0b26466 100644 --- a/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java +++ b/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java @@ -1,14 +1,64 @@ package com.imeeting.config; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import com.imeeting.security.LoginUser; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; import org.apache.ibatis.reflection.MetaObject; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import java.time.LocalDateTime; +import java.util.List; @Configuration public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { + @Override + public Expression getTenantId() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.getPrincipal() instanceof LoginUser) { + LoginUser user = (LoginUser) auth.getPrincipal(); + if (user.getTenantId() != null) { + return new LongValue(user.getTenantId()); + } + } + // If no tenant context (e.g. system task or error), return 0 + return new LongValue(0); + } + + @Override + public String getTenantIdColumn() { + return "tenant_id"; + } + + @Override + public boolean ignoreTable(String tableName) { + // System level tables that should not be filtered by tenant_id + // and check if current user is platform admin to skip filtering + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.getPrincipal() instanceof LoginUser) { + LoginUser user = (LoginUser) auth.getPrincipal(); + if (Boolean.TRUE.equals(user.getIsPlatformAdmin())) { + return true; + } + } + + return List.of("sys_tenant", "sys_dict_type", "sys_dict_item", "sys_log").contains(tableName.toLowerCase()); + } + })); + return interceptor; + } + @Bean public MetaObjectHandler metaObjectHandler() { return new MetaObjectHandler() { diff --git a/backend/src/main/java/com/imeeting/mapper/SysTenantMapper.java b/backend/src/main/java/com/imeeting/mapper/SysTenantMapper.java index 31cd9a2..24a4e5d 100644 --- a/backend/src/main/java/com/imeeting/mapper/SysTenantMapper.java +++ b/backend/src/main/java/com/imeeting/mapper/SysTenantMapper.java @@ -3,7 +3,13 @@ package com.imeeting.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.imeeting.entity.SysTenant; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Param; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; @Mapper public interface SysTenantMapper extends BaseMapper { + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT * FROM sys_tenant WHERE id = #{id}") + SysTenant selectByIdIgnoreTenant(@Param("id") Long id); } diff --git a/backend/src/main/java/com/imeeting/mapper/SysUserMapper.java b/backend/src/main/java/com/imeeting/mapper/SysUserMapper.java index 0fcbd5a..29c78e3 100644 --- a/backend/src/main/java/com/imeeting/mapper/SysUserMapper.java +++ b/backend/src/main/java/com/imeeting/mapper/SysUserMapper.java @@ -5,10 +5,30 @@ import com.imeeting.entity.SysUser; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Param; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; import java.util.List; @Mapper public interface SysUserMapper extends BaseMapper { @Select("SELECT u.* FROM sys_user u JOIN sys_user_role ur ON u.user_id = ur.user_id WHERE ur.role_id = #{roleId}") List selectUsersByRoleId(@Param("roleId") Long roleId); + + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT * FROM sys_user WHERE username = #{username} AND is_deleted = 0") + SysUser selectByUsernameIgnoreTenant(@Param("username") String username); + + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT * FROM sys_user WHERE user_id = #{userId} AND is_deleted = 0") + SysUser selectByIdIgnoreTenant(@Param("userId") Long userId); + + @InterceptorIgnore(tenantLine = "true") + @Select(""" + SELECT u.* + FROM sys_user u + JOIN sys_tenant t ON u.tenant_id = t.id + WHERE u.username = #{username} + AND t.tenant_code = #{tenantCode} + AND u.is_deleted = 0 + """) + SysUser selectByUsernameAndTenantCode(@Param("username") String username, @Param("tenantCode") String tenantCode); } diff --git a/backend/src/main/java/com/imeeting/security/LoginUser.java b/backend/src/main/java/com/imeeting/security/LoginUser.java index 9d9acd0..55b5a6a 100644 --- a/backend/src/main/java/com/imeeting/security/LoginUser.java +++ b/backend/src/main/java/com/imeeting/security/LoginUser.java @@ -16,7 +16,9 @@ import java.util.stream.Collectors; @AllArgsConstructor public class LoginUser implements UserDetails { private Long userId; + private Long tenantId; private String username; + private Boolean isPlatformAdmin; private Set permissions; @Override diff --git a/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java index 7ba00d2..5f6ad8f 100644 --- a/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java @@ -9,6 +9,7 @@ import com.imeeting.common.SysParamKeys; import com.imeeting.entity.Device; import com.imeeting.entity.SysLog; import com.imeeting.entity.SysUser; +import com.imeeting.mapper.SysUserMapper; import com.imeeting.service.*; import io.jsonwebtoken.Claims; import jakarta.servlet.http.HttpServletRequest; @@ -26,6 +27,7 @@ import java.util.UUID; @Service public class AuthServiceImpl implements AuthService { private final SysUserService sysUserService; + private final SysUserMapper sysUserMapper; private final DeviceService deviceService; private final SysParamService sysParamService; private final StringRedisTemplate stringRedisTemplate; @@ -41,10 +43,17 @@ public class AuthServiceImpl implements AuthService { @Value("${app.captcha.max-attempts:5}") private int captchaMaxAttempts; - public AuthServiceImpl(SysUserService sysUserService, DeviceService deviceService, SysParamService sysParamService, - StringRedisTemplate stringRedisTemplate, PasswordEncoder passwordEncoder, - JwtTokenProvider jwtTokenProvider, SysLogService sysLogService, HttpServletRequest httpServletRequest) { + public AuthServiceImpl(SysUserService sysUserService, + SysUserMapper sysUserMapper, + DeviceService deviceService, + SysParamService sysParamService, + StringRedisTemplate stringRedisTemplate, + PasswordEncoder passwordEncoder, + JwtTokenProvider jwtTokenProvider, + SysLogService sysLogService, + HttpServletRequest httpServletRequest) { this.sysUserService = sysUserService; + this.sysUserMapper = sysUserMapper; this.deviceService = deviceService; this.sysParamService = sysParamService; this.stringRedisTemplate = stringRedisTemplate; @@ -61,12 +70,20 @@ public class AuthServiceImpl implements AuthService { validateCaptcha(request.getCaptchaId(), request.getCaptchaCode()); } - SysUser user = sysUserService.getOne(new LambdaQueryWrapper() - .eq(SysUser::getUsername, request.getUsername()) - .eq(SysUser::getIsDeleted, 0) - .eq(SysUser::getStatus, 1)); - if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { - throw new IllegalArgumentException("用户名或密码错误"); + SysUser user; + if (request.getTenantCode() == null || request.getTenantCode().trim().isEmpty()) { + // 平台管理登录逻辑 (全局搜索) + user = sysUserMapper.selectByUsernameIgnoreTenant(request.getUsername()); + if (user != null && !Boolean.TRUE.equals(user.getIsPlatformAdmin())) { + throw new IllegalArgumentException("非平台管理账号请提供租户编码"); + } + } else { + // 租户用户登录逻辑 (按租户搜索) + user = sysUserMapper.selectByUsernameAndTenantCode(request.getUsername(), request.getTenantCode().trim()); + } + + if (user == null || user.getStatus() != 1 || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { + throw new IllegalArgumentException("用户名、密码或租户编码错误"); } String deviceCode = request.getDeviceCode(); @@ -150,12 +167,15 @@ public class AuthServiceImpl implements AuthService { validateCaptcha(request.getCaptchaId(), request.getCaptchaCode()); } - SysUser user = sysUserService.getOne(new LambdaQueryWrapper() - .eq(SysUser::getUsername, request.getUsername()) - .eq(SysUser::getIsDeleted, 0) - .eq(SysUser::getStatus, 1)); - if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { - throw new IllegalArgumentException("用户名或密码错误"); + SysUser user; + if (request.getTenantCode() == null || request.getTenantCode().trim().isEmpty()) { + user = sysUserMapper.selectByUsernameIgnoreTenant(request.getUsername()); + } else { + user = sysUserMapper.selectByUsernameAndTenantCode(request.getUsername(), request.getTenantCode().trim()); + } + + if (user == null || user.getStatus() != 1 || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { + throw new IllegalArgumentException("用户名、密码或租户编码错误"); } String deviceCode = UUID.randomUUID().toString().replace("-", ""); @@ -209,12 +229,14 @@ public class AuthServiceImpl implements AuthService { Map accessClaims = new HashMap<>(); accessClaims.put("tokenType", "access"); accessClaims.put("userId", user.getUserId()); + accessClaims.put("tenantId", user.getTenantId()); accessClaims.put("username", user.getUsername()); accessClaims.put("deviceCode", deviceCode); Map refreshClaims = new HashMap<>(); refreshClaims.put("tokenType", "refresh"); refreshClaims.put("userId", user.getUserId()); + refreshClaims.put("tenantId", user.getTenantId()); refreshClaims.put("deviceCode", deviceCode); String access = jwtTokenProvider.createToken(accessClaims, Duration.ofMinutes(accessMinutes).toMillis()); diff --git a/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java index 751350c..7db63c3 100644 --- a/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java @@ -61,6 +61,7 @@ public class SysParamServiceImpl extends ServiceImpl i } else { // 4. 数据库也无数据,设置空标记防止穿透 try { + // Use default value or empty marker if needed redisTemplate.opsForValue().set(redisKey, RedisKeys.CACHE_EMPTY_MARKER, Duration.ofMinutes(5)); } catch (Exception e) { log.error("Redis write empty marker error for key {}: {}", redisKey, e.getMessage()); diff --git a/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java index d5d00a4..ab22258 100644 --- a/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java @@ -2,8 +2,11 @@ package com.imeeting.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.imeeting.entity.SysPermission; +import com.imeeting.entity.SysUser; import com.imeeting.mapper.SysPermissionMapper; import com.imeeting.service.SysPermissionService; +import com.imeeting.service.SysUserService; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import java.util.List; @@ -12,14 +15,29 @@ import java.util.stream.Collectors; @Service public class SysPermissionServiceImpl extends ServiceImpl implements SysPermissionService { + + private final SysUserService sysUserService; + + public SysPermissionServiceImpl(@Lazy SysUserService sysUserService) { + this.sysUserService = sysUserService; + } + @Override public List listByUserId(Long userId) { if (userId == null) { return List.of(); } + + // Super admin or Platform Admin gets all permissions if (userId == 1L) { return list(); } + + SysUser user = sysUserService.getById(userId); + if (user != null && Boolean.TRUE.equals(user.getIsPlatformAdmin())) { + return list(); + } + return baseMapper.selectByUserId(userId); } diff --git a/backend/src/main/java/com/imeeting/service/impl/SysUserServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysUserServiceImpl.java index c8951df..77da52d 100644 --- a/backend/src/main/java/com/imeeting/service/impl/SysUserServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/impl/SysUserServiceImpl.java @@ -1,31 +1,48 @@ package com.imeeting.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; - import com.imeeting.entity.SysUser; - import com.imeeting.mapper.SysUserMapper; - import com.imeeting.service.SysUserService; - import org.springframework.stereotype.Service; import java.util.List; - - @Service - public class SysUserServiceImpl extends ServiceImpl implements SysUserService { @Override - public List listUsersByRoleId(Long roleId) { - return baseMapper.selectUsersByRoleId(roleId); - } + @Override + public boolean save(SysUser entity) { + validateUniqueUsername(entity); + return super.save(entity); + } + + @Override + public boolean updateById(SysUser entity) { + validateUniqueUsername(entity); + return super.updateById(entity); + } + + private void validateUniqueUsername(SysUser user) { + if (user.getUsername() == null) return; + + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysUser::getUsername, user.getUsername()) + .eq(SysUser::getTenantId, user.getTenantId()) + .eq(SysUser::getIsDeleted, 0); + + if (user.getUserId() != null) { + query.ne(SysUser::getUserId, user.getUserId()); + } + + if (count(query) > 0) { + throw new IllegalArgumentException("该租户下已存在名为 [" + user.getUsername() + "] 的用户"); + } + } } - - diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 1b00f2b..16f3363 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -15,6 +15,7 @@ export interface TokenResponse { export interface LoginPayload { username: string; password: string; + tenantCode?: string; captchaId?: string; captchaCode?: string; deviceCode?: string; diff --git a/frontend/src/api/http.ts b/frontend/src/api/http.ts index c206071..4844c05 100644 --- a/frontend/src/api/http.ts +++ b/frontend/src/api/http.ts @@ -22,7 +22,17 @@ http.interceptors.response.use( } return resp; }, - (error) => Promise.reject(error) + (error) => { + if (error.response && error.response.status === 401) { + // Clear session/local storage + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + sessionStorage.removeItem("userProfile"); + // Force redirect to login + window.location.href = "/login"; + } + return Promise.reject(error); + } ); export default http; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 7e53407..a44a148 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -2,7 +2,7 @@ import { Button, Checkbox, Form, Input, message, Typography } from "antd"; import { useEffect, useState, useCallback } from "react"; import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth"; import { getCurrentUser, getSystemParamValue } from "../api"; -import { UserOutlined, LockOutlined, SafetyOutlined, ReloadOutlined } from "@ant-design/icons"; +import { UserOutlined, LockOutlined, SafetyOutlined, ReloadOutlined, ShopOutlined } from "@ant-design/icons"; import "./Login.css"; const { Title, Text, Link } = Typography; @@ -48,6 +48,7 @@ export default function Login() { const data = await login({ username: values.username, password: values.password, + tenantCode: values.tenantCode, captchaId: captchaEnabled ? captcha?.captchaId : undefined, captchaCode: captchaEnabled ? values.captchaCode : undefined }); @@ -114,6 +115,18 @@ export default function Login() { requiredMark={false} autoComplete="off" > + + } + placeholder="租户编码 (平台管理可留空)" + aria-label="租户编码" + /> + +