feat(auth): 添加多租户登录支持和权限控制
- 在登录接口中添加租户编码参数支持 - 实现租户隔离的用户认证逻辑 - 添加平台管理员和租户用户的区分处理 - 集成 MyBatis Plus 多租户插件实现数据隔离 - 在 JWT Token 中添加租户 ID 信息 - 实现前端登录页面租户编码输入字段 - 添加 401 认证失败时的自动登出处理 - 优化权限缓存机制并集成 Redis - 添加租户状态和过期时间验证master
parent
5b73b53de3
commit
69dc3e6788
|
|
@ -1,12 +1,19 @@
|
||||||
package com.imeeting.auth;
|
package com.imeeting.auth;
|
||||||
|
|
||||||
|
import com.imeeting.entity.SysTenant;
|
||||||
|
import com.imeeting.entity.SysUser;
|
||||||
import com.imeeting.security.LoginUser;
|
import com.imeeting.security.LoginUser;
|
||||||
|
import com.imeeting.service.SysParamService;
|
||||||
import com.imeeting.service.SysPermissionService;
|
import com.imeeting.service.SysPermissionService;
|
||||||
|
import com.imeeting.mapper.SysTenantMapper;
|
||||||
|
import com.imeeting.mapper.SysUserMapper;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
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.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
|
@ -14,21 +21,42 @@ import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
private final SysPermissionService sysPermissionService;
|
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.jwtTokenProvider = jwtTokenProvider;
|
||||||
this.sysPermissionService = sysPermissionService;
|
this.sysPermissionService = sysPermissionService;
|
||||||
|
this.sysTenantMapper = sysTenantMapper;
|
||||||
|
this.sysUserMapper = sysUserMapper;
|
||||||
|
this.sysParamService = sysParamService;
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
throws ServletException, IOException {
|
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");
|
String authHeader = request.getHeader("Authorization");
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
String token = authHeader.substring(7);
|
String token = authHeader.substring(7);
|
||||||
|
|
@ -36,10 +64,51 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
Claims claims = jwtTokenProvider.parseToken(token);
|
Claims claims = jwtTokenProvider.parseToken(token);
|
||||||
String username = claims.get("username", String.class);
|
String username = claims.get("username", String.class);
|
||||||
Long userId = claims.get("userId", Long.class);
|
Long userId = claims.get("userId", Long.class);
|
||||||
|
Long tenantId = claims.get("tenantId", Long.class);
|
||||||
|
|
||||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
Set<String> permissions = sysPermissionService.listPermissionCodesByUserId(userId);
|
// 1. Validate User Status (Ignore Tenant isolation here)
|
||||||
LoginUser loginUser = new LoginUser(userId, username, permissions);
|
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 =
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
|
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class LoginRequest {
|
public class LoginRequest {
|
||||||
|
private String tenantCode;
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String username;
|
private String username;
|
||||||
@NotBlank
|
@NotBlank
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,64 @@
|
||||||
package com.imeeting.config;
|
package com.imeeting.config;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
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.apache.ibatis.reflection.MetaObject;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class MybatisPlusConfig {
|
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
|
@Bean
|
||||||
public MetaObjectHandler metaObjectHandler() {
|
public MetaObjectHandler metaObjectHandler() {
|
||||||
return new MetaObjectHandler() {
|
return new MetaObjectHandler() {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@ package com.imeeting.mapper;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.imeeting.entity.SysTenant;
|
import com.imeeting.entity.SysTenant;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
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
|
@Mapper
|
||||||
public interface SysTenantMapper extends BaseMapper<SysTenant> {
|
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.Mapper;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface SysUserMapper extends BaseMapper<SysUser> {
|
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}")
|
@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);
|
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
|
@AllArgsConstructor
|
||||||
public class LoginUser implements UserDetails {
|
public class LoginUser implements UserDetails {
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
private Long tenantId;
|
||||||
private String username;
|
private String username;
|
||||||
|
private Boolean isPlatformAdmin;
|
||||||
private Set<String> permissions;
|
private Set<String> permissions;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.imeeting.common.SysParamKeys;
|
||||||
import com.imeeting.entity.Device;
|
import com.imeeting.entity.Device;
|
||||||
import com.imeeting.entity.SysLog;
|
import com.imeeting.entity.SysLog;
|
||||||
import com.imeeting.entity.SysUser;
|
import com.imeeting.entity.SysUser;
|
||||||
|
import com.imeeting.mapper.SysUserMapper;
|
||||||
import com.imeeting.service.*;
|
import com.imeeting.service.*;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
@ -26,6 +27,7 @@ import java.util.UUID;
|
||||||
@Service
|
@Service
|
||||||
public class AuthServiceImpl implements AuthService {
|
public class AuthServiceImpl implements AuthService {
|
||||||
private final SysUserService sysUserService;
|
private final SysUserService sysUserService;
|
||||||
|
private final SysUserMapper sysUserMapper;
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
private final SysParamService sysParamService;
|
private final SysParamService sysParamService;
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
@ -41,10 +43,17 @@ public class AuthServiceImpl implements AuthService {
|
||||||
@Value("${app.captcha.max-attempts:5}")
|
@Value("${app.captcha.max-attempts:5}")
|
||||||
private int captchaMaxAttempts;
|
private int captchaMaxAttempts;
|
||||||
|
|
||||||
public AuthServiceImpl(SysUserService sysUserService, DeviceService deviceService, SysParamService sysParamService,
|
public AuthServiceImpl(SysUserService sysUserService,
|
||||||
StringRedisTemplate stringRedisTemplate, PasswordEncoder passwordEncoder,
|
SysUserMapper sysUserMapper,
|
||||||
JwtTokenProvider jwtTokenProvider, SysLogService sysLogService, HttpServletRequest httpServletRequest) {
|
DeviceService deviceService,
|
||||||
|
SysParamService sysParamService,
|
||||||
|
StringRedisTemplate stringRedisTemplate,
|
||||||
|
PasswordEncoder passwordEncoder,
|
||||||
|
JwtTokenProvider jwtTokenProvider,
|
||||||
|
SysLogService sysLogService,
|
||||||
|
HttpServletRequest httpServletRequest) {
|
||||||
this.sysUserService = sysUserService;
|
this.sysUserService = sysUserService;
|
||||||
|
this.sysUserMapper = sysUserMapper;
|
||||||
this.deviceService = deviceService;
|
this.deviceService = deviceService;
|
||||||
this.sysParamService = sysParamService;
|
this.sysParamService = sysParamService;
|
||||||
this.stringRedisTemplate = stringRedisTemplate;
|
this.stringRedisTemplate = stringRedisTemplate;
|
||||||
|
|
@ -61,12 +70,20 @@ public class AuthServiceImpl implements AuthService {
|
||||||
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
|
SysUser user;
|
||||||
.eq(SysUser::getUsername, request.getUsername())
|
if (request.getTenantCode() == null || request.getTenantCode().trim().isEmpty()) {
|
||||||
.eq(SysUser::getIsDeleted, 0)
|
// 平台管理登录逻辑 (全局搜索)
|
||||||
.eq(SysUser::getStatus, 1));
|
user = sysUserMapper.selectByUsernameIgnoreTenant(request.getUsername());
|
||||||
if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
|
if (user != null && !Boolean.TRUE.equals(user.getIsPlatformAdmin())) {
|
||||||
throw new IllegalArgumentException("用户名或密码错误");
|
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();
|
String deviceCode = request.getDeviceCode();
|
||||||
|
|
@ -150,12 +167,15 @@ public class AuthServiceImpl implements AuthService {
|
||||||
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
|
SysUser user;
|
||||||
.eq(SysUser::getUsername, request.getUsername())
|
if (request.getTenantCode() == null || request.getTenantCode().trim().isEmpty()) {
|
||||||
.eq(SysUser::getIsDeleted, 0)
|
user = sysUserMapper.selectByUsernameIgnoreTenant(request.getUsername());
|
||||||
.eq(SysUser::getStatus, 1));
|
} else {
|
||||||
if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
|
user = sysUserMapper.selectByUsernameAndTenantCode(request.getUsername(), request.getTenantCode().trim());
|
||||||
throw new IllegalArgumentException("用户名或密码错误");
|
}
|
||||||
|
|
||||||
|
if (user == null || user.getStatus() != 1 || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
|
||||||
|
throw new IllegalArgumentException("用户名、密码或租户编码错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
String deviceCode = UUID.randomUUID().toString().replace("-", "");
|
String deviceCode = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
|
@ -209,12 +229,14 @@ public class AuthServiceImpl implements AuthService {
|
||||||
Map<String, Object> accessClaims = new HashMap<>();
|
Map<String, Object> accessClaims = new HashMap<>();
|
||||||
accessClaims.put("tokenType", "access");
|
accessClaims.put("tokenType", "access");
|
||||||
accessClaims.put("userId", user.getUserId());
|
accessClaims.put("userId", user.getUserId());
|
||||||
|
accessClaims.put("tenantId", user.getTenantId());
|
||||||
accessClaims.put("username", user.getUsername());
|
accessClaims.put("username", user.getUsername());
|
||||||
accessClaims.put("deviceCode", deviceCode);
|
accessClaims.put("deviceCode", deviceCode);
|
||||||
|
|
||||||
Map<String, Object> refreshClaims = new HashMap<>();
|
Map<String, Object> refreshClaims = new HashMap<>();
|
||||||
refreshClaims.put("tokenType", "refresh");
|
refreshClaims.put("tokenType", "refresh");
|
||||||
refreshClaims.put("userId", user.getUserId());
|
refreshClaims.put("userId", user.getUserId());
|
||||||
|
refreshClaims.put("tenantId", user.getTenantId());
|
||||||
refreshClaims.put("deviceCode", deviceCode);
|
refreshClaims.put("deviceCode", deviceCode);
|
||||||
|
|
||||||
String access = jwtTokenProvider.createToken(accessClaims, Duration.ofMinutes(accessMinutes).toMillis());
|
String access = jwtTokenProvider.createToken(accessClaims, Duration.ofMinutes(accessMinutes).toMillis());
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ public class SysParamServiceImpl extends ServiceImpl<SysParamMapper, SysParam> i
|
||||||
} else {
|
} else {
|
||||||
// 4. 数据库也无数据,设置空标记防止穿透
|
// 4. 数据库也无数据,设置空标记防止穿透
|
||||||
try {
|
try {
|
||||||
|
// Use default value or empty marker if needed
|
||||||
redisTemplate.opsForValue().set(redisKey, RedisKeys.CACHE_EMPTY_MARKER, Duration.ofMinutes(5));
|
redisTemplate.opsForValue().set(redisKey, RedisKeys.CACHE_EMPTY_MARKER, Duration.ofMinutes(5));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Redis write empty marker error for key {}: {}", redisKey, e.getMessage());
|
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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.imeeting.entity.SysPermission;
|
import com.imeeting.entity.SysPermission;
|
||||||
|
import com.imeeting.entity.SysUser;
|
||||||
import com.imeeting.mapper.SysPermissionMapper;
|
import com.imeeting.mapper.SysPermissionMapper;
|
||||||
import com.imeeting.service.SysPermissionService;
|
import com.imeeting.service.SysPermissionService;
|
||||||
|
import com.imeeting.service.SysUserService;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -12,14 +15,29 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class SysPermissionServiceImpl extends ServiceImpl<SysPermissionMapper, SysPermission> implements SysPermissionService {
|
public class SysPermissionServiceImpl extends ServiceImpl<SysPermissionMapper, SysPermission> implements SysPermissionService {
|
||||||
|
|
||||||
|
private final SysUserService sysUserService;
|
||||||
|
|
||||||
|
public SysPermissionServiceImpl(@Lazy SysUserService sysUserService) {
|
||||||
|
this.sysUserService = sysUserService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<SysPermission> listByUserId(Long userId) {
|
public List<SysPermission> listByUserId(Long userId) {
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Super admin or Platform Admin gets all permissions
|
||||||
if (userId == 1L) {
|
if (userId == 1L) {
|
||||||
return list();
|
return list();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SysUser user = sysUserService.getById(userId);
|
||||||
|
if (user != null && Boolean.TRUE.equals(user.getIsPlatformAdmin())) {
|
||||||
|
return list();
|
||||||
|
}
|
||||||
|
|
||||||
return baseMapper.selectByUserId(userId);
|
return baseMapper.selectByUserId(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,48 @@
|
||||||
package com.imeeting.service.impl;
|
package com.imeeting.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
import com.imeeting.entity.SysUser;
|
import com.imeeting.entity.SysUser;
|
||||||
|
|
||||||
import com.imeeting.mapper.SysUserMapper;
|
import com.imeeting.mapper.SysUserMapper;
|
||||||
|
|
||||||
import com.imeeting.service.SysUserService;
|
import com.imeeting.service.SysUserService;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|
||||||
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
|
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
||||||
public List<SysUser> listUsersByRoleId(Long roleId) {
|
public List<SysUser> listUsersByRoleId(Long roleId) {
|
||||||
|
|
||||||
return baseMapper.selectUsersByRoleId(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 {
|
export interface LoginPayload {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
tenantCode?: string;
|
||||||
captchaId?: string;
|
captchaId?: string;
|
||||||
captchaCode?: string;
|
captchaCode?: string;
|
||||||
deviceCode?: string;
|
deviceCode?: string;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,17 @@ http.interceptors.response.use(
|
||||||
}
|
}
|
||||||
return resp;
|
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;
|
export default http;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Button, Checkbox, Form, Input, message, Typography } from "antd";
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth";
|
import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth";
|
||||||
import { getCurrentUser, getSystemParamValue } from "../api";
|
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";
|
import "./Login.css";
|
||||||
|
|
||||||
const { Title, Text, Link } = Typography;
|
const { Title, Text, Link } = Typography;
|
||||||
|
|
@ -48,6 +48,7 @@ export default function Login() {
|
||||||
const data = await login({
|
const data = await login({
|
||||||
username: values.username,
|
username: values.username,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
|
tenantCode: values.tenantCode,
|
||||||
captchaId: captchaEnabled ? captcha?.captchaId : undefined,
|
captchaId: captchaEnabled ? captcha?.captchaId : undefined,
|
||||||
captchaCode: captchaEnabled ? values.captchaCode : undefined
|
captchaCode: captchaEnabled ? values.captchaCode : undefined
|
||||||
});
|
});
|
||||||
|
|
@ -114,6 +115,18 @@ export default function Login() {
|
||||||
requiredMark={false}
|
requiredMark={false}
|
||||||
autoComplete="off"
|
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
|
<Form.Item
|
||||||
name="username"
|
name="username"
|
||||||
rules={[{ required: true, message: "请输入用户名" }]}
|
rules={[{ required: true, message: "请输入用户名" }]}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue