Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_63일차_"Swagger"

LEFT 2025. 3. 11. 17:15

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [63]일차

🚀63차에는 API 문서화의 Swagger에 대해 배울 수 있었다. 이전에 SpringContextHolder 등의 개념에 대해 다시 복습해보았다.

학습 목표 : Swagger로 만들 수 있는 프로젝트의 명세를 파악할 수 있어야함

학습 과정 : 회고를 통해 작성


OAuth2

SpringContextHolder

  • Spring의 ApplicationContext를 정적으로 보관하고 접근할 수 있도록 도와주는 유틸리티 클래스
  • 빈(Bean) 객체를 전역적으로 조회할 수 있게 해줌
  • 일반적으로 @Autowired를 사용하면 빈을 주입받을 수 있지만,
    정적 메서드나 외부 클래스에서 빈을 직접 가져와야 할 때 SpringContextHolder를 활용
@Component
public class SpringContextHolder implements ApplicationContextAware {
    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        context = applicationContext;
    }
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}
// SpringContextHolder사용
MyService myService = SpringContextHolder.getBean(MyService.class);

  • ❓SpringContextHolder가 필요한 경우

➡️정적 메서드, 스케줄링 작업, 또는 @Autowired를 사용할 수 없는 외부 라이브러리에서 빈을 조회해야 할 때 필요


Authentication 객체

  • Spring Security에서 인증된 사용자 정보를 담는 핵심 객체 (=사용자의 로그인 정보 및 권한을 담는 핵심 객체)
  • 인증이 성공하면 SecurityContext 내부에 Authentication 객체가 저장
  • 이 객체는 현재 로그인한 사용자의 신원, 권한 정보 등을 포함
  • SecurityContextHolder를 통해 현재 사용자의 Authentication을 조회 가능
  • 주로 UsernamePasswordAuthenticationToken을 사용하여 사용자 인증을 처리
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName(); // 현재 로그인한 사용자 이름
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 권한 목록
  • ❓Authentication 객체 생성 시기
    ➡️로그인 인증이 성공하면 Spring Security 필터 체인(SecurityFilterChain)에서 생성됩니다.

  • ❓Authentication 객체 저장 위치
    ➡️SecurityContext 내부에 저장되며, 기본적으로 ThreadLocal을 사용합니다.

Authentication의 Principal, Credentials, Authorities

Principal 사용자의 주체 정보 (ex: UserDetails, 사용자 ID)
Credentials 사용자의 인증 수단 (ex: 비밀번호, 토큰)
Authorities 사용자의 권한 목록 (ex: ROLE_USER, ROLE_ADMIN)
  • Principal: 현재 로그인한 사용자의 정보
  • Credentials: 인증에 사용된 비밀번호 같은 민감한 정보를 포함, 인증 후에는 null로 설정
  • Authorities: 사용자의 권한(Role) 목록을 제공, GrantedAuthority 형태로 저장
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

Object principal = authentication.getPrincipal(); // UserDetails 객체 또는 사용자 ID
Object credentials = authentication.getCredentials(); // 인증 정보 (일반적으로 null)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 권한 목록
  • ❓로그인 후 credentials 값
    ➡️ 보안 강화를 위해 credentials는 일반적으로 null 처리

  • ❓Principal이 객체 확인 방법
    ➡️authentication.getPrincipal()을 출력. 보통 UserDetails 또는 사용자 ID

SecurityConfig - authorizeHttpRequests()

.authorizeHttpRequests(auth -> auth
        .requestMatchers("/oauth/users/loginform", "/loginform","/userregform", "/").permitAll()
        .requestMatchers("/oauth2/**", "/login/oauth2/code/github", "/registerSocialUser", "/saveSocialUser").permitAll()
        .requestMatchers("/error").permitAll()
        .anyRequest().authenticated()
)
  • 동작 흐름 :
    1) OAuth 2.0 인증 과정에서 서버가 리다이렉트 요청을 받음
    2) 사용자가 GitHub 로그인 시도
    3) GitHub은 인증이 완료된 후 redirect_uri에 설정된 URL로 리다이렉트
    3-1) GitHub가 우리 서버로 요청을 보냄
    ➡️GET /login/oauth2/code/github?code=abcdefg123456

    4) /login/oauth2/code/github 경로를 허용하지 않으면
    5) Spring Security가 요청을 차단하여 OAuth 2.0 인증이 제대로 동작하지 않음

  • 경로를 허용하게되면
    1) Spring Security는 이 요청을 가로채고,
    2) OAuth2LoginAuthenticationFilter가 code 값을 사용하여 GitHub에 다시 토큰 요청을 보냄
    3) GitHub이 발급한 Access Token을 받아 사용자 정보를 가져옴
    4) 로그인 처리
    5) /login/oauth2/code/github 경로는 반드시 허용(permitAll())해야 OAuth 인증이 정상적으로 완료

  •  /login/oauth2/code/github
    ➡️ GitHub이 인증 후 리다이렉트하는 URL을 허용

