본문 바로가기

개발

Spring Boot WebFlux + JWT + Swagger

반응형

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 접속 / 테스트

 

refresh-token-list.csv 에서 만료기간이 지나지 않은 토큰을 사용하여 JWT 생성
생성 결과
swagger Authorize 를 클릭 후 생성한 JWT 를 저장한다.

 

user 조회
결과

 

반응형

'개발' 카테고리의 다른 글

Docker Compose 로 Kafka KRaft 서버 올리기  (0) 2024.01.20
Spring Boot WebFlux + R2DBC + H2  (0) 2024.01.20
Kafka(카프카) 기본 개념  (0) 2024.01.20