Skip to content

Commit

Permalink
chore: Swagger 세팅 (#40)
Browse files Browse the repository at this point in the history
* chore: springdoc 의존성 추가

* feat: 스웨거 전역 헤더 설정 추가

* feat: 스웨거 URL 상수 추가

* feat: 스웨거 프로퍼티 추가

* feat: 스웨거가 운영환경에 맞는 서버 url로 요청하도록 설정

* feat: 개발 및 운영환경에서의 스웨거 Basic 인증 추가

* refactor: 불린 원시 타입을 사용하도록 개선

* chore: 스웨거 계정정보 수정

* feat: 리다이렉트 수정 테스트

* feat: 스웨거 전용 filterChain 추가

* feat: 개발서버 쿠키 설정을 위해 클라이언트 https 도메인 추가

* refactor: `@ConditionalOnProperty` 를 사용하도록 개선

* feat: 스웨거 전용 헤더 로직 추가

* refactor: 상수 이름 수정
  • Loading branch information
uwoobeat authored Feb 11, 2024
1 parent a223438 commit 2cdb216
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 11 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ dependencies {
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public class SecurityConstant {
public static final String REGISTRATION_REQUIRED_HEADER = "Registration-Required";
public static final String TOKEN_ROLE_NAME = "role";
public static final String GITHUB_NAME_ATTR_KEY = "id";
public static final String ACCESS_TOKEN_HEADER_PREFIX = "Bearer ";

private SecurityConstant() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.gdschongik.gdsc.global.common.constant;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum SwaggerUrlConstant {
SWAGGER_RESOURCES_URL("/swagger-resources/**"),
SWAGGER_UI_URL("/swagger-ui/**"),
SWAGGER_API_DOCS_URL("/v3/api-docs/**"),
;

private final String value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@ private UrlConstant() {}
public static final String PROD_CLIENT_URL = "https://onboarding.gdschongik.com";
public static final String DEV_CLIENT_URL = "https://dev-onboarding.gdschongik.com";
public static final String LOCAL_REACT_CLIENT_URL = "http://localhost:3000";
public static final String LOCAL_REACT_CLIENT_SECURE_URL = "https://localhost:3000";
public static final String LOCAL_VITE_CLIENT_URL = "http://localhost:5173";
public static final String LOCAL_VITE_CLIENT_SECURE_URL = "https://localhost:5173";

public static final String PROD_SERVER_URL = "https://api.gdschongik.com";
public static final String DEV_SERVER_URL = "https://dev-api.gdschongik.com";
public static final String LOCAL_SERVER_URL = "http://localhost:8080";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import com.gdschongik.gdsc.global.property.JwtProperty;
import com.gdschongik.gdsc.global.property.RedisProperty;
import com.gdschongik.gdsc.global.property.SwaggerProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@EnableConfigurationProperties({JwtProperty.class, RedisProperty.class})
@EnableConfigurationProperties({JwtProperty.class, RedisProperty.class, SwaggerProperty.class})
@Configuration
public class PropertyConfig {}
60 changes: 60 additions & 0 deletions src/main/java/com/gdschongik/gdsc/global/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.gdschongik.gdsc.global.config;

import static com.gdschongik.gdsc.global.common.constant.EnvironmentConstant.*;
import static com.gdschongik.gdsc.global.common.constant.UrlConstant.*;
import static org.springframework.http.HttpHeaders.*;

import com.gdschongik.gdsc.global.util.EnvironmentUtil;
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 io.swagger.v3.oas.models.servers.Server;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class SwaggerConfig {

private final EnvironmentUtil environmentUtil;

@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.servers(swaggerServers())
.addSecurityItem(securityRequirement())
.components(authSetting());
}

private List<Server> swaggerServers() {
Server server = new Server().url(getServerUrl());
return List.of(server);
}

private String getServerUrl() {
return switch (environmentUtil.getCurrentProfile()) {
case PROD -> PROD_SERVER_URL;
case DEV -> DEV_SERVER_URL;
default -> LOCAL_SERVER_URL;
};
}

private Components authSetting() {
return new Components()
.addSecuritySchemes(
"Authorization",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name(AUTHORIZATION));
}

private SecurityRequirement securityRequirement() {
return new SecurityRequirement().addList(AUTHORIZATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,31 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gdschongik.gdsc.domain.auth.application.JwtService;
import com.gdschongik.gdsc.domain.member.dao.MemberRepository;
import com.gdschongik.gdsc.global.common.constant.SwaggerUrlConstant;
import com.gdschongik.gdsc.global.property.SwaggerProperty;
import com.gdschongik.gdsc.global.security.CustomSuccessHandler;
import com.gdschongik.gdsc.global.security.CustomUserService;
import com.gdschongik.gdsc.global.security.JwtExceptionFilter;
import com.gdschongik.gdsc.global.security.JwtFilter;
import com.gdschongik.gdsc.global.util.CookieUtil;
import com.gdschongik.gdsc.global.util.EnvironmentUtil;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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.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;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
Expand All @@ -36,25 +46,65 @@ public class WebSecurityConfig {
private final CookieUtil cookieUtil;
private final ObjectMapper objectMapper;
private final EnvironmentUtil environmentUtil;
private final SwaggerProperty swaggerProperty;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
private static void defaultFilterChain(HttpSecurity http) throws Exception {
http.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.cors(withDefaults())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
}

@Bean
@Order(1)
@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "dev")
public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws Exception {
defaultFilterChain(http);

http.securityMatcher(getSwaggerUrls())
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.httpBasic(withDefaults());

return http.build();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
defaultFilterChain(http);

http.oauth2Login(
oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(customUserService(memberRepository)))
.successHandler(customSuccessHandler(jwtService, cookieUtil)));
.successHandler(customSuccessHandler(jwtService, cookieUtil))
.failureHandler((request, response, exception) -> response.setStatus(401)));

http.addFilterBefore(jwtFilter(jwtService, cookieUtil), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtExceptionFilter(objectMapper), JwtFilter.class);
http.addFilterAfter(jwtExceptionFilter(objectMapper), LogoutFilter.class);
http.addFilterAfter(jwtFilter(jwtService, cookieUtil), LogoutFilter.class);

return http.build();
}

private static String[] getSwaggerUrls() {
return Arrays.stream(SwaggerUrlConstant.values())
.map(SwaggerUrlConstant::getValue)
.toArray(String[]::new);
}

@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
UserDetails user = User.withUsername(swaggerProperty.getUsername())
.password(passwordEncoder().encode(swaggerProperty.getPassword()))
.roles("SWAGGER")
.build();
return new InMemoryUserDetailsManager(user);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public CustomUserService customUserService(MemberRepository memberRepository) {
return new CustomUserService(memberRepository);
Expand Down Expand Up @@ -86,7 +136,9 @@ public CorsConfigurationSource corsConfigurationSource() {
if (environmentUtil.isDevProfile()) {
configuration.addAllowedOriginPattern(DEV_CLIENT_URL);
configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_URL);
configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_SECURE_URL);
configuration.addAllowedOriginPattern(LOCAL_VITE_CLIENT_URL);
configuration.addAllowedOriginPattern(LOCAL_VITE_CLIENT_SECURE_URL);
}

configuration.addAllowedHeader("*");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.gdschongik.gdsc.global.property;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@AllArgsConstructor
@ConfigurationProperties(prefix = "swagger")
public class SwaggerProperty {

private final String username;
private final String password;
}
21 changes: 21 additions & 0 deletions src/main/java/com/gdschongik/gdsc/global/security/JwtFilter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.gdschongik.gdsc.global.security;

import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.*;

import com.gdschongik.gdsc.domain.auth.application.JwtService;
import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto;
import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto;
Expand All @@ -14,6 +16,7 @@
import java.io.IOException;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
Expand All @@ -31,9 +34,20 @@ public class JwtFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

String accessTokenHeaderValue = extractAccessTokenFromHeader(request);
String accessTokenValue = extractTokenValue(JwtConstant.ACCESS_TOKEN, request);
String refreshTokenValue = extractTokenValue(JwtConstant.REFRESH_TOKEN, request);

// 헤더에 AT가 있으면 우선적으로 검증 (Swagger 테스트 전용)
if (accessTokenHeaderValue != null) {
AccessTokenDto accessTokenDto = jwtService.retrieveAccessToken(accessTokenHeaderValue);
if (accessTokenDto != null) {
setAuthenticationToContext(accessTokenDto.memberId(), accessTokenDto.memberRole());
filterChain.doFilter(request, response);
return;
}
}

// AT와 RT 중 하나라도 없으면 실패
if (accessTokenValue == null || refreshTokenValue == null) {
filterChain.doFilter(request, response);
Expand Down Expand Up @@ -71,6 +85,13 @@ private String extractTokenValue(JwtConstant jwtConstant, HttpServletRequest req
.orElse(null);
}

private String extractAccessTokenFromHeader(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
.filter(header -> header.startsWith(ACCESS_TOKEN_HEADER_PREFIX))
.map(header -> header.replace(ACCESS_TOKEN_HEADER_PREFIX, ""))
.orElse(null);
}

private void setAuthenticationToContext(Long memberId, MemberRole memberRole) {
UserDetails userDetails = new PrincipalDetails(memberId, memberRole);
Authentication authentication =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private ResponseCookie generateCookie(String cookieName, String tokenValue, Stri
.path("/")
.secure(true)
.sameSite(sameSite)
.httpOnly(false)
.httpOnly(true)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ public String getCurrentProfile() {
.orElse(LOCAL);
}

public Boolean isProdProfile() {
public boolean isProdProfile() {
return getActiveProfiles().anyMatch(PROD::equals);
}

public Boolean isDevProfile() {
public boolean isDevProfile() {
return getActiveProfiles().anyMatch(DEV::equals);
}

public Boolean isProdAndDevProfile() {
public boolean isProdAndDevProfile() {
return getActiveProfiles().anyMatch(PROD_AND_DEV::contains);
}

Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/application-swagger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
spring:
config:
activate:
on-profile: "security"

swagger:
username: ${SWAGGER_USER:default}
password: ${SWAGGER_PASSWORD:default}
1 change: 1 addition & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ spring:
- redis
- storage
- security
- swagger

logging:
level:
Expand Down

0 comments on commit 2cdb216

Please sign in to comment.