[api] swagger 셋팅중
- swagger login 처리
This commit is contained in:
@@ -26,6 +26,15 @@ dependencies {
|
|||||||
// Web 기본
|
// Web 기본
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
|
||||||
|
// security
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
|
|
||||||
|
// jwt
|
||||||
|
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
|
||||||
|
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
|
||||||
|
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
|
||||||
|
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||||
|
|
||||||
// Lombok
|
// Lombok
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package com.alist.api;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@ConfigurationPropertiesScan
|
||||||
public class ApiApplication {
|
public class ApiApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.alist.api.common.response;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class ApiResponse<T> {
|
||||||
|
private T data; // 실제 데이터
|
||||||
|
private String code; // 결과 코드 (SUCCESS, ERROR_001 등)
|
||||||
|
private String message; // 사용자 메시지
|
||||||
|
|
||||||
|
public ApiResponse(T data, String code, String message) {
|
||||||
|
this.data = data;
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApiResponse<Void> body(ApiResponseCode code, Object... args) {
|
||||||
|
return new ApiResponse<>(null, code.code(), code.message(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> body(T data, ApiResponseCode code, Object... args) {
|
||||||
|
return new ApiResponse<>(data, code.code(), code.message(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResponseEntity<ApiResponse<Void>> entity(ApiResponseCode code, Object... args) {
|
||||||
|
return ResponseEntity.status(code.httpStatus()).body(ApiResponse.body(code, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ResponseEntity<ApiResponse<T>> entity(T data, ApiResponseCode code, Object... args) {
|
||||||
|
return ResponseEntity.status(code.httpStatus()).body(ApiResponse.body(data, code, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.alist.api.common.response;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
|
||||||
|
public enum ApiResponseCode {
|
||||||
|
// ===== Common =====
|
||||||
|
CODE_200 ("200", "성공", HttpStatus.OK),
|
||||||
|
|
||||||
|
CODE_400 ("400", "잘못된 요청", HttpStatus.BAD_REQUEST),
|
||||||
|
CODE_401 ("401", "인증 필요 합니다.", HttpStatus.UNAUTHORIZED),
|
||||||
|
CODE_403 ("403", "접근 권한 필요 합니다.", HttpStatus.FORBIDDEN),
|
||||||
|
CODE_404 ("404", "페이지를 찾을 수 없습니다. 입력하신 주소가 올바른지 확인해주세요.", HttpStatus.NOT_FOUND),
|
||||||
|
CODE_405 ("405", "잘못된 요청입니다. 요청 방식을 확인해 주세요.", HttpStatus.METHOD_NOT_ALLOWED),
|
||||||
|
CODE_500 ("500", "요청을 처리하는 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
|
|
||||||
|
// ===== Success detail =====
|
||||||
|
CODE_2001("2001", "{0} 정보 조회에 성공하였습니다.", HttpStatus.OK),
|
||||||
|
CODE_2002("2002", "{0} 등록 되었습니다.", HttpStatus.CREATED),
|
||||||
|
CODE_2003("2003", "조회된 정보가 없습니다.", HttpStatus.OK),
|
||||||
|
CODE_2004("2004", "중복된 {0} 정보 입니다.", HttpStatus.CONFLICT),
|
||||||
|
|
||||||
|
// ===== Client input errors =====
|
||||||
|
// @Valid / 바인딩 / 타입미스매치 / JSON 파싱 실패 등은 다 여기로
|
||||||
|
CODE_4001("4001", "입력값을 확인해주세요.", HttpStatus.BAD_REQUEST),
|
||||||
|
|
||||||
|
// 필수 요청 파라미터 누락
|
||||||
|
CODE_4003("4003", "필수 요청 파라미터가 누락되었습니다.", HttpStatus.BAD_REQUEST),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String message;
|
||||||
|
private final HttpStatus httpStatus;
|
||||||
|
|
||||||
|
ApiResponseCode(String code, String message, HttpStatus httpStatus) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
this.httpStatus = httpStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String code() { return code; }
|
||||||
|
|
||||||
|
public String message() { return message; }
|
||||||
|
|
||||||
|
public String message(Object... args) {
|
||||||
|
return MessageFormat.format(this.message, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpStatus httpStatus() {
|
||||||
|
return httpStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.alist.api.common.utils;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public class ApiKeyGenerator {
|
||||||
|
|
||||||
|
private static final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
private static final Base64.Encoder base64Encoder = Base64.getUrlEncoder().withoutPadding();
|
||||||
|
|
||||||
|
public static String userApiKeyProc() {
|
||||||
|
byte[] randomBytes = new byte[32]; // 256-bit
|
||||||
|
secureRandom.nextBytes(randomBytes);
|
||||||
|
return base64Encoder.encodeToString(randomBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,11 @@ package com.alist.api.config;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||||
import io.swagger.v3.oas.annotations.info.Info;
|
import io.swagger.v3.oas.annotations.info.Info;
|
||||||
|
import io.swagger.v3.oas.models.Components;
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -13,4 +18,20 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
public class OpenApiConfig {
|
public class OpenApiConfig {
|
||||||
|
private static final String SECURITY_SCHEME_NAME = "bearerAuth";
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI openAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
|
||||||
|
.components(new Components()
|
||||||
|
.addSecuritySchemes(SECURITY_SCHEME_NAME,
|
||||||
|
new SecurityScheme()
|
||||||
|
.name("Authorization")
|
||||||
|
.type(SecurityScheme.Type.HTTP)
|
||||||
|
.scheme("bearer")
|
||||||
|
.bearerFormat("JWT")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.alist.api.config;
|
||||||
|
|
||||||
|
import com.alist.api.config.jwt.JwtAccessDeniedHandler;
|
||||||
|
import com.alist.api.config.jwt.JwtAuthenticationEntryPoint;
|
||||||
|
import com.alist.api.config.jwt.JwtAuthenticationFilter;
|
||||||
|
import com.alist.api.config.jwt.JwtTokenProvider;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
@Value("${swagger.login.id}")
|
||||||
|
private String swaggerLoginId;
|
||||||
|
|
||||||
|
@Value("${swagger.login.password}")
|
||||||
|
private String swaggerLoginPassword;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtAuthenticationFilter jwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
|
||||||
|
return new JwtAuthenticationFilter(jwtTokenProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(1)
|
||||||
|
public SecurityFilterChain swaggerFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.securityMatcher("/", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html")
|
||||||
|
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
||||||
|
.httpBasic(Customizer.withDefaults())
|
||||||
|
.csrf(csrf -> csrf.disable());
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(2)
|
||||||
|
public SecurityFilterChain apiFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.exceptionHandling(ex -> ex
|
||||||
|
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
|
||||||
|
.accessDeniedHandler(new JwtAccessDeniedHandler())
|
||||||
|
)
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/", "/actuator/health", "/auth/**", "/api/user/signup").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public UserDetailsService userDetailsService() {
|
||||||
|
UserDetails alist = User.builder()
|
||||||
|
.username(swaggerLoginId)
|
||||||
|
.password(passwordEncoder().encode(swaggerLoginPassword))
|
||||||
|
.roles("ADMIN")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new InMemoryUserDetailsManager(alist);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.alist.api.config.exception;
|
||||||
|
|
||||||
|
import com.alist.api.common.response.ApiResponse;
|
||||||
|
import com.alist.api.common.response.ApiResponseCode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.javassist.NotFoundException;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentException e) {
|
||||||
|
|
||||||
|
log.warn("IllegalArgumentException: {}", e.getMessage());
|
||||||
|
|
||||||
|
return ApiResponse.entity(ApiResponseCode.CODE_400);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleMissingParam(MissingServletRequestParameterException e) {
|
||||||
|
|
||||||
|
log.warn("Missing request parameter: {} (type={})", e.getParameterName(), e.getParameterType());
|
||||||
|
|
||||||
|
return ApiResponse.entity(ApiResponseCode.CODE_4003);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
|
||||||
|
|
||||||
|
log.warn("Method not supported: {} (supported={})", e.getMethod(), e.getSupportedHttpMethods());
|
||||||
|
|
||||||
|
return ApiResponse.entity(ApiResponseCode.CODE_405);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(NotFoundException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleNotFound(NotFoundException e) {
|
||||||
|
|
||||||
|
log.warn("Not found: {}", e.getMessage());
|
||||||
|
|
||||||
|
return ApiResponse.entity(ApiResponseCode.CODE_404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력오류
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Map<String, String>>> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
|
||||||
|
Map<String, String> fieldErrors = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (FieldError fe : e.getBindingResult().getFieldErrors()) {
|
||||||
|
fieldErrors.putIfAbsent(fe.getField(), fe.getDefaultMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("Validation failed: {}", e.getMessage());
|
||||||
|
|
||||||
|
return ApiResponse.entity(fieldErrors, ApiResponseCode.CODE_4001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleTypeMismatch(MethodArgumentTypeMismatchException e) {
|
||||||
|
|
||||||
|
log.warn("Type mismatch: name={}, value={}", e.getName(), e.getValue());
|
||||||
|
|
||||||
|
return ApiResponse.entity(ApiResponseCode.CODE_4001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleNotReadable(HttpMessageNotReadableException e) {
|
||||||
|
|
||||||
|
log.warn("Unreadable message (json parse?) : {}", e.getMessage());
|
||||||
|
|
||||||
|
return ApiResponse.entity(ApiResponseCode.CODE_4001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(BindException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Map<String, String>>> handleBindException(BindException e) {
|
||||||
|
Map<String, String> fieldErrors = new LinkedHashMap<>();
|
||||||
|
for (FieldError fe : e.getBindingResult().getFieldErrors()) {
|
||||||
|
fieldErrors.putIfAbsent(fe.getField(), fe.getDefaultMessage());
|
||||||
|
}
|
||||||
|
log.warn("Bind failed: {}", e.getMessage());
|
||||||
|
|
||||||
|
return ApiResponse.entity(fieldErrors, ApiResponseCode.CODE_4001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
|
||||||
|
|
||||||
|
log.error("Unhandled exception occurred", e);
|
||||||
|
|
||||||
|
return ApiResponse.entity(ApiResponseCode.CODE_500);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.alist.api.config.jwt;
|
||||||
|
|
||||||
|
import com.alist.api.common.response.ApiResponse;
|
||||||
|
import com.alist.api.common.response.ApiResponseCode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
AccessDeniedException e) throws IOException {
|
||||||
|
|
||||||
|
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
|
||||||
|
ApiResponse<Void> body = ApiResponse.body(ApiResponseCode.CODE_403);
|
||||||
|
objectMapper.writeValue(response.getOutputStream(), body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.alist.api.config.jwt;
|
||||||
|
|
||||||
|
import com.alist.api.common.response.ApiResponse;
|
||||||
|
import com.alist.api.common.response.ApiResponseCode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commence(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
AuthenticationException e) throws IOException {
|
||||||
|
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
|
||||||
|
ApiResponse<Void> body = ApiResponse.body(ApiResponseCode.CODE_401);
|
||||||
|
objectMapper.writeValue(response.getOutputStream(), body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.alist.api.config.jwt;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
|
||||||
|
this.jwtTokenProvider = jwtTokenProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain
|
||||||
|
) throws ServletException, IOException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
String token = resolveToken(request);
|
||||||
|
|
||||||
|
if (token != null && jwtTokenProvider.validateToken(token)) {
|
||||||
|
String userId = jwtTokenProvider.getUserId(token);
|
||||||
|
|
||||||
|
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
|
||||||
|
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
// 로깅은 여기서 해도 됨 (SLF4J)
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveToken(HttpServletRequest request) {
|
||||||
|
String bearer = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||||
|
if (bearer == null) return null;
|
||||||
|
|
||||||
|
// "Bearer " 뒤 토큰만 추출
|
||||||
|
if (bearer.startsWith("Bearer ")) {
|
||||||
|
String token = bearer.substring(7).trim();
|
||||||
|
return token.isEmpty() ? null : token;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.alist.api.config.jwt;
|
||||||
|
|
||||||
|
import com.alist.api.config.properties.JwtProperties;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtTokenProvider {
|
||||||
|
private final JwtProperties jwtProperties;
|
||||||
|
private final SecretKey secretKey;
|
||||||
|
|
||||||
|
public JwtTokenProvider(JwtProperties jwtProperties) {
|
||||||
|
this.jwtProperties = jwtProperties;
|
||||||
|
this.secretKey = Keys.hmacShaKeyFor(
|
||||||
|
jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 토큰생성 **/
|
||||||
|
public String createToken(String userId) {
|
||||||
|
Date now = new Date();
|
||||||
|
Date expiry = new Date(
|
||||||
|
now.getTime() + jwtProperties.getAccessTokenValiditySeconds() * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.setSubject(userId)
|
||||||
|
.setIssuedAt(now)
|
||||||
|
.setExpiration(expiry)
|
||||||
|
.signWith(secretKey, SignatureAlgorithm.HS256)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 엑세스 토큰 생성 */
|
||||||
|
public String createAccessToken(long userTokenIdx, String role) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Instant expiry = now.plusSeconds(jwtProperties.getAccessTokenValiditySeconds());
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.setSubject(String.valueOf(userTokenIdx))
|
||||||
|
.claim("role", role)
|
||||||
|
.setIssuedAt(Date.from(now))
|
||||||
|
.setExpiration(Date.from(expiry))
|
||||||
|
.signWith(secretKey, SignatureAlgorithm.HS256)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 리프레시 토큰 생성 */
|
||||||
|
public String createRefreshToken(long userTokenIdx) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Instant expiry = now.plusSeconds(jwtProperties.getAccessTokenValiditySeconds());
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.setSubject(String.valueOf(userTokenIdx))
|
||||||
|
.setIssuedAt(Date.from(now))
|
||||||
|
.setExpiration(Date.from(expiry))
|
||||||
|
.signWith(secretKey, SignatureAlgorithm.HS256)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 토큰에서 subject 추출 */
|
||||||
|
public String getUserId(String token) {
|
||||||
|
return parseClaims(token).getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 토큰 검증 */
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
try {
|
||||||
|
parseClaims(token);
|
||||||
|
return true;
|
||||||
|
} catch (JwtException | IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Claims parseClaims(String token) {
|
||||||
|
return Jwts.parserBuilder()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.alist.api.config.properties;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ConfigurationProperties(prefix = "jwt")
|
||||||
|
public class JwtProperties {
|
||||||
|
String secret;
|
||||||
|
long accessTokenValiditySeconds;
|
||||||
|
long refreshTokenValiditySeconds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.alist.api.modules.main;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/")
|
||||||
|
public class MainController {
|
||||||
|
@GetMapping(value = "/")
|
||||||
|
public String index() {
|
||||||
|
return "redirect:/swagger-ui/index.html";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,16 @@ spring:
|
|||||||
logging:
|
logging:
|
||||||
config: classpath:logback-local.xml
|
config: classpath:logback-local.xml
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: "F9k3s!29dkF#1lP0X9QZx8eW!2m@0AbD"
|
||||||
|
access-token-validity-seconds: 3600
|
||||||
|
refresh-token-validity-seconds: 2592000
|
||||||
|
|
||||||
|
swagger:
|
||||||
|
login:
|
||||||
|
id: alist
|
||||||
|
password: "1qaz2wsx!@"
|
||||||
|
|
||||||
springdoc:
|
springdoc:
|
||||||
api-docs:
|
api-docs:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ spring:
|
|||||||
logging:
|
logging:
|
||||||
config: classpath:logback-pjt.xml
|
config: classpath:logback-pjt.xml
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: ${JWT_SECRET}
|
||||||
|
access-token-validity-seconds: 3600
|
||||||
|
refresh-token-validity-seconds: 2592000
|
||||||
|
|
||||||
|
swagger:
|
||||||
|
login:
|
||||||
|
id: ${SWAGGER_ID}
|
||||||
|
password: ${SWAGGER_PASSWORD}
|
||||||
|
|
||||||
springdoc:
|
springdoc:
|
||||||
api-docs:
|
api-docs:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ mybatis:
|
|||||||
|
|
||||||
springdoc:
|
springdoc:
|
||||||
swagger-ui:
|
swagger-ui:
|
||||||
path: /swagger
|
path: /swagger-ui.html
|
||||||
api-docs:
|
api-docs:
|
||||||
path: /v3/api-docs
|
path: /v3/api-docs
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user