반응형
Spring Boot WebFlux + JWT 프로젝트 구현 및 swagger 테스트
Version
- Spring Boot 3.1.4
- gradle 8.2.1
Dependencies
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.2.0'
implementation 'io.jsonwebtoken:jjwt-api:0.12.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.15.2'
Project structure
main
L java.your.package
L Application
L util
L ObjectMapperUtil
L JwtRefreshTokenUtil
L exception
L ClientException
L ServerException
L ExceptionHandler
L config
L security
L AuthenticationManagerImpl
L SecurityContextRepositoryImpl
L JwtProvider
L OpenApiConfig
L WebFluxConfig
L api
L SuccessResponse
L FailureResponse
L ResponseWrapper
L dto
L request
L CreateJwtRequestDto
L JwtRefreshTokenDto
L handler // mvc 의 service 역할
L CreateJwtHandler
L GetUsernameHandler
L router // mvc 의 controller 역할
L UserRouter
L resources
L application.yml
L jwt
L refresh-token-list.csv // jwt 발급을 위핸 테스트용 refresh token 리스트
application.yml
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
jwt:
token-secret: e1558a7d-58fe-4d38-a63d-cbcc973437ab # 원하는 문자열 입력
expiration-ms: 3600000 # 원하는 만료시간(ms) 입력
- jwt.token-secret 가 너무 짧을 경우 아래와 같은 에러 메시지를 받게 된다.
"The specified key byte array is 96 bits which is not secure enough for any JWT HMAC-SHA algorithm. The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size). Consider using the Jwts.SIG.HS256.key() builder (or HS384.key() or HS512.key()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information."
refresh-token-list.csv
token,expiredAt
12345,2023-10-01
67890,2023-10-31
77777,2050-12-31
Application
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
ClientException
import lombok.Getter;
@Getter
public class ClientException extends RuntimeException {
public ClientException(Throwable cause) {
super(cause);
}
}
ServerException
import lombok.Getter;
@Getter
public class ServerException extends RuntimeException {
public ServerException(Throwable cause) {
super(cause);
}
}
ObjectMapperUtil
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.io.InputStream;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import your.package.exception.ServerException;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ObjectMapperUtil {
public static ObjectMapper getInstance() {
return ObjectMapperLazyHolder.INSTANCE;
}
private static class ObjectMapperLazyHolder {
private static final ObjectMapper INSTANCE = getMapper();
private static ObjectMapper getMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
return objectMapper;
}
}
public static <T> String toJson(T t) {
try {
return getInstance().writeValueAsString(t);
} catch (JsonProcessingException e) {
throw new ServerException(e);
}
}
public static byte[] writeValueAsBytes(Object value) {
try {
return getInstance().writeValueAsBytes(value);
} catch (JsonProcessingException e) {
throw new ServerException(e);
}
}
}
JwtRefreshTokenUtil
import static com.fasterxml.jackson.dataformat.csv.CsvSchema.builder;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import com.fasterxml.jackson.dataformat.csv.CsvSchema.ColumnType;
import java.io.IOException;
import java.util.List;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import your.package.api.dto.JwtRefreshTokenDto;
import your.package.exception.ClientException;
import your.package.exception.ServerException;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JwtRefreshTokenUtil {
private static final String PATH = "jwt/refresh-token-list.csv";
public static List<JwtRefreshTokenDto> getRefreshTokens() {
CsvMapper csvMapper = new CsvMapper();
try (MappingIterator<JwtRefreshTokenDto> mappingIterator = csvMapper
.readerFor(JwtRefreshTokenDto.class)
.with(csvSchema())
.readValues(getPathResource(PATH).getFile())) {
return mappingIterator.readAll();
} catch (IOException e) {
throw new ServerException(e);
}
}
private static CsvSchema csvSchema() {
return builder()
.addColumn("token", ColumnType.STRING)
.addColumn("expiredAt", ColumnType.STRING)
.build().withHeader();
}
private static ClassPathResource getPathResource(String path) {
if (StringUtils.isBlank(path)) {
throw new ClientException(new IllegalArgumentException("path is invalid."));
}
return new ClassPathResource(path);
}
}
ExceptionHandler
import java.util.List;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import your.package.api.FailureResponse;
import your.package.util.ObjectMapperUtil;
import reactor.core.publisher.Mono;
@Order(-2) // DefaultErrorWebExceptionHandler 의 @Order(-1) 보다 높은 우선순위를 설정
@Component
public class ExceptionHandler implements ErrorWebExceptionHandler {
@NonNull
@Override
public Mono<Void> handle(ServerWebExchange exchange, @NonNull Throwable throwable) {
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
if (throwable instanceof ServerException serverException) {
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return toResponse(exchange, bufferFactory, failure(serverException));
}
if (throwable instanceof ClientException clientException) {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return toResponse(exchange, bufferFactory, failure(clientException));
}
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return toResponse(exchange, bufferFactory, failure(throwable));
}
private static Mono<Void> toResponse(ServerWebExchange exchange, DataBufferFactory bufferFactory, FailureResponse<Object> serverException) {
return exchange.getResponse().writeWith(Mono.just(bufferFactory.wrap(ObjectMapperUtil
.writeValueAsBytes(serverException))));
}
private static FailureResponse<Object> failure(Throwable throwable) {
return FailureResponse.builder()
.timestamp(System.currentTimeMillis())
.errors(List.of(throwable.getMessage()))
.build();
}
}
AuthenticationManagerImpl
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthenticationManagerImpl implements ReactiveAuthenticationManager {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
//TODO: find user data from database and authenticate it
return Mono.just(new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), null,
authentication.getAuthorities()));
}
}
JwtProvider
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class JwtProvider {
private final byte[] tokenSecretBytes;
private final long expirationMs;
public JwtProvider(@Value("${jwt.token-secret}") String tokenSecret,
@Value("${jwt.expiration-ms}") long expirationMs) {
this.tokenSecretBytes = Base64.getEncoder().encode(tokenSecret.getBytes(StandardCharsets.UTF_8));
this.expirationMs = expirationMs;
}
public String createToken(String username) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expirationMs);
return Jwts.builder()
.subject(username)
.issuedAt(new Date())
.expiration(expiryDate)
.signWith(Keys.hmacShaKeyFor(tokenSecretBytes))
.compact();
}
public String getUsername(String token) {
return getClaims(token).getSubject();
}
public Claims getClaims(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(tokenSecretBytes))
.build()
.parseSignedClaims(token)
.getPayload();
}
}
SecurityContextRepositoryImpl
import io.jsonwebtoken.Claims;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import your.package.exception.ServerException;
import reactor.core.publisher.Mono;
@Component
@RequiredArgsConstructor
public class SecurityContextRepositoryImpl implements ServerSecurityContextRepository {
private final AuthenticationManagerImpl jwtAuthenticationManager;
private final JwtProvider jwtProvider;
@Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
return Mono.empty();
}
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION))
.filter(authHeader -> authHeader.startsWith("Bearer "))
.map(authHeader -> authHeader.substring(7))
.flatMap(token -> {
Claims claims = jwtProvider.getClaims(token);
return jwtAuthenticationManager.authenticate(new UsernamePasswordAuthenticationToken(claims.getSubject(), null, List.of()))
.map(auth -> {
SecurityContextHolder.getContext().setAuthentication(auth);
return SecurityContextHolder.getContext();
});
})
.onErrorMap(ServerException::new);
}
}
OpenApiConfig : swagger 설정
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.security.SecurityScheme.In;
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
import io.swagger.v3.oas.models.servers.Server;
import java.util.List;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
@Configuration
public class OpenApiConfig {
@Value("${server.port}")
private String port;
@Bean
public GroupedOpenApi authOpenApi() {
return GroupedOpenApi.builder()
.group("jwt")
.packagesToScan("your.package.api")
.addOpenApiCustomizer(e -> e.info(new Info()
.title("WebFlux JWT")
.version("1.0"))
.servers(List.of(new Server().url("/")))
.schemaRequirement(HttpHeaders.AUTHORIZATION,
new SecurityScheme().in(In.HEADER)
.scheme("bearer")
.bearerFormat("JWT")
.name(HttpHeaders.AUTHORIZATION)
.type(Type.HTTP)
)
.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))
)
.build();
}
}
WebFluxConfig : spring security 설정
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec;
import org.springframework.security.config.web.server.ServerHttpSecurity.FormLoginSpec;
import org.springframework.security.config.web.server.ServerHttpSecurity.HttpBasicSpec;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import your.package.config.security.AuthenticationManagerImpl;
import your.package.config.security.SecurityContextRepositoryImpl;
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@Configuration
@RequiredArgsConstructor
public class WebFluxConfig implements WebFluxConfigurer {
private final SecurityContextRepositoryImpl securityContextRepository;
private final AuthenticationManagerImpl authenticationManager;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf(CsrfSpec::disable)
.formLogin(FormLoginSpec::disable)
.httpBasic(HttpBasicSpec::disable)
.securityContextRepository(securityContextRepository)
.authenticationManager(authenticationManager)
.authorizeExchange(spec -> spec
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/favicon.ico").permitAll()
.pathMatchers("/swagger-ui.html", "/webjars/swagger-ui/**", "/api-docs/**").permitAll()
.pathMatchers(HttpMethod.GET, "/healthcheck").permitAll()
.pathMatchers(HttpMethod.POST, "/users/jwt").permitAll()
.anyExchange().authenticated())
.exceptionHandling(spec ->
spec.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))
.accessDeniedHandler(new HttpStatusServerAccessDeniedHandler(HttpStatus.FORBIDDEN)))
.build();
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders(HttpHeaders.AUTHORIZATION);
}
}
SuccessResponse
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class SuccessResponse<T> {
private Long timestamp;
private T result;
}
FailureResponse
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class FailureResponse<T> {
private Long timestamp;
private List<T> errors;
}
ResponseWrapper
import java.util.List;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.ServerResponse;
import your.package.exception.ClientException;
import your.package.exception.ServerException;
import reactor.core.publisher.Mono;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ResponseWrapper {
public static <T> Mono<ServerResponse> success(T result) {
return ServerResponse.ok()
.bodyValue(SuccessResponse.builder()
.timestamp(System.currentTimeMillis())
.result(result)
.build());
}
public static Mono<ServerResponse> fail(Throwable exception) {
if (exception instanceof ClientException clientException) {
return ServerResponse.status(HttpStatus.BAD_REQUEST)
.bodyValue(FailureResponse.builder()
.timestamp(System.currentTimeMillis())
.errors(List.of(clientException.getMessage()))
.build());
}
if (exception instanceof ServerException serverException) {
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue(FailureResponse.builder()
.timestamp(System.currentTimeMillis())
.errors(List.of(serverException.getMessage()))
.build());
}
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue(FailureResponse.builder()
.timestamp(System.currentTimeMillis())
.errors(List.of(exception.getMessage()))
.build());
}
}
JwtRefreshTokenDto
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class JwtRefreshTokenDto {
private String token;
private String expiredAt;
}
CreateJwtRequestDto
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDate;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import your.package.exception.ClientException;
import your.package.util.JwtRefreshTokenUtil;
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class CreateJwtRequestDto {
@NotBlank
private String refreshToken;
@NotBlank
private String username;
public void validate() {
if (JwtRefreshTokenUtil.getRefreshTokens().stream()
.filter(e -> Objects.equals(refreshToken, e.getToken()) &&
LocalDate.now().isBefore(LocalDate.parse(e.getExpiredAt())))
.findAny().isEmpty()) {
throw new ClientException(new IllegalArgumentException("token is invalid."));
}
}
}
CreateJwtHandler
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import your.package.api.ResponseWrapper;
import your.package.config.security.JwtProvider;
import your.package.api.dto.request.CreateJwtRequestDto;
import reactor.core.publisher.Mono;
@Slf4j
@Component
@RequiredArgsConstructor
public class CreateJwtHandler implements HandlerFunction<ServerResponse> {
private final JwtProvider jwtProvider;
@NonNull
@Override
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
return request.bodyToMono(CreateJwtRequestDto.class)
.doOnNext(CreateJwtRequestDto::validate)
.flatMap(dto -> {
String token = jwtProvider.createToken(dto.getUsername());
return ResponseWrapper.success(token);
});
}
}
GetUsernameHandler
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import your.package.api.ResponseWrapper;
import reactor.core.publisher.Mono;
@Slf4j
@Component
@RequiredArgsConstructor
public class GetUsernameHandler implements HandlerFunction<ServerResponse> {
@NonNull
@Override
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
return ResponseWrapper.success(SecurityContextHolder.getContext()
.getAuthentication().getPrincipal());
}
}
UserRouter
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.RouterOperation;
import org.springdoc.core.annotations.RouterOperations;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import your.package.api.FailureResponse;
import your.package.api.SuccessResponse;
import your.package.api.dto.request.CreateJwtRequestDto;
import your.package.api.handler.CreateJwtHandler;
import your.package.api.handler.GetUsernameHandler;
import your.package.api.ResponseWrapper;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class UserRouter {
private final CreateJwtHandler createJwtHandler;
private final GetUsernameHandler getUsernameHandler;
@RouterOperations({
@RouterOperation(path = "/users/jwt", produces = {
MediaType.APPLICATION_JSON_VALUE}, method = RequestMethod.POST,
beanClass = CreateJwtHandler.class, beanMethod = "handle",
operation = @Operation(operationId = "createJwtToken",
tags = {"user"},
responses = {
@ApiResponse(responseCode = "200", description = "SUCCESS", content = @Content(schema = @Schema(implementation = SuccessResponse.class))),
@ApiResponse(responseCode = "400", description = "BAD REQUEST", content = @Content(schema = @Schema(implementation = FailureResponse.class))),
@ApiResponse(responseCode = "404", description = "NOT FOUND", content = @Content(schema = @Schema(implementation = FailureResponse.class))),
@ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR", content = @Content(schema = @Schema(implementation = FailureResponse.class)))},
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = CreateJwtRequestDto.class))))
),
@RouterOperation(path = "/users/username", produces = {
MediaType.APPLICATION_JSON_VALUE}, method = RequestMethod.GET,
beanClass = GetUsernameHandler.class, beanMethod = "handle",
operation = @Operation(operationId = "getJwtUsername",
tags = {"user"},
responses = {
@ApiResponse(responseCode = "200", description = "SUCCESS", content = @Content(schema = @Schema(implementation = SuccessResponse.class))),
@ApiResponse(responseCode = "400", description = "BAD REQUEST", content = @Content(schema = @Schema(implementation = FailureResponse.class))),
@ApiResponse(responseCode = "404", description = "NOT FOUND", content = @Content(schema = @Schema(implementation = FailureResponse.class))),
@ApiResponse(responseCode = "500", description = "INTERNAL SERVER ERROR", content = @Content(schema = @Schema(implementation = FailureResponse.class)))
}
)
)
})
@Bean
public RouterFunction<ServerResponse> jwtRouterFunction() {
return RouterFunctions.route()
.path("/users/", builder -> builder
.nest(RequestPredicates.accept(MediaType.APPLICATION_JSON), nestBuilder -> nestBuilder
.POST("jwt", createJwtHandler)
.GET("/username", getUsernameHandler)))
.onError(Exception.class, (exception, request) -> ResponseWrapper.fail(exception))
.build();
}
}
Swagger 접속 / 테스트







반응형
'개발' 카테고리의 다른 글
| Docker Compose 로 Kafka KRaft 서버 올리기 (0) | 2024.01.20 |
|---|---|
| Spring Boot WebFlux + R2DBC + H2 (0) | 2024.01.20 |
| Kafka(카프카) 기본 개념 (0) | 2024.01.20 |