feat(auth): 添加多租户登录支持和权限控制
- 在登录接口中添加租户编码参数支持 - 实现租户隔离的用户认证逻辑 - 添加平台管理员和租户用户的区分处理 - 集成 MyBatis Plus 多租户插件实现数据隔离 - 在 JWT Token 中添加租户 ID 信息 - 实现前端登录页面租户编码输入字段 - 添加 401 认证失败时的自动登出处理 - 优化权限缓存机制并集成 Redis - 添加租户状态和过期时间验证master
parent
5b73b53de3
commit
69dc3e6788
|
|
@ -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<String> 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<String> 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());
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import lombok.Data;
|
|||
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
private String tenantCode;
|
||||
@NotBlank
|
||||
private String username;
|
||||
@NotBlank
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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<SysTenant> {
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
@Select("SELECT * FROM sys_tenant WHERE id = #{id}")
|
||||
SysTenant selectByIdIgnoreTenant(@Param("id") Long id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SysUser> {
|
||||
@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<SysUser> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> permissions;
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -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<SysUser>()
|
||||
.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<SysUser>()
|
||||
.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<String, Object> 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<String, Object> 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());
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ public class SysParamServiceImpl extends ServiceImpl<SysParamMapper, SysParam> 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());
|
||||
|
|
|
|||
|
|
@ -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<SysPermissionMapper, SysPermission> implements SysPermissionService {
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
public SysPermissionServiceImpl(@Lazy SysUserService sysUserService) {
|
||||
this.sysUserService = sysUserService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysPermission> 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SysUserMapper, SysUser> implements SysUserService {
|
||||
|
||||
@Override
|
||||
|
||||
public List<SysUser> 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<SysUser> query = new LambdaQueryWrapper<SysUser>()
|
||||
.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() + "] 的用户");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface TokenResponse {
|
|||
export interface LoginPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
tenantCode?: string;
|
||||
captchaId?: string;
|
||||
captchaCode?: string;
|
||||
deviceCode?: string;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<Form.Item
|
||||
name="tenantCode"
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<ShopOutlined className="text-gray-400" aria-hidden="true" />}
|
||||
placeholder="租户编码 (平台管理可留空)"
|
||||
aria-label="租户编码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: "请输入用户名" }]}
|
||||
|
|
|
|||
Loading…
Reference in New Issue