SecurityConfig - oauth2Login()

.oauth2Login(oauth2 -> oauth2
        .loginPage("/loginform")
        .failureUrl("/loginFailure")
        .userInfoEndpoint(userInfo -> userInfo
                .userService(this.oauth2UserService())
        )
        // 소셜 인증 성공 후 수행할 일
        .successHandler(customOAuth2AuthenticationSuccessHandler)
);

❓userInfoEndpoint() 부분

➡️OAuth 2.0 인증 과정에서 클라이언트가 신뢰할만한 경우에만 사용자 정보를 받을 수 있으며,
userInfoEndpoint()는 해당 정보를 어떻게 다룰지 결정

Spring Security는 기본적으로 DefaultOAuth2UserService를 사용하여 OAuth 2.0 사용자 정보를 가져오며,
이를 상속하거나 감싸서 확장 가능 (=사용자 정보를 DB에 저장하거나, 특정 권한을 부여하는 등의 추가 처리)

💡DefaultOAuth2UserService : OAuth 2.0 제공자(GitHub, Google 등)에서 사용자 정보를 가져오는 기본 서비스

@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(){
    DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
    return oauth2UserRequest -> {
        OAuth2User oAuth2User = delegate.loadUser(oauth2UserRequest);
        
        String token = oauth2UserRequest.getAccessToken().getTokenValue();

        String provider = oauth2UserRequest.getClientRegistration().getRegistrationId();
        String socialId = String.valueOf(oAuth2User.getAttributes().get("id"));
        String username = (String) oAuth2User.getAttributes().get("login");
        String email = (String) oAuth2User.getAttributes().get("email");
        String avatarUrl = (String) oAuth2User.getAttributes().get("avatar_url");

        socialUserService.saveOrUpdateUser(socialId, provider, username, email, avatarUrl);
        return oAuth2User;
    };
}

정리하자면

OAuth 2.0 인증 과정에서 userInfoEndpoint()는 사용자 정보를 가져오는 단계를 담당

1) 인증 요청 : 사용자가 소셜 로그인 버튼 클릭

2) 인가 코드 발급 : 사용자가 OAuth 제공자에서 로그인하면 code를 서버로 전달
➡️사용자가 소셜 로그인을 진행하면 OAuth 서버(GitHub 등)에서 인증을 수행하고 Access Token을 발급

3) Access Token 발급 : code를 사용하여 Access Token 요청 후 사용자 정보 요청
➡️userInfoEndpoint()가 OAuth 서버에 Access Token을 보내서 사용자 정보를 요청

4) 응답을 받은 후 userService(this.oauth2UserService())를 호출하여 사용자 정보 객체(OAuth2User)를 생성

5) 생성된 사용자 정보를 Spring Security가 인증 객체(Authentication)로 변환하여 인증 완료


CustomOAuth2AuthenticationSuccessHandler

else{
    SocialLoginInfo socialLoginInfo = socialLoginInfoService.saveSocialLoginInfo(provider, socialId);
    response.sendRedirect("/registerSocialUser?provider="+provider+"&socialId="
            +socialId+"&name="+name+"&uuid="+socialLoginInfo.getUuid());
}
  • 회원가입되지 않은 상태일때 추가적으로 가져오고 싶은 데이터가 있으면 /registerSocialUser로 리다이렉트
    ➡️회원가입이 되어 있지 않으면 /registerSocialUser로 리다이렉트하여 추가 정보 입력을 유도하는 것

    리다이렉트할때는provider, socialId, name, uuid 등을 추가적으로 얻어내어 보내줌

로그아웃 흐름

 

SecurityConfig

.logout(logout -> logout
        .logoutSuccessUrl("/") // 로그아웃이 성공적으로 이루어졌을때 ("/" = /home)으로 갈 것
        .invalidateHttpSession(true)
        .deleteCookies("JSESSIONID") // JSESSIONID의 쿠키를 삭제
);
  • .invalidateHttpSession(true)
    ➡️현재 로그인된 세션을 제거하여 세션 기반 로그인 정보를 삭제
    ➡️다시 로그인을 시도하면 새로운 세션이 생성

  • .deleteCookies("JSESSIONID")
    ➡️
    JSESSIONID 쿠키를 삭제하여 세션 유지가 불가능하도록 처리

🚀실습 - Login With Github : 소셜로그인 흐름 확인

  • Login With Github 하이퍼링크를 클릭하면 GET 요청으로 이러한 URL이 Github.com에 전달

  • 값들을 가져와서 provider, socialId, name, uuid 등을 앱의 서버로 다시 보냄

Swagger

  • API는 소프트웨어 간 원활한 상호작용을 가능하게 하는 필수적 요소
    (= Swagger : API문서를 자동으로 생성하고 관리할 수 있도록 도와주는 도구)

  • API의 문서화의 필요성에 대해 효과적으로 수행하는 도구가 Swagger
  • API의 문서화 : 개발자들이 API를 쉽게 이해하고 사용할 수 있도록
    ➡️API의구조, 요청 및 응답 방식, 인증방법, 오류 코드 등을 정리한 문서 ex. 사용설명서

설정 파일 - @config

@Configuration // Configuration을 제외하고 모두 Swagger가 제공하는 어노테이션
@OpenAPIDefinition(
        info = @Info(title = "모임 및 일정 관리 API 문서",
        version = "1.0",
        description = "모임 및 일정을 관리하기 위한 API 문서"
        )
)
public class SwaggerConfig { ... }

  • http://localhost:8080/swagger-ui/index.html 접속해보기

  • @OpenAPIDefinition 설정
    @OpenAPIDefinition : OpenAPI의 명세를 설정하는데 사용

    info : API문서의 기본정보 (제목, 설명, 버전 등)
    servers : API서버의 URL 설정
    security : 전역 보안 요구 사항 설정 (ex. JWT 인증)

AuthController

@RestController
@RequestMapping("/auth")
public class AuthController {
    @Operation(
            summary = "회원가입",
            description = "이메일과 비밀번호를 입력하여 회원가입을 합니다."
    )
    @PostMapping("/register")
    public ResponseEntity<String> register(){
        return ResponseEntity.ok("ok");
    }
}


Controller - @Schema로 DTO 연결

RegisterRequestDto

@Getter@Setter
@Schema(description = "회원가입 요청 DTO") // Swagger의 어노테이션
public class RegisterRequestDto {
    // 데이터가 들어가는 부분에 샘플데이터를 지정해서 넣어줄 수 있음
    @Schema(description = "사용자 이메일", example = "user@example.com")
    private String email;

    @Schema(description = "사용자 비밀번호", example = "password123")
    private String password;
}


AuthController의 register()메소드

@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody RegisterRequestDto requestDto){
    // ...
    return ResponseEntity.ok("ok");
}
  • 인자로 RegisterRequestDto 추가

  • 장점 : Try it out으로 데이터들을 테스트 가능

SwaggerConfig 설정 추가

@Configuration
@OpenAPIDefinition(
        info = @Info(title = "모임 및 일정 관리 API",
                version = "1.0",
                description = "모임 및 일정을 관리하기 위한 API 문서"
        )
        ,security = @SecurityRequirement(name = "bearerAuth")
)
@SecurityScheme(
        name = "bearerAuth",
        type = SecuritySchemeType.HTTP,
        scheme = "bearer",
        bearerFormat = "JWT"
)
public class SwaggerConfig {
}

  • Scheme 관련하여 bearer 추가

Controller - 로그아웃, 사용자 목록/정보 조회 메소드 추가

@PostMapping("/logout")
public ResponseEntity<String> logout(
        @Parameter(description = "JWT 인증 토큰", required = true, example = "Bearer eyJhbGciOiJIUzI....")
        @RequestHeader("Authorization") String token
){

    return ResponseEntity.ok("로그아웃 성공");
}

@Operation(summary = "사용자 목록 조회", description = "사용자 목록을 페이지 단위로 조회합니다.")
@GetMapping("/users")
public ResponseEntity<List<UserDto>> getUsers(
        @RequestParam(value = "page", required = false, defaultValue = "1") int page,
        @RequestParam(value = "size", required = false, defaultValue = "10") int size) {
//        List<UserDto> users = userService.getUsers(page, size);
    return ResponseEntity.ok(null);
}
@Operation(summary = "사용자 정보 조회", description = "사용자의 고유 ID를 이용하여 정보를 조회합니다.")
@GetMapping("/users/{id}")
public ResponseEntity<UserDto> getUserById(
        @PathVariable("id") Long id) {
//        UserDto user = userService.getUserById(id);
    return ResponseEntity.ok(null);
}

  • @Operation, @Parameter 등으로 API 문서에 넣을 콘텐츠를 설정할 수 있다.

🚀회고 결과 :

Swagger의 핵심 개념에 대해서 알 수 있었다. 이를 통해 프로젝트를 어떻게 구성해야할지를 좀 더 학습할 수 있었다.

향후 계획 : 

- Swagger를 활용한 모임 프로젝트 명세서 분석