mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
Merge branch 'feature/project-init' into feature/phase1-foundation-auth
This commit is contained in:
commit
ff5ecc90ac
10 changed files with 309 additions and 33 deletions
|
|
@ -1,22 +0,0 @@
|
|||
package com.iflytek.skillhub.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/v1/health", "/actuator/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.iflytek.skillhub.controller;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
public class AuthController {
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<Map<String, Object>> me(HttpSession session) {
|
||||
PlatformPrincipal principal = (PlatformPrincipal) session.getAttribute("platformPrincipal");
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"userId", principal.userId(),
|
||||
"displayName", principal.displayName(),
|
||||
"email", principal.email() != null ? principal.email() : "",
|
||||
"avatarUrl", principal.avatarUrl() != null ? principal.avatarUrl() : "",
|
||||
"oauthProvider", principal.oauthProvider(),
|
||||
"platformRoles", principal.platformRoles()
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/providers")
|
||||
public ResponseEntity<Map<String, Object>> providers() {
|
||||
var github = Map.of(
|
||||
"id", "github",
|
||||
"name", "GitHub",
|
||||
"authorizationUrl", "/oauth2/authorization/github"
|
||||
);
|
||||
return ResponseEntity.ok(Map.of("data", List.of(github)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.iflytek.skillhub.controller;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.auth.token.ApiTokenService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/tokens")
|
||||
public class TokenController {
|
||||
|
||||
private final ApiTokenService apiTokenService;
|
||||
|
||||
public TokenController(ApiTokenService apiTokenService) {
|
||||
this.apiTokenService = apiTokenService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> create(
|
||||
@AuthenticationPrincipal PlatformPrincipal principal,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
String name = (String) body.get("name");
|
||||
String scopeJson = body.containsKey("scopes")
|
||||
? body.get("scopes").toString() : "[\"skill:read\",\"skill:publish\"]";
|
||||
|
||||
var result = apiTokenService.createToken(principal.userId(), name, scopeJson);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"token", result.rawToken(),
|
||||
"id", result.entity().getId(),
|
||||
"name", result.entity().getName(),
|
||||
"tokenPrefix", result.entity().getTokenPrefix()
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> list(@AuthenticationPrincipal PlatformPrincipal principal) {
|
||||
var tokens = apiTokenService.listActiveTokens(principal.userId());
|
||||
var result = tokens.stream().map(t -> Map.of(
|
||||
"id", t.getId(),
|
||||
"name", t.getName(),
|
||||
"tokenPrefix", t.getTokenPrefix(),
|
||||
"createdAt", t.getCreatedAt().toString(),
|
||||
"expiresAt", t.getExpiresAt() != null ? t.getExpiresAt().toString() : "",
|
||||
"lastUsedAt", t.getLastUsedAt() != null ? t.getLastUsedAt().toString() : ""
|
||||
)).toList();
|
||||
return ResponseEntity.ok(Map.of("data", result));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> revoke(
|
||||
@AuthenticationPrincipal PlatformPrincipal principal,
|
||||
@PathVariable Long id) {
|
||||
apiTokenService.revokeToken(id, principal.userId());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,19 @@
|
|||
package com.iflytek.skillhub.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record ErrorResponse(
|
||||
int status,
|
||||
String error,
|
||||
String message
|
||||
) {}
|
||||
String message,
|
||||
String requestId,
|
||||
Instant timestamp
|
||||
) {
|
||||
public ErrorResponse(int status, String error, String message, String requestId) {
|
||||
this(status, error, message, requestId, Instant.now());
|
||||
}
|
||||
|
||||
public ErrorResponse(int status, String error, String message) {
|
||||
this(status, error, message, null, Instant.now());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,35 @@
|
|||
package com.iflytek.skillhub.exception;
|
||||
|
||||
import com.iflytek.skillhub.dto.ErrorResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<ErrorResponse> handleBadRequest(IllegalArgumentException ex,
|
||||
HttpServletRequest request) {
|
||||
String requestId = MDC.get("requestId");
|
||||
return ResponseEntity.badRequest().body(
|
||||
new ErrorResponse(400, "Bad Request", ex.getMessage(), requestId));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex, WebRequest request) {
|
||||
logger.error("Unhandled exception", ex);
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
"Internal server error",
|
||||
ex.getMessage()
|
||||
);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
|
||||
public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex,
|
||||
HttpServletRequest request) {
|
||||
String requestId = MDC.get("requestId");
|
||||
logger.error("Unhandled exception [requestId={}]", requestId, ex);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
|
||||
new ErrorResponse(500, "Internal Server Error",
|
||||
"An unexpected error occurred", requestId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ spring:
|
|||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
session:
|
||||
store-type: redis
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
github:
|
||||
client-id: ${OAUTH2_GITHUB_CLIENT_ID:local-placeholder}
|
||||
client-secret: ${OAUTH2_GITHUB_CLIENT_SECRET:local-placeholder}
|
||||
|
||||
logging:
|
||||
level:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,25 @@ spring:
|
|||
password: skillhub_dev
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
session:
|
||||
store-type: redis
|
||||
redis:
|
||||
namespace: skillhub:session
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
github:
|
||||
client-id: ${OAUTH2_GITHUB_CLIENT_ID:placeholder}
|
||||
client-secret: ${OAUTH2_GITHUB_CLIENT_SECRET:placeholder}
|
||||
scope: read:user,user:email
|
||||
provider:
|
||||
github:
|
||||
user-info-uri: https://api.github.com/user
|
||||
|
||||
skillhub:
|
||||
access-policy:
|
||||
mode: OPEN
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
|
|
|||
|
|
@ -18,3 +18,18 @@ spring:
|
|||
exclude:
|
||||
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
|
||||
- org.springframework.boot.autoconfigure.session.SessionAutoConfiguration
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
github:
|
||||
client-id: test-client-id
|
||||
client-secret: test-client-secret
|
||||
scope: read:user,user:email
|
||||
provider:
|
||||
github:
|
||||
user-info-uri: https://api.github.com/user
|
||||
|
||||
skillhub:
|
||||
access-policy:
|
||||
mode: OPEN
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.iflytek.skillhub.auth.config;
|
||||
|
||||
import com.iflytek.skillhub.auth.oauth.CustomOAuth2UserService;
|
||||
import com.iflytek.skillhub.auth.oauth.OAuth2LoginSuccessHandler;
|
||||
import com.iflytek.skillhub.auth.token.ApiTokenAuthenticationFilter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final CustomOAuth2UserService customOAuth2UserService;
|
||||
private final OAuth2LoginSuccessHandler successHandler;
|
||||
private final ApiTokenAuthenticationFilter apiTokenAuthenticationFilter;
|
||||
|
||||
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService,
|
||||
OAuth2LoginSuccessHandler successHandler,
|
||||
ApiTokenAuthenticationFilter apiTokenAuthenticationFilter) {
|
||||
this.customOAuth2UserService = customOAuth2UserService;
|
||||
this.successHandler = successHandler;
|
||||
this.apiTokenAuthenticationFilter = apiTokenAuthenticationFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
var csrfHandler = new CsrfTokenRequestAttributeHandler();
|
||||
csrfHandler.setCsrfRequestAttributeName(null);
|
||||
|
||||
http
|
||||
.csrf(csrf -> csrf
|
||||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
||||
.csrfTokenRequestHandler(csrfHandler)
|
||||
.ignoringRequestMatchers("/api/v1/cli/**", "/api/compat/**")
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/api/v1/health",
|
||||
"/api/v1/auth/providers",
|
||||
"/api/v1/skills/**",
|
||||
"/api/v1/namespaces/**",
|
||||
"/actuator/health",
|
||||
"/v3/api-docs/**",
|
||||
"/swagger-ui/**",
|
||||
"/.well-known/**"
|
||||
).permitAll()
|
||||
.requestMatchers("/api/v1/admin/**").hasAnyRole("SUPER_ADMIN", "SKILL_ADMIN", "USER_ADMIN", "AUDITOR")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2Login(oauth2 -> oauth2
|
||||
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
|
||||
.successHandler(successHandler)
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/api/v1/auth/logout")
|
||||
.logoutSuccessUrl("/")
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("SESSION")
|
||||
)
|
||||
.addFilterBefore(apiTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.iflytek.skillhub.auth.mock;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@Profile("local")
|
||||
@Order(-100)
|
||||
public class MockAuthFilter extends OncePerRequestFilter {
|
||||
|
||||
private final UserAccountRepository userRepo;
|
||||
private final UserRoleBindingRepository roleBindingRepo;
|
||||
|
||||
public MockAuthFilter(UserAccountRepository userRepo,
|
||||
UserRoleBindingRepository roleBindingRepo) {
|
||||
this.userRepo = userRepo;
|
||||
this.roleBindingRepo = roleBindingRepo;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String mockUserId = request.getHeader("X-Mock-User-Id");
|
||||
if (mockUserId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
Long userId = Long.parseLong(mockUserId);
|
||||
userRepo.findById(userId)
|
||||
.filter(UserAccount::isActive)
|
||||
.ifPresent(user -> {
|
||||
Set<String> roles = roleBindingRepo.findByUserId(userId).stream()
|
||||
.map(rb -> rb.getRole().getCode())
|
||||
.collect(Collectors.toSet());
|
||||
var principal = new PlatformPrincipal(
|
||||
user.getId(), user.getDisplayName(), user.getEmail(),
|
||||
user.getAvatarUrl(), "mock", roles
|
||||
);
|
||||
var authorities = roles.stream()
|
||||
.map(r -> new SimpleGrantedAuthority("ROLE_" + r))
|
||||
.toList();
|
||||
var auth = new UsernamePasswordAuthenticationToken(principal, null, authorities);
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
request.getSession().setAttribute("platformPrincipal", principal);
|
||||
});
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue