You've already forked DataMate
feat(auth): 完善API网关JWT认证和权限控制功能
- 实现网关侧JWT工具类和权限规则匹配器 - 集成JWT认证流程,支持Bearer Token验证 - 添加基于路径和HTTP方法的权限控制机制 - 配置白名单路由规则,优化认证性能 - 更新前端受保护路由组件,实现权限验证 - 添加403禁止访问页面和权限检查逻辑 - 重构登录页面,集成实际认证API调用 - 实现用户信息获取和权限加载功能 - 优化全局异常处理器中的认证错误状态码 - 集成FastJSON2和JJWT依赖库支持
This commit is contained in:
@@ -36,6 +36,23 @@
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,34 +1,124 @@
|
||||
package com.datamate.gateway.filter;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.datamate.gateway.security.GatewayJwtUtils;
|
||||
import com.datamate.gateway.security.PermissionRuleMatcher;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户信息过滤器
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserContextFilter implements GlobalFilter {
|
||||
@Value("${commercial.switch:false}")
|
||||
private boolean isCommercial;
|
||||
public class UserContextFilter implements GlobalFilter, Ordered {
|
||||
private final GatewayJwtUtils gatewayJwtUtils;
|
||||
private final PermissionRuleMatcher permissionRuleMatcher;
|
||||
|
||||
@Value("${datamate.auth.enabled:true}")
|
||||
private boolean authEnabled;
|
||||
|
||||
public UserContextFilter(GatewayJwtUtils gatewayJwtUtils, PermissionRuleMatcher permissionRuleMatcher) {
|
||||
this.gatewayJwtUtils = gatewayJwtUtils;
|
||||
this.permissionRuleMatcher = permissionRuleMatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
if (!isCommercial) {
|
||||
if (!authEnabled) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
try {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String path = request.getURI().getPath();
|
||||
HttpMethod method = request.getMethod();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("get current user info error", e);
|
||||
if (!path.startsWith("/api/")) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
return chain.filter(exchange);
|
||||
if (HttpMethod.OPTIONS.equals(method)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
if (permissionRuleMatcher.isWhitelisted(path)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
String token = extractBearerToken(request.getHeaders().getFirst("Authorization"));
|
||||
if (!StringUtils.hasText(token)) {
|
||||
return writeError(exchange, HttpStatus.UNAUTHORIZED, "auth.0003", "未登录或登录状态已失效");
|
||||
}
|
||||
|
||||
Claims claims;
|
||||
try {
|
||||
if (!gatewayJwtUtils.validateToken(token)) {
|
||||
return writeError(exchange, HttpStatus.UNAUTHORIZED, "auth.0003", "登录状态已失效");
|
||||
}
|
||||
claims = gatewayJwtUtils.getClaimsFromToken(token);
|
||||
} catch (Exception ex) {
|
||||
log.warn("JWT校验失败: {}", ex.getMessage());
|
||||
return writeError(exchange, HttpStatus.UNAUTHORIZED, "auth.0003", "登录状态已失效");
|
||||
}
|
||||
|
||||
String requiredPermission = permissionRuleMatcher.resolveRequiredPermission(method, path);
|
||||
if (StringUtils.hasText(requiredPermission)) {
|
||||
List<String> permissionCodes = gatewayJwtUtils.getStringListClaim(claims, "permissions");
|
||||
if (!permissionCodes.contains(requiredPermission)) {
|
||||
return writeError(exchange, HttpStatus.FORBIDDEN, "auth.0006", "权限不足");
|
||||
}
|
||||
}
|
||||
|
||||
String userId = String.valueOf(claims.get("userId"));
|
||||
String username = claims.getSubject();
|
||||
List<String> roles = gatewayJwtUtils.getStringListClaim(claims, "roles");
|
||||
|
||||
ServerHttpRequest mutatedRequest = request.mutate()
|
||||
.header("X-User-Id", userId)
|
||||
.header("X-User-Name", username)
|
||||
.header("X-User-Roles", String.join(",", roles))
|
||||
.build();
|
||||
return chain.filter(exchange.mutate().request(mutatedRequest).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return -200;
|
||||
}
|
||||
|
||||
private String extractBearerToken(String authorizationHeader) {
|
||||
if (!StringUtils.hasText(authorizationHeader)) {
|
||||
return null;
|
||||
}
|
||||
if (!authorizationHeader.startsWith("Bearer ")) {
|
||||
return null;
|
||||
}
|
||||
String token = authorizationHeader.substring("Bearer ".length()).trim();
|
||||
return token.isEmpty() ? null : token;
|
||||
}
|
||||
|
||||
private Mono<Void> writeError(ServerWebExchange exchange,
|
||||
HttpStatus status,
|
||||
String code,
|
||||
String message) {
|
||||
exchange.getResponse().setStatusCode(status);
|
||||
exchange.getResponse().getHeaders().set("Content-Type", "application/json;charset=UTF-8");
|
||||
byte[] body = JSONObject.toJSONString(new ErrorBody(code, message, null))
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body)));
|
||||
}
|
||||
|
||||
private record ErrorBody(String code, String message, Object data) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.datamate.gateway.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 网关侧JWT工具
|
||||
*/
|
||||
@Component
|
||||
public class GatewayJwtUtils {
|
||||
private static final String DEFAULT_SECRET = "datamate-secret-key-for-jwt-token-generation";
|
||||
|
||||
@Value("${jwt.secret:" + DEFAULT_SECRET + "}")
|
||||
private String secret;
|
||||
|
||||
public Claims getClaimsFromToken(String token) {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
Date expiration = claims.getExpiration();
|
||||
return expiration != null && expiration.after(new Date());
|
||||
}
|
||||
|
||||
public List<String> getStringListClaim(Claims claims, String claimName) {
|
||||
Object claimValue = claims.get(claimName);
|
||||
if (!(claimValue instanceof Collection<?> values)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return values.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private SecretKey getSigningKey() {
|
||||
String secretValue = StringUtils.hasText(secret) ? secret : DEFAULT_SECRET;
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
byte[] keyBytes = digest.digest(secretValue.getBytes(StandardCharsets.UTF_8));
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Cannot initialize JWT signing key", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.datamate.gateway.security;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 权限规则匹配器
|
||||
*/
|
||||
@Component
|
||||
public class PermissionRuleMatcher {
|
||||
private static final Set<HttpMethod> READ_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
|
||||
private static final Set<HttpMethod> WRITE_METHODS = Set.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE);
|
||||
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
private final List<String> whiteListPatterns = List.of(
|
||||
"/api/auth/login",
|
||||
"/api/auth/login/**"
|
||||
);
|
||||
private final List<PermissionRule> rules = buildRules();
|
||||
|
||||
public boolean isWhitelisted(String path) {
|
||||
return whiteListPatterns.stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
|
||||
}
|
||||
|
||||
public String resolveRequiredPermission(HttpMethod method, String path) {
|
||||
for (PermissionRule rule : rules) {
|
||||
if (rule.matches(method, path, pathMatcher)) {
|
||||
return rule.getPermissionCode();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<PermissionRule> buildRules() {
|
||||
List<PermissionRule> permissionRules = new ArrayList<>();
|
||||
addModuleRules(permissionRules, "/api/data-management/**", "module:data-management:read", "module:data-management:write");
|
||||
addModuleRules(permissionRules, "/api/annotation/**", "module:data-annotation:read", "module:data-annotation:write");
|
||||
addModuleRules(permissionRules, "/api/data-collection/**", "module:data-collection:read", "module:data-collection:write");
|
||||
addModuleRules(permissionRules, "/api/evaluation/**", "module:data-evaluation:read", "module:data-evaluation:write");
|
||||
addModuleRules(permissionRules, "/api/synthesis/**", "module:data-synthesis:read", "module:data-synthesis:write");
|
||||
addModuleRules(permissionRules, "/api/knowledge-base/**", "module:knowledge-base:read", "module:knowledge-base:write");
|
||||
addModuleRules(permissionRules, "/api/operator-market/**", "module:operator-market:read", "module:operator-market:write");
|
||||
addModuleRules(permissionRules, "/api/orchestration/**", "module:orchestration:read", "module:orchestration:write");
|
||||
addModuleRules(permissionRules, "/api/content-generation/**", "module:content-generation:use", "module:content-generation:use");
|
||||
|
||||
permissionRules.add(new PermissionRule(READ_METHODS, "/api/auth/users/**", "system:user:manage"));
|
||||
permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/users/**", "system:user:manage"));
|
||||
permissionRules.add(new PermissionRule(READ_METHODS, "/api/auth/roles/**", "system:role:manage"));
|
||||
permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/roles/**", "system:role:manage"));
|
||||
permissionRules.add(new PermissionRule(READ_METHODS, "/api/auth/permissions/**", "system:permission:manage"));
|
||||
permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/permissions/**", "system:permission:manage"));
|
||||
return permissionRules;
|
||||
}
|
||||
|
||||
private void addModuleRules(List<PermissionRule> rules,
|
||||
String pathPattern,
|
||||
String readPermissionCode,
|
||||
String writePermissionCode) {
|
||||
rules.add(new PermissionRule(READ_METHODS, pathPattern, readPermissionCode));
|
||||
rules.add(new PermissionRule(WRITE_METHODS, pathPattern, writePermissionCode));
|
||||
}
|
||||
|
||||
@Getter
|
||||
private static class PermissionRule {
|
||||
private final Set<HttpMethod> methods;
|
||||
private final String pathPattern;
|
||||
private final String permissionCode;
|
||||
|
||||
private PermissionRule(Set<HttpMethod> methods, String pathPattern, String permissionCode) {
|
||||
this.methods = methods;
|
||||
this.pathPattern = pathPattern;
|
||||
this.permissionCode = permissionCode;
|
||||
}
|
||||
|
||||
private boolean matches(HttpMethod method, String path, AntPathMatcher matcher) {
|
||||
return method != null && methods.contains(method) && matcher.match(pathPattern, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@
|
||||
<description>DDD领域通用组件</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.datamate</groupId>
|
||||
<artifactId>security-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.datamate.common.auth.application;
|
||||
|
||||
import com.datamate.common.auth.domain.model.AuthPermissionInfo;
|
||||
import com.datamate.common.auth.domain.model.AuthRoleInfo;
|
||||
import com.datamate.common.auth.domain.model.AuthUserAccount;
|
||||
import com.datamate.common.auth.domain.model.AuthUserSummary;
|
||||
import com.datamate.common.auth.infrastructure.exception.AuthErrorCode;
|
||||
import com.datamate.common.auth.infrastructure.persistence.mapper.AuthMapper;
|
||||
import com.datamate.common.auth.interfaces.rest.dto.AuthCurrentUserResponse;
|
||||
import com.datamate.common.auth.interfaces.rest.dto.AuthLoginResponse;
|
||||
import com.datamate.common.auth.interfaces.rest.dto.AuthUserView;
|
||||
import com.datamate.common.auth.interfaces.rest.dto.AuthUserWithRolesResponse;
|
||||
import com.datamate.common.infrastructure.exception.BusinessAssert;
|
||||
import com.datamate.common.security.JwtUtils;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 认证授权应用服务
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthApplicationService {
|
||||
private static final String TOKEN_TYPE = "Bearer";
|
||||
|
||||
private final AuthMapper authMapper;
|
||||
private final JwtUtils jwtUtils;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public AuthLoginResponse login(String username, String password) {
|
||||
AuthUserAccount user = authMapper.findUserByUsername(username);
|
||||
BusinessAssert.notNull(user, AuthErrorCode.INVALID_CREDENTIALS);
|
||||
BusinessAssert.isTrue(Boolean.TRUE.equals(user.getEnabled()), AuthErrorCode.ACCOUNT_DISABLED);
|
||||
BusinessAssert.isTrue(passwordEncoder.matches(password, user.getPasswordHash()), AuthErrorCode.INVALID_CREDENTIALS);
|
||||
|
||||
AuthBundle authBundle = loadAuthBundle(user.getId());
|
||||
String token = buildToken(authBundle);
|
||||
authMapper.updateLastLoginAt(user.getId());
|
||||
|
||||
return new AuthLoginResponse(
|
||||
token,
|
||||
TOKEN_TYPE,
|
||||
computeExpiresInSeconds(token),
|
||||
toUserView(authBundle.user()),
|
||||
authBundle.roleCodes(),
|
||||
authBundle.permissionCodes()
|
||||
);
|
||||
}
|
||||
|
||||
public AuthCurrentUserResponse getCurrentUser(String token) {
|
||||
Claims claims = parseClaims(token);
|
||||
Long userId = parseUserId(claims);
|
||||
AuthBundle authBundle = loadAuthBundle(userId);
|
||||
return new AuthCurrentUserResponse(
|
||||
toUserView(authBundle.user()),
|
||||
authBundle.roleCodes(),
|
||||
authBundle.permissionCodes()
|
||||
);
|
||||
}
|
||||
|
||||
public AuthLoginResponse refreshToken(String token) {
|
||||
Claims claims = parseClaims(token);
|
||||
Long userId = parseUserId(claims);
|
||||
AuthBundle authBundle = loadAuthBundle(userId);
|
||||
String refreshedToken = buildToken(authBundle);
|
||||
return new AuthLoginResponse(
|
||||
refreshedToken,
|
||||
TOKEN_TYPE,
|
||||
computeExpiresInSeconds(refreshedToken),
|
||||
toUserView(authBundle.user()),
|
||||
authBundle.roleCodes(),
|
||||
authBundle.permissionCodes()
|
||||
);
|
||||
}
|
||||
|
||||
public List<AuthUserWithRolesResponse> listUsersWithRoles() {
|
||||
List<AuthUserSummary> users = authMapper.listUsers();
|
||||
List<AuthUserWithRolesResponse> responses = new ArrayList<>(users.size());
|
||||
for (AuthUserSummary user : users) {
|
||||
List<String> roleCodes = authMapper.findRolesByUserId(user.getId())
|
||||
.stream()
|
||||
.map(AuthRoleInfo::getRoleCode)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
responses.add(new AuthUserWithRolesResponse(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getFullName(),
|
||||
user.getEmail(),
|
||||
user.getEnabled(),
|
||||
roleCodes
|
||||
));
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
public List<AuthRoleInfo> listRoles() {
|
||||
return authMapper.listRoles();
|
||||
}
|
||||
|
||||
public List<AuthPermissionInfo> listPermissions() {
|
||||
return authMapper.listPermissions();
|
||||
}
|
||||
|
||||
public void assignUserRoles(Long userId, List<String> roleIds) {
|
||||
AuthUserAccount user = authMapper.findUserById(userId);
|
||||
BusinessAssert.notNull(user, AuthErrorCode.USER_NOT_FOUND);
|
||||
|
||||
Set<String> distinctRoleIds = new LinkedHashSet<>(roleIds);
|
||||
BusinessAssert.notEmpty(distinctRoleIds, AuthErrorCode.ROLE_NOT_FOUND);
|
||||
|
||||
int existingRoleCount = authMapper.countRolesByIds(new ArrayList<>(distinctRoleIds));
|
||||
BusinessAssert.isTrue(existingRoleCount == distinctRoleIds.size(), AuthErrorCode.ROLE_NOT_FOUND);
|
||||
|
||||
authMapper.deleteUserRoles(userId);
|
||||
authMapper.insertUserRoles(userId, new ArrayList<>(distinctRoleIds));
|
||||
}
|
||||
|
||||
private String buildToken(AuthBundle authBundle) {
|
||||
Map<String, Object> claims = Map.of(
|
||||
"userId", authBundle.user().getId(),
|
||||
"roles", authBundle.roleCodes(),
|
||||
"permissions", authBundle.permissionCodes()
|
||||
);
|
||||
return jwtUtils.generateToken(authBundle.user().getUsername(), claims);
|
||||
}
|
||||
|
||||
private AuthBundle loadAuthBundle(Long userId) {
|
||||
AuthUserAccount user = authMapper.findUserById(userId);
|
||||
BusinessAssert.notNull(user, AuthErrorCode.USER_NOT_FOUND);
|
||||
BusinessAssert.isTrue(Boolean.TRUE.equals(user.getEnabled()), AuthErrorCode.ACCOUNT_DISABLED);
|
||||
|
||||
List<String> roleCodes = authMapper.findRolesByUserId(userId).stream()
|
||||
.map(AuthRoleInfo::getRoleCode)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
List<String> permissionCodes = authMapper.findPermissionCodesByUserId(userId).stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
return new AuthBundle(user, roleCodes, permissionCodes);
|
||||
}
|
||||
|
||||
private Claims parseClaims(String token) {
|
||||
try {
|
||||
return jwtUtils.getClaimsFromToken(token);
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
throw com.datamate.common.infrastructure.exception.BusinessException.of(AuthErrorCode.TOKEN_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
private Long parseUserId(Claims claims) {
|
||||
Object userIdObject = claims.get("userId");
|
||||
if (userIdObject instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (userIdObject instanceof String str) {
|
||||
try {
|
||||
return Long.parseLong(str);
|
||||
} catch (NumberFormatException e) {
|
||||
throw com.datamate.common.infrastructure.exception.BusinessException.of(AuthErrorCode.TOKEN_INVALID);
|
||||
}
|
||||
}
|
||||
throw com.datamate.common.infrastructure.exception.BusinessException.of(AuthErrorCode.TOKEN_INVALID);
|
||||
}
|
||||
|
||||
private long computeExpiresInSeconds(String token) {
|
||||
Date expirationDate = jwtUtils.getExpirationDateFromToken(token);
|
||||
long seconds = Duration.between(new Date().toInstant(), expirationDate.toInstant()).toSeconds();
|
||||
return Math.max(seconds, 0L);
|
||||
}
|
||||
|
||||
private AuthUserView toUserView(AuthUserAccount user) {
|
||||
return new AuthUserView(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getFullName(),
|
||||
user.getEmail(),
|
||||
user.getAvatarUrl(),
|
||||
user.getOrganization()
|
||||
);
|
||||
}
|
||||
|
||||
private record AuthBundle(
|
||||
AuthUserAccount user,
|
||||
List<String> roleCodes,
|
||||
List<String> permissionCodes
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.datamate.common.auth.domain.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 权限信息
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class AuthPermissionInfo {
|
||||
private String id;
|
||||
private String permissionCode;
|
||||
private String permissionName;
|
||||
private String module;
|
||||
private String action;
|
||||
private String pathPattern;
|
||||
private String method;
|
||||
private Boolean enabled;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.datamate.common.auth.domain.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 角色信息
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class AuthRoleInfo {
|
||||
private String id;
|
||||
private String roleCode;
|
||||
private String roleName;
|
||||
private String description;
|
||||
private Boolean enabled;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.datamate.common.auth.domain.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 认证用户账户
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class AuthUserAccount {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String email;
|
||||
private String passwordHash;
|
||||
private String fullName;
|
||||
private String avatarUrl;
|
||||
private String organization;
|
||||
private Boolean enabled;
|
||||
private LocalDateTime lastLoginAt;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.datamate.common.auth.domain.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 用户摘要
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class AuthUserSummary {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String email;
|
||||
private String fullName;
|
||||
private Boolean enabled;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.datamate.common.auth.infrastructure.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
/**
|
||||
* 认证模块配置
|
||||
*/
|
||||
@Configuration
|
||||
public class AuthConfiguration {
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.datamate.common.auth.infrastructure.exception;
|
||||
|
||||
import com.datamate.common.infrastructure.exception.ErrorCode;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 认证授权错误码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum AuthErrorCode implements ErrorCode {
|
||||
INVALID_CREDENTIALS("auth.0001", "用户名或密码错误"),
|
||||
ACCOUNT_DISABLED("auth.0002", "账号已被禁用"),
|
||||
TOKEN_INVALID("auth.0003", "登录状态已失效"),
|
||||
USER_NOT_FOUND("auth.0004", "用户不存在"),
|
||||
ROLE_NOT_FOUND("auth.0005", "角色不存在"),
|
||||
AUTHORIZATION_DENIED("auth.0006", "无权限执行该操作");
|
||||
|
||||
private final String code;
|
||||
private final String message;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.datamate.common.auth.infrastructure.persistence.mapper;
|
||||
|
||||
import com.datamate.common.auth.domain.model.AuthPermissionInfo;
|
||||
import com.datamate.common.auth.domain.model.AuthRoleInfo;
|
||||
import com.datamate.common.auth.domain.model.AuthUserAccount;
|
||||
import com.datamate.common.auth.domain.model.AuthUserSummary;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 认证授权数据访问
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuthMapper {
|
||||
AuthUserAccount findUserByUsername(@Param("username") String username);
|
||||
|
||||
AuthUserAccount findUserById(@Param("userId") Long userId);
|
||||
|
||||
int updateLastLoginAt(@Param("userId") Long userId);
|
||||
|
||||
List<AuthRoleInfo> findRolesByUserId(@Param("userId") Long userId);
|
||||
|
||||
List<String> findPermissionCodesByUserId(@Param("userId") Long userId);
|
||||
|
||||
List<AuthUserSummary> listUsers();
|
||||
|
||||
List<AuthRoleInfo> listRoles();
|
||||
|
||||
List<AuthPermissionInfo> listPermissions();
|
||||
|
||||
int countRolesByIds(@Param("roleIds") List<String> roleIds);
|
||||
|
||||
int deleteUserRoles(@Param("userId") Long userId);
|
||||
|
||||
int insertUserRoles(@Param("userId") Long userId, @Param("roleIds") List<String> roleIds);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.datamate.common.auth.interfaces.rest;
|
||||
|
||||
import com.datamate.common.auth.application.AuthApplicationService;
|
||||
import com.datamate.common.auth.domain.model.AuthPermissionInfo;
|
||||
import com.datamate.common.auth.domain.model.AuthRoleInfo;
|
||||
import com.datamate.common.auth.interfaces.rest.dto.AssignUserRolesRequest;
|
||||
import com.datamate.common.auth.interfaces.rest.dto.AuthCurrentUserResponse;
|
||||
import com.datamate.common.auth.interfaces.rest.dto.AuthLoginResponse;
|
||||
import com.datamate.common.auth.interfaces.rest.dto.AuthUserWithRolesResponse;
|
||||
import com.datamate.common.auth.interfaces.rest.dto.LoginRequest;
|
||||
import com.datamate.common.infrastructure.exception.BusinessAssert;
|
||||
import com.datamate.common.auth.infrastructure.exception.AuthErrorCode;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 认证授权控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
private final AuthApplicationService authApplicationService;
|
||||
|
||||
@PostMapping("/login")
|
||||
public AuthLoginResponse login(@RequestBody @Valid LoginRequest loginRequest) {
|
||||
return authApplicationService.login(loginRequest.username(), loginRequest.password());
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public AuthCurrentUserResponse me(HttpServletRequest request) {
|
||||
return authApplicationService.getCurrentUser(extractBearerToken(request.getHeader("Authorization")));
|
||||
}
|
||||
|
||||
@PostMapping("/refresh")
|
||||
public AuthLoginResponse refresh(@RequestHeader("Authorization") String authorization) {
|
||||
return authApplicationService.refreshToken(extractBearerToken(authorization));
|
||||
}
|
||||
|
||||
@GetMapping("/users")
|
||||
public List<AuthUserWithRolesResponse> listUsers() {
|
||||
return authApplicationService.listUsersWithRoles();
|
||||
}
|
||||
|
||||
@PutMapping("/users/{userId}/roles")
|
||||
public void assignRoles(@PathVariable("userId") Long userId,
|
||||
@RequestBody @Valid AssignUserRolesRequest request) {
|
||||
authApplicationService.assignUserRoles(userId, request.roleIds());
|
||||
}
|
||||
|
||||
@GetMapping("/roles")
|
||||
public List<AuthRoleInfo> listRoles() {
|
||||
return authApplicationService.listRoles();
|
||||
}
|
||||
|
||||
@GetMapping("/permissions")
|
||||
public List<AuthPermissionInfo> listPermissions() {
|
||||
return authApplicationService.listPermissions();
|
||||
}
|
||||
|
||||
private String extractBearerToken(String authorizationHeader) {
|
||||
BusinessAssert.isTrue(
|
||||
authorizationHeader != null && authorizationHeader.startsWith("Bearer "),
|
||||
AuthErrorCode.TOKEN_INVALID
|
||||
);
|
||||
String token = authorizationHeader.substring("Bearer ".length()).trim();
|
||||
BusinessAssert.isTrue(!token.isEmpty(), AuthErrorCode.TOKEN_INVALID);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.datamate.common.auth.interfaces.rest.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户角色分配请求
|
||||
*/
|
||||
public record AssignUserRolesRequest(
|
||||
@NotEmpty(message = "角色列表不能为空") List<String> roleIds
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.datamate.common.auth.interfaces.rest.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 当前用户信息响应
|
||||
*/
|
||||
public record AuthCurrentUserResponse(
|
||||
AuthUserView user,
|
||||
List<String> roles,
|
||||
List<String> permissions
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.datamate.common.auth.interfaces.rest.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
*/
|
||||
public record AuthLoginResponse(
|
||||
String token,
|
||||
String tokenType,
|
||||
long expiresInSeconds,
|
||||
AuthUserView user,
|
||||
List<String> roles,
|
||||
List<String> permissions
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.datamate.common.auth.interfaces.rest.dto;
|
||||
|
||||
/**
|
||||
* 登录用户信息
|
||||
*/
|
||||
public record AuthUserView(
|
||||
Long id,
|
||||
String username,
|
||||
String fullName,
|
||||
String email,
|
||||
String avatarUrl,
|
||||
String organization
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.datamate.common.auth.interfaces.rest.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户与角色响应
|
||||
*/
|
||||
public record AuthUserWithRolesResponse(
|
||||
Long id,
|
||||
String username,
|
||||
String fullName,
|
||||
String email,
|
||||
Boolean enabled,
|
||||
List<String> roleCodes
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.datamate.common.auth.interfaces.rest.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 登录请求
|
||||
*/
|
||||
public record LoginRequest(
|
||||
@NotBlank(message = "用户名不能为空") String username,
|
||||
@NotBlank(message = "密码不能为空") String password
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.datamate.common.infrastructure.config;
|
||||
import com.datamate.common.infrastructure.common.Response;
|
||||
import com.datamate.common.infrastructure.exception.BusinessException;
|
||||
import com.datamate.common.infrastructure.exception.SystemErrorCode;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.BindException;
|
||||
@@ -28,7 +29,8 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ResponseEntity<Response<?>> handleBusinessException(BusinessException e) {
|
||||
log.warn("BusinessException: code={}, message={}", e.getCode(), e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError().body(Response.error(e.getErrorCodeEnum()));
|
||||
HttpStatus status = resolveBusinessStatus(e.getCode());
|
||||
return ResponseEntity.status(status).body(Response.error(e.getErrorCodeEnum()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,4 +53,17 @@ public class GlobalExceptionHandler {
|
||||
log.error("SystemException: ", e);
|
||||
return ResponseEntity.internalServerError().body(Response.error(SystemErrorCode.SYSTEM_BUSY));
|
||||
}
|
||||
|
||||
private HttpStatus resolveBusinessStatus(String code) {
|
||||
if (code == null) {
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
if (!code.startsWith("auth.")) {
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
if ("auth.0006".equals(code)) {
|
||||
return HttpStatus.FORBIDDEN;
|
||||
}
|
||||
return HttpStatus.UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.datamate.common.auth.infrastructure.persistence.mapper.AuthMapper">
|
||||
|
||||
<select id="findUserByUsername" resultType="com.datamate.common.auth.domain.model.AuthUserAccount">
|
||||
SELECT id,
|
||||
username,
|
||||
email,
|
||||
password_hash AS passwordHash,
|
||||
full_name AS fullName,
|
||||
avatar_url AS avatarUrl,
|
||||
organization,
|
||||
enabled,
|
||||
last_login_at AS lastLoginAt
|
||||
FROM users
|
||||
WHERE username = #{username}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="findUserById" resultType="com.datamate.common.auth.domain.model.AuthUserAccount">
|
||||
SELECT id,
|
||||
username,
|
||||
email,
|
||||
password_hash AS passwordHash,
|
||||
full_name AS fullName,
|
||||
avatar_url AS avatarUrl,
|
||||
organization,
|
||||
enabled,
|
||||
last_login_at AS lastLoginAt
|
||||
FROM users
|
||||
WHERE id = #{userId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<update id="updateLastLoginAt">
|
||||
UPDATE users
|
||||
SET last_login_at = NOW()
|
||||
WHERE id = #{userId}
|
||||
</update>
|
||||
|
||||
<select id="findRolesByUserId" resultType="com.datamate.common.auth.domain.model.AuthRoleInfo">
|
||||
SELECT r.id,
|
||||
r.role_code AS roleCode,
|
||||
r.role_name AS roleName,
|
||||
r.description,
|
||||
r.enabled
|
||||
FROM t_auth_roles r
|
||||
INNER JOIN t_auth_user_roles ur ON ur.role_id = r.id
|
||||
WHERE ur.user_id = #{userId}
|
||||
ORDER BY r.role_code
|
||||
</select>
|
||||
|
||||
<select id="findPermissionCodesByUserId" resultType="string">
|
||||
SELECT DISTINCT p.permission_code
|
||||
FROM t_auth_permissions p
|
||||
INNER JOIN t_auth_role_permissions rp ON rp.permission_id = p.id
|
||||
INNER JOIN t_auth_user_roles ur ON ur.role_id = rp.role_id
|
||||
WHERE ur.user_id = #{userId}
|
||||
AND p.enabled = 1
|
||||
ORDER BY p.permission_code
|
||||
</select>
|
||||
|
||||
<select id="listUsers" resultType="com.datamate.common.auth.domain.model.AuthUserSummary">
|
||||
SELECT id,
|
||||
username,
|
||||
email,
|
||||
full_name AS fullName,
|
||||
enabled
|
||||
FROM users
|
||||
ORDER BY id ASC
|
||||
</select>
|
||||
|
||||
<select id="listRoles" resultType="com.datamate.common.auth.domain.model.AuthRoleInfo">
|
||||
SELECT id,
|
||||
role_code AS roleCode,
|
||||
role_name AS roleName,
|
||||
description,
|
||||
enabled
|
||||
FROM t_auth_roles
|
||||
ORDER BY role_code ASC
|
||||
</select>
|
||||
|
||||
<select id="listPermissions" resultType="com.datamate.common.auth.domain.model.AuthPermissionInfo">
|
||||
SELECT id,
|
||||
permission_code AS permissionCode,
|
||||
permission_name AS permissionName,
|
||||
module,
|
||||
action,
|
||||
path_pattern AS pathPattern,
|
||||
method,
|
||||
enabled
|
||||
FROM t_auth_permissions
|
||||
ORDER BY module ASC, action ASC
|
||||
</select>
|
||||
|
||||
<select id="countRolesByIds" resultType="int">
|
||||
SELECT COUNT(1)
|
||||
FROM t_auth_roles
|
||||
WHERE id IN
|
||||
<foreach collection="roleIds" item="roleId" open="(" separator="," close=")">
|
||||
#{roleId}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<delete id="deleteUserRoles">
|
||||
DELETE
|
||||
FROM t_auth_user_roles
|
||||
WHERE user_id = #{userId}
|
||||
</delete>
|
||||
|
||||
<insert id="insertUserRoles">
|
||||
INSERT INTO t_auth_user_roles (user_id, role_id)
|
||||
VALUES
|
||||
<foreach collection="roleIds" item="roleId" separator=",">
|
||||
(#{userId}, #{roleId})
|
||||
</foreach>
|
||||
</insert>
|
||||
</mapper>
|
||||
|
||||
@@ -3,9 +3,13 @@ package com.datamate.common.security;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -15,15 +19,23 @@ import java.util.Map;
|
||||
*/
|
||||
@Component
|
||||
public class JwtUtils {
|
||||
private static final String DEFAULT_SECRET = "datamate-secret-key-for-jwt-token-generation";
|
||||
|
||||
@Value("${jwt.secret:datamate-secret-key-for-jwt-token-generation}")
|
||||
@Value("${jwt.secret:" + DEFAULT_SECRET + "}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration:86400}") // 24小时
|
||||
private Long expiration;
|
||||
|
||||
private SecretKey getSigningKey() {
|
||||
return Keys.hmacShaKeyFor(secret.getBytes());
|
||||
String secretValue = StringUtils.hasText(secret) ? secret : DEFAULT_SECRET;
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
byte[] keyBytes = digest.digest(secretValue.getBytes(StandardCharsets.UTF_8));
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Cannot initialize JWT signing key", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +96,18 @@ public class JwtUtils {
|
||||
public Boolean validateToken(String token, String username) {
|
||||
try {
|
||||
String tokenUsername = getUsernameFromToken(token);
|
||||
return (username.equals(tokenUsername) && !isTokenExpired(token));
|
||||
return (username.equals(tokenUsername) && validateToken(token));
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅校验令牌格式与有效期
|
||||
*/
|
||||
public Boolean validateToken(String token) {
|
||||
try {
|
||||
return !isTokenExpired(token);
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
75
frontend/src/auth/permissions.ts
Normal file
75
frontend/src/auth/permissions.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
export const PermissionCodes = {
|
||||
dataManagementRead: "module:data-management:read",
|
||||
dataManagementWrite: "module:data-management:write",
|
||||
dataAnnotationRead: "module:data-annotation:read",
|
||||
dataAnnotationWrite: "module:data-annotation:write",
|
||||
dataCollectionRead: "module:data-collection:read",
|
||||
dataCollectionWrite: "module:data-collection:write",
|
||||
dataEvaluationRead: "module:data-evaluation:read",
|
||||
dataEvaluationWrite: "module:data-evaluation:write",
|
||||
dataSynthesisRead: "module:data-synthesis:read",
|
||||
dataSynthesisWrite: "module:data-synthesis:write",
|
||||
knowledgeManagementRead: "module:knowledge-management:read",
|
||||
knowledgeManagementWrite: "module:knowledge-management:write",
|
||||
knowledgeBaseRead: "module:knowledge-base:read",
|
||||
knowledgeBaseWrite: "module:knowledge-base:write",
|
||||
operatorMarketRead: "module:operator-market:read",
|
||||
operatorMarketWrite: "module:operator-market:write",
|
||||
orchestrationRead: "module:orchestration:read",
|
||||
orchestrationWrite: "module:orchestration:write",
|
||||
contentGenerationUse: "module:content-generation:use",
|
||||
agentUse: "module:agent:use",
|
||||
userManage: "system:user:manage",
|
||||
roleManage: "system:role:manage",
|
||||
permissionManage: "system:permission:manage",
|
||||
} as const;
|
||||
|
||||
const routePermissionRules: Array<{ prefix: string; permission: string }> = [
|
||||
{ prefix: "/data/management", permission: PermissionCodes.dataManagementRead },
|
||||
{ prefix: "/data/annotation", permission: PermissionCodes.dataAnnotationRead },
|
||||
{ prefix: "/data/collection", permission: PermissionCodes.dataCollectionRead },
|
||||
{ prefix: "/data/evaluation", permission: PermissionCodes.dataEvaluationRead },
|
||||
{ prefix: "/data/synthesis", permission: PermissionCodes.dataSynthesisRead },
|
||||
{ prefix: "/data/knowledge-management", permission: PermissionCodes.knowledgeManagementRead },
|
||||
{ prefix: "/data/knowledge-base", permission: PermissionCodes.knowledgeBaseRead },
|
||||
{ prefix: "/data/operator-market", permission: PermissionCodes.operatorMarketRead },
|
||||
{ prefix: "/data/orchestration", permission: PermissionCodes.orchestrationRead },
|
||||
{ prefix: "/data/content-generation", permission: PermissionCodes.contentGenerationUse },
|
||||
{ prefix: "/chat", permission: PermissionCodes.agentUse },
|
||||
];
|
||||
|
||||
const defaultRouteCandidates: Array<{ path: string; permission: string }> = [
|
||||
{ path: "/data/management", permission: PermissionCodes.dataManagementRead },
|
||||
{ path: "/data/annotation", permission: PermissionCodes.dataAnnotationRead },
|
||||
{ path: "/data/knowledge-management", permission: PermissionCodes.knowledgeManagementRead },
|
||||
{ path: "/data/knowledge-base", permission: PermissionCodes.knowledgeBaseRead },
|
||||
{ path: "/chat", permission: PermissionCodes.agentUse },
|
||||
];
|
||||
|
||||
export function hasPermission(
|
||||
userPermissions: string[] | undefined,
|
||||
requiredPermission?: string | null
|
||||
): boolean {
|
||||
if (!requiredPermission) {
|
||||
return true;
|
||||
}
|
||||
return (userPermissions ?? []).includes(requiredPermission);
|
||||
}
|
||||
|
||||
export function resolveRequiredPermissionByPath(pathname: string): string | null {
|
||||
if (pathname === "/403") {
|
||||
return null;
|
||||
}
|
||||
const matchedRule = routePermissionRules.find((rule) =>
|
||||
pathname.startsWith(rule.prefix)
|
||||
);
|
||||
return matchedRule?.permission ?? null;
|
||||
}
|
||||
|
||||
export function resolveDefaultAuthorizedPath(userPermissions: string[]): string {
|
||||
const matchedPath = defaultRouteCandidates.find((candidate) =>
|
||||
hasPermission(userPermissions, candidate.permission)
|
||||
)?.path;
|
||||
return matchedPath ?? "/403";
|
||||
}
|
||||
|
||||
@@ -1,20 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Navigate, useLocation, Outlet } from 'react-router';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { fetchCurrentUser, markInitialized } from '@/store/slices/authSlice';
|
||||
import {
|
||||
hasPermission,
|
||||
resolveDefaultAuthorizedPath,
|
||||
resolveRequiredPermissionByPath,
|
||||
} from '@/auth/permissions';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
||||
const { isAuthenticated } = useAppSelector((state) => state.auth);
|
||||
const dispatch = useAppDispatch();
|
||||
const { isAuthenticated, token, initialized, loading, permissions } = useAppSelector(
|
||||
(state) => state.auth
|
||||
);
|
||||
const location = useLocation();
|
||||
const requiredPermission = resolveRequiredPermissionByPath(location.pathname);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (initialized || loading) {
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
dispatch(markInitialized());
|
||||
return;
|
||||
}
|
||||
void dispatch(fetchCurrentUser());
|
||||
}, [dispatch, initialized, loading, token]);
|
||||
|
||||
if (!initialized || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
// Redirect to the login page, but save the current location they were trying to go to
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
if (!hasPermission(permissions, requiredPermission)) {
|
||||
const fallbackPath = resolveDefaultAuthorizedPath(permissions);
|
||||
if (location.pathname === fallbackPath) {
|
||||
return <Navigate to="/403" replace />;
|
||||
}
|
||||
return <Navigate to={fallbackPath} replace />;
|
||||
}
|
||||
|
||||
return children ? <>{children}</> : <Outlet />;
|
||||
};
|
||||
|
||||
|
||||
24
frontend/src/pages/Forbidden/ForbiddenPage.tsx
Normal file
24
frontend/src/pages/Forbidden/ForbiddenPage.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { Button, Result } from "antd";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
const ForbiddenPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="h-screen w-full flex items-center justify-center bg-[#050b14]">
|
||||
<Result
|
||||
status="403"
|
||||
title="403"
|
||||
subTitle="你当前账号没有访问该页面的权限。"
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate("/data/management")}>
|
||||
返回首页
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForbiddenPage;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Drawer, Menu, Popover } from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
@@ -14,6 +14,7 @@ import SettingsPage from "../SettingsPage/SettingsPage";
|
||||
import { useAppSelector, useAppDispatch } from "@/store/hooks";
|
||||
import { showSettings, hideSettings } from "@/store/slices/settingsSlice";
|
||||
import { logout } from "@/store/slices/authSlice";
|
||||
import { hasPermission } from "@/auth/permissions";
|
||||
|
||||
const isPathMatch = (currentPath: string, targetPath: string) =>
|
||||
currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
|
||||
@@ -25,13 +26,36 @@ const AsiderAndHeaderLayout = () => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [taskCenterVisible, setTaskCenterVisible] = useState(false);
|
||||
const settingVisible = useAppSelector((state) => state.settings.visible);
|
||||
const permissions = useAppSelector((state) => state.auth.permissions);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const visibleMenuItems = useMemo(
|
||||
() =>
|
||||
menuItems
|
||||
.map((item) => ({
|
||||
...item,
|
||||
children: item.children?.filter((subItem) =>
|
||||
hasPermission(permissions, (subItem as { permissionCode?: string }).permissionCode)
|
||||
),
|
||||
}))
|
||||
.filter((item) => {
|
||||
const selfVisible = hasPermission(
|
||||
permissions,
|
||||
(item as { permissionCode?: string }).permissionCode
|
||||
);
|
||||
if (item.children && item.children.length > 0) {
|
||||
return selfVisible;
|
||||
}
|
||||
return selfVisible;
|
||||
}),
|
||||
[permissions]
|
||||
);
|
||||
|
||||
// Initialize active item based on current pathname
|
||||
const initActiveItem = useCallback(() => {
|
||||
const dataPath = pathname.startsWith("/data/") ? pathname.slice(6) : pathname;
|
||||
for (let index = 0; index < menuItems.length; index++) {
|
||||
const element = menuItems[index];
|
||||
for (let index = 0; index < visibleMenuItems.length; index++) {
|
||||
const element = visibleMenuItems[index];
|
||||
if (element.children) {
|
||||
for (const subItem of element.children) {
|
||||
if (isPathMatch(dataPath, subItem.id)) {
|
||||
@@ -44,7 +68,8 @@ const AsiderAndHeaderLayout = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
setActiveItem(visibleMenuItems[0]?.id ?? "");
|
||||
}, [pathname, visibleMenuItems]);
|
||||
|
||||
useEffect(() => {
|
||||
initActiveItem();
|
||||
@@ -100,7 +125,7 @@ const AsiderAndHeaderLayout = () => {
|
||||
<Menu
|
||||
mode="inline"
|
||||
inlineCollapsed={!sidebarOpen}
|
||||
items={menuItems.map((item) => ({
|
||||
items={visibleMenuItems.map((item) => ({
|
||||
key: item.id,
|
||||
label: item.title,
|
||||
icon: item.icon ? <item.icon className="w-4 h-4" /> : null,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
// Store,
|
||||
// Merge,
|
||||
} from "lucide-react";
|
||||
import { PermissionCodes } from "@/auth/permissions";
|
||||
|
||||
export const menuItems = [
|
||||
// {
|
||||
@@ -26,6 +27,7 @@ export const menuItems = [
|
||||
id: "management",
|
||||
title: "数集管理",
|
||||
icon: FolderOpen,
|
||||
permissionCode: PermissionCodes.dataManagementRead,
|
||||
description: "创建、导入和管理数据集",
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
@@ -33,6 +35,7 @@ export const menuItems = [
|
||||
id: "annotation",
|
||||
title: "数据标注",
|
||||
icon: Tag,
|
||||
permissionCode: PermissionCodes.dataAnnotationRead,
|
||||
description: "对数据进行标注和标记",
|
||||
color: "bg-green-500",
|
||||
},
|
||||
@@ -40,6 +43,7 @@ export const menuItems = [
|
||||
id: "content-generation",
|
||||
title: "内容生成",
|
||||
icon: Sparkles,
|
||||
permissionCode: PermissionCodes.contentGenerationUse,
|
||||
description: "智能内容生成与创作",
|
||||
color: "bg-purple-500",
|
||||
},
|
||||
@@ -47,6 +51,7 @@ export const menuItems = [
|
||||
id: "knowledge-management",
|
||||
title: "知识管理",
|
||||
icon: Shield,
|
||||
permissionCode: PermissionCodes.knowledgeManagementRead,
|
||||
description: "管理知识集与知识条目",
|
||||
color: "bg-indigo-500",
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import { Form, Input, Button, Typography, message, Card } from 'antd';
|
||||
import { Form, Input, Button, Typography, message } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { loginLocal } from '@/store/slices/authSlice';
|
||||
import { loginUser } from '@/store/slices/authSlice';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -11,19 +11,20 @@ const LoginPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { loading, error } = useAppSelector((state) => state.auth);
|
||||
const { loading } = useAppSelector((state) => state.auth);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const from = location.state?.from?.pathname || '/data';
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
dispatch(loginLocal(values));
|
||||
// The reducer updates state synchronously.
|
||||
if (values.username === 'admin' && values.password === '123456') {
|
||||
messageApi.success('登录成功');
|
||||
navigate(from, { replace: true });
|
||||
} else {
|
||||
messageApi.error('账号或密码错误');
|
||||
const onFinish = async (values: { username: string; password: string }) => {
|
||||
try {
|
||||
await dispatch(loginUser(values)).unwrap();
|
||||
messageApi.success('登录成功');
|
||||
navigate(from, { replace: true });
|
||||
} catch (loginError) {
|
||||
const messageText =
|
||||
typeof loginError === 'string' ? loginError : '账号或密码错误';
|
||||
messageApi.error(messageText);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,9 +60,9 @@ const LoginPage: React.FC = () => {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
<Form<{ username: string; password: string }>
|
||||
name="login"
|
||||
initialValues={{ remember: true, username: 'admin', password: '123456' }}
|
||||
initialValues={{ username: 'admin', password: '123456' }}
|
||||
onFinish={onFinish}
|
||||
layout="vertical"
|
||||
size="large"
|
||||
|
||||
@@ -51,6 +51,7 @@ import Home from "@/pages/Home/Home";
|
||||
import ContentGenerationPage from "@/pages/ContentGeneration/ContentGenerationPage";
|
||||
import LoginPage from "@/pages/Login/LoginPage";
|
||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||
import ForbiddenPage from "@/pages/Forbidden/ForbiddenPage";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -64,6 +65,10 @@ const router = createBrowserRouter([
|
||||
{
|
||||
Component: ProtectedRoute,
|
||||
children: [
|
||||
{
|
||||
path: "/403",
|
||||
Component: ForbiddenPage,
|
||||
},
|
||||
{
|
||||
path: "/chat",
|
||||
Component: withErrorBoundary(AgentPage),
|
||||
@@ -299,4 +304,4 @@ const router = createBrowserRouter([
|
||||
}
|
||||
]);
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@@ -1,66 +1,124 @@
|
||||
// store/slices/authSlice.js
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { get, post } from "@/utils/request";
|
||||
|
||||
// 异步 thunk
|
||||
export const loginUser = createAsyncThunk(
|
||||
'auth/login',
|
||||
async (credentials, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
interface AuthUserView {
|
||||
id: number;
|
||||
username: string;
|
||||
fullName?: string;
|
||||
email?: string;
|
||||
avatarUrl?: string;
|
||||
organization?: string;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
interface AuthLoginPayload {
|
||||
token: string;
|
||||
tokenType: string;
|
||||
expiresInSeconds: number;
|
||||
user: AuthUserView;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
interface AuthCurrentUserPayload {
|
||||
user: AuthUserView;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: AuthUserView | null;
|
||||
token: string | null;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
isAuthenticated: boolean;
|
||||
initialized: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface LoginCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const extractErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof Error) {
|
||||
const nestedMessage = (error as { data?: { message?: string } }).data?.message;
|
||||
return nestedMessage ?? error.message;
|
||||
}
|
||||
);
|
||||
return "登录失败,请稍后重试";
|
||||
};
|
||||
|
||||
export const loginUser = createAsyncThunk<
|
||||
AuthLoginPayload,
|
||||
LoginCredentials,
|
||||
{ rejectValue: string }
|
||||
>("auth/login", async (credentials, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = (await post("/api/auth/login", credentials)) as ApiResponse<AuthLoginPayload>;
|
||||
if (!response?.data?.token) {
|
||||
return rejectWithValue(response?.message ?? "登录失败");
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(extractErrorMessage(error));
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchCurrentUser = createAsyncThunk<
|
||||
AuthCurrentUserPayload,
|
||||
void,
|
||||
{ rejectValue: string }
|
||||
>("auth/fetchCurrentUser", async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = (await get("/api/auth/me")) as ApiResponse<AuthCurrentUserPayload>;
|
||||
if (!response?.data?.user) {
|
||||
return rejectWithValue(response?.message ?? "用户信息加载失败");
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(extractErrorMessage(error));
|
||||
}
|
||||
});
|
||||
|
||||
const initialToken = localStorage.getItem("token");
|
||||
|
||||
const initialState: AuthState = {
|
||||
user: null,
|
||||
token: initialToken,
|
||||
roles: [],
|
||||
permissions: [],
|
||||
isAuthenticated: Boolean(initialToken),
|
||||
initialized: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState: {
|
||||
user: null,
|
||||
token: localStorage.getItem('token'),
|
||||
isAuthenticated: !!localStorage.getItem('token'),
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
logout: (state) => {
|
||||
state.user = null;
|
||||
state.token = null;
|
||||
state.roles = [];
|
||||
state.permissions = [];
|
||||
state.isAuthenticated = false;
|
||||
localStorage.removeItem('token');
|
||||
state.error = null;
|
||||
state.initialized = true;
|
||||
localStorage.removeItem("token");
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
setToken: (state, action) => {
|
||||
state.token = action.payload;
|
||||
localStorage.setItem('token', action.payload);
|
||||
},
|
||||
loginLocal: (state, action) => {
|
||||
const { username, password } = action.payload;
|
||||
if (username === 'admin' && password === '123456') {
|
||||
state.user = { username: 'admin', role: 'admin' };
|
||||
state.token = 'mock-token-' + Date.now();
|
||||
state.isAuthenticated = true;
|
||||
localStorage.setItem('token', state.token);
|
||||
state.error = null;
|
||||
} else {
|
||||
state.error = 'Invalid credentials';
|
||||
state.isAuthenticated = false;
|
||||
}
|
||||
markInitialized: (state) => {
|
||||
state.initialized = true;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
@@ -71,18 +129,52 @@ const authSlice = createSlice({
|
||||
})
|
||||
.addCase(loginUser.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.initialized = true;
|
||||
state.user = action.payload.user;
|
||||
state.token = action.payload.token;
|
||||
state.roles = action.payload.roles ?? [];
|
||||
state.permissions = action.payload.permissions ?? [];
|
||||
state.isAuthenticated = true;
|
||||
localStorage.setItem('token', action.payload.token);
|
||||
state.error = null;
|
||||
localStorage.setItem("token", action.payload.token);
|
||||
})
|
||||
.addCase(loginUser.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload;
|
||||
state.initialized = true;
|
||||
state.user = null;
|
||||
state.roles = [];
|
||||
state.permissions = [];
|
||||
state.isAuthenticated = false;
|
||||
state.token = null;
|
||||
state.error = action.payload ?? "登录失败";
|
||||
localStorage.removeItem("token");
|
||||
})
|
||||
.addCase(fetchCurrentUser.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchCurrentUser.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.initialized = true;
|
||||
state.user = action.payload.user;
|
||||
state.roles = action.payload.roles ?? [];
|
||||
state.permissions = action.payload.permissions ?? [];
|
||||
state.isAuthenticated = Boolean(state.token);
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchCurrentUser.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.initialized = true;
|
||||
state.user = null;
|
||||
state.roles = [];
|
||||
state.permissions = [];
|
||||
state.isAuthenticated = false;
|
||||
state.token = null;
|
||||
state.error = action.payload ?? "登录状态已失效";
|
||||
localStorage.removeItem("token");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { logout, clearError, setToken, loginLocal } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
export const { logout, clearError, markInitialized } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
|
||||
@@ -524,8 +524,16 @@ request.addRequestInterceptor((config) => {
|
||||
|
||||
// 添加默认响应拦截器 - 错误处理
|
||||
request.addResponseInterceptor((response) => {
|
||||
// 可以在这里添加全局错误处理逻辑
|
||||
// 比如token过期自动跳转登录页等
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem("token");
|
||||
sessionStorage.removeItem("token");
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
if (response.status === 403 && window.location.pathname !== "/403") {
|
||||
window.location.href = "/403";
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
|
||||
149
scripts/db/zz-auth-init.sql
Normal file
149
scripts/db/zz-auth-init.sql
Normal file
@@ -0,0 +1,149 @@
|
||||
USE datamate;
|
||||
|
||||
-- =============================================
|
||||
-- 认证与授权(RBAC)基础表
|
||||
-- 注意:该脚本命名为 zz- 前缀,确保在 users 表初始化后执行
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS t_auth_roles
|
||||
(
|
||||
id VARCHAR(36) PRIMARY KEY COMMENT '角色ID',
|
||||
role_code VARCHAR(100) NOT NULL COMMENT '角色编码',
|
||||
role_name VARCHAR(100) NOT NULL COMMENT '角色名称',
|
||||
description VARCHAR(255) DEFAULT '' COMMENT '角色描述',
|
||||
enabled TINYINT DEFAULT 1 COMMENT '是否启用:1-启用,0-禁用',
|
||||
is_built_in TINYINT DEFAULT 1 COMMENT '是否内置:1-是,0-否',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
UNIQUE KEY uk_auth_role_code (role_code)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='角色表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS t_auth_permissions
|
||||
(
|
||||
id VARCHAR(36) PRIMARY KEY COMMENT '权限ID',
|
||||
permission_code VARCHAR(120) NOT NULL COMMENT '权限编码',
|
||||
permission_name VARCHAR(120) NOT NULL COMMENT '权限名称',
|
||||
module VARCHAR(100) NOT NULL COMMENT '模块',
|
||||
action VARCHAR(50) NOT NULL COMMENT '动作',
|
||||
path_pattern VARCHAR(255) DEFAULT '' COMMENT '路径模式',
|
||||
method VARCHAR(20) DEFAULT '' COMMENT 'HTTP方法',
|
||||
enabled TINYINT DEFAULT 1 COMMENT '是否启用:1-启用,0-禁用',
|
||||
is_built_in TINYINT DEFAULT 1 COMMENT '是否内置:1-是,0-否',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
UNIQUE KEY uk_auth_permission_code (permission_code),
|
||||
INDEX idx_auth_permission_module_action (module, action)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='权限表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS t_auth_role_permissions
|
||||
(
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||
role_id VARCHAR(36) NOT NULL COMMENT '角色ID',
|
||||
permission_id VARCHAR(36) NOT NULL COMMENT '权限ID',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
UNIQUE KEY uk_auth_role_permission (role_id, permission_id),
|
||||
INDEX idx_auth_role_permission_role (role_id),
|
||||
INDEX idx_auth_role_permission_permission (permission_id),
|
||||
CONSTRAINT fk_auth_rp_role FOREIGN KEY (role_id) REFERENCES t_auth_roles (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_auth_rp_permission FOREIGN KEY (permission_id) REFERENCES t_auth_permissions (id) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='角色权限关系表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS t_auth_user_roles
|
||||
(
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID(users.id)',
|
||||
role_id VARCHAR(36) NOT NULL COMMENT '角色ID',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
UNIQUE KEY uk_auth_user_role (user_id, role_id),
|
||||
INDEX idx_auth_user_role_user (user_id),
|
||||
INDEX idx_auth_user_role_role (role_id),
|
||||
CONSTRAINT fk_auth_ur_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_auth_ur_role FOREIGN KEY (role_id) REFERENCES t_auth_roles (id) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='用户角色关系表';
|
||||
|
||||
-- =============================================
|
||||
-- 角色初始化
|
||||
-- =============================================
|
||||
INSERT IGNORE INTO t_auth_roles (id, role_code, role_name, description, enabled, is_built_in)
|
||||
VALUES ('role-admin', 'ROLE_ADMIN', '系统管理员', '拥有平台全部权限', 1, 1),
|
||||
('role-data-editor', 'ROLE_DATA_EDITOR', '数据运营', '拥有业务模块读写权限', 1, 1),
|
||||
('role-knowledge-user', 'ROLE_KNOWLEDGE_USER', '知识用户', '以知识管理为主的业务权限', 1, 1);
|
||||
|
||||
-- =============================================
|
||||
-- 权限初始化(接口级)
|
||||
-- =============================================
|
||||
INSERT IGNORE INTO t_auth_permissions (id, permission_code, permission_name, module, action, path_pattern, method, enabled, is_built_in)
|
||||
VALUES ('perm-dm-read', 'module:data-management:read', '数据管理读取', 'data-management', 'read', '/api/data-management/**', 'GET', 1, 1),
|
||||
('perm-dm-write', 'module:data-management:write', '数据管理写入', 'data-management', 'write', '/api/data-management/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-da-read', 'module:data-annotation:read', '数据标注读取', 'data-annotation', 'read', '/api/annotation/**', 'GET', 1, 1),
|
||||
('perm-da-write', 'module:data-annotation:write', '数据标注写入', 'data-annotation', 'write', '/api/annotation/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-dc-read', 'module:data-collection:read', '数据归集读取', 'data-collection', 'read', '/api/data-collection/**', 'GET', 1, 1),
|
||||
('perm-dc-write', 'module:data-collection:write', '数据归集写入', 'data-collection', 'write', '/api/data-collection/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-de-read', 'module:data-evaluation:read', '数据评估读取', 'data-evaluation', 'read', '/api/evaluation/**', 'GET', 1, 1),
|
||||
('perm-de-write', 'module:data-evaluation:write', '数据评估写入', 'data-evaluation', 'write', '/api/evaluation/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-ds-read', 'module:data-synthesis:read', '数据合成读取', 'data-synthesis', 'read', '/api/synthesis/**', 'GET', 1, 1),
|
||||
('perm-ds-write', 'module:data-synthesis:write', '数据合成写入', 'data-synthesis', 'write', '/api/synthesis/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-km-read', 'module:knowledge-management:read', '知识管理读取', 'knowledge-management', 'read', '/api/data-management/knowledge/**', 'GET', 1, 1),
|
||||
('perm-km-write', 'module:knowledge-management:write', '知识管理写入', 'knowledge-management', 'write', '/api/data-management/knowledge/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-kb-read', 'module:knowledge-base:read', '知识库读取', 'knowledge-base', 'read', '/api/knowledge-base/**', 'GET', 1, 1),
|
||||
('perm-kb-write', 'module:knowledge-base:write', '知识库写入', 'knowledge-base', 'write', '/api/knowledge-base/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-om-read', 'module:operator-market:read', '算子市场读取', 'operator-market', 'read', '/api/operator-market/**', 'GET', 1, 1),
|
||||
('perm-om-write', 'module:operator-market:write', '算子市场写入', 'operator-market', 'write', '/api/operator-market/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-orch-read', 'module:orchestration:read', '流程编排读取', 'orchestration', 'read', '/api/orchestration/**', 'GET', 1, 1),
|
||||
('perm-orch-write', 'module:orchestration:write', '流程编排写入', 'orchestration', 'write', '/api/orchestration/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-agent-use', 'module:agent:use', '对话助手使用', 'agent', 'use', '/chat/**', 'GET', 1, 1),
|
||||
('perm-content-use', 'module:content-generation:use', '内容生成功能使用', 'content-generation', 'use', '/api/content-generation/**', 'POST,PUT,PATCH', 1, 1),
|
||||
('perm-user-manage', 'system:user:manage', '用户管理', 'system', 'manage-user', '/api/auth/users/**', 'GET,POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-role-manage', 'system:role:manage', '角色管理', 'system', 'manage-role', '/api/auth/roles/**', 'GET,POST,PUT,PATCH,DELETE', 1, 1),
|
||||
('perm-perm-manage', 'system:permission:manage', '权限管理', 'system', 'manage-permission', '/api/auth/permissions/**', 'GET,POST,PUT,PATCH,DELETE', 1, 1);
|
||||
|
||||
-- 管理员拥有所有权限
|
||||
INSERT IGNORE INTO t_auth_role_permissions (role_id, permission_id)
|
||||
SELECT 'role-admin', p.id
|
||||
FROM t_auth_permissions p;
|
||||
|
||||
-- 数据运营拥有业务模块读写权限(不含系统管理)
|
||||
INSERT IGNORE INTO t_auth_role_permissions (role_id, permission_id)
|
||||
SELECT 'role-data-editor', p.id
|
||||
FROM t_auth_permissions p
|
||||
WHERE p.permission_code IN (
|
||||
'module:data-management:read', 'module:data-management:write',
|
||||
'module:data-annotation:read', 'module:data-annotation:write',
|
||||
'module:data-collection:read', 'module:data-collection:write',
|
||||
'module:data-evaluation:read', 'module:data-evaluation:write',
|
||||
'module:data-synthesis:read', 'module:data-synthesis:write',
|
||||
'module:knowledge-management:read', 'module:knowledge-management:write',
|
||||
'module:knowledge-base:read', 'module:knowledge-base:write',
|
||||
'module:operator-market:read', 'module:operator-market:write',
|
||||
'module:orchestration:read', 'module:orchestration:write',
|
||||
'module:agent:use', 'module:content-generation:use'
|
||||
);
|
||||
|
||||
-- 知识用户拥有知识相关权限及必要数据读取权限
|
||||
INSERT IGNORE INTO t_auth_role_permissions (role_id, permission_id)
|
||||
SELECT 'role-knowledge-user', p.id
|
||||
FROM t_auth_permissions p
|
||||
WHERE p.permission_code IN (
|
||||
'module:data-management:read',
|
||||
'module:knowledge-management:read', 'module:knowledge-management:write',
|
||||
'module:knowledge-base:read', 'module:knowledge-base:write',
|
||||
'module:agent:use'
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- 用户角色初始化(绑定到已有 users)
|
||||
-- =============================================
|
||||
INSERT IGNORE INTO t_auth_user_roles (user_id, role_id)
|
||||
SELECT u.id, 'role-admin'
|
||||
FROM users u
|
||||
WHERE u.username = 'admin';
|
||||
|
||||
INSERT IGNORE INTO t_auth_user_roles (user_id, role_id)
|
||||
SELECT u.id, 'role-knowledge-user'
|
||||
FROM users u
|
||||
WHERE u.username = 'knowledge_user';
|
||||
|
||||
Reference in New Issue
Block a user