Spring

Spring Boot 3 + Spring Security 6 + Swagger 3 으로 기본 Rest API CRUD 구현하기

주니어주니 2023. 8. 27. 01:18

 

개발환경

Spring Boot 3.1.3
Spring Security 6.1.3
Swagger 3

 

 

Swagger

 

프론트 <-> 백 간 API를 사용할 때 문서로 작성해야 함

- API 문서 자동화

- 간편한 API 확인

 

 

1. Swagger 라이브러리 

그새 또 버전이 업그레이드 돼서 이전 버전으로 하면 안됩디다.. 

 

 

https://springdoc.org/

 

OpenAPI 3 Library for spring-boot

Library for OpenAPI 3 with spring boot projects. Is based on swagger-ui, to display the OpenAPI description.Generates automatically the OpenAPI file.

springdoc.org

 

 

maven

   <dependency>
      <groupId>org.springdoc</groupId>
      <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
      <version>2.2.0</version>
   </dependency>

 

gradle

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

 

 

2. SwaggerConfiguration

원래 없어도 되지만 jwt 구현 등 뭔가 설정이 필요할 때 사용

지금 swagger만 구현해볼 때는 딱히 필요없는 것 같슴다

 

@OpenAPIDefinition 으로 swagger 디폴트 화면 설정

@OpenAPIDefinition(
        info = @Info(
            title = "Member 서비스 API 명세서",
            description = "사용자 서비스 API 명세서",
            version = "v1")
)
@Configuration
public class SwaggerConfig {
...
}

 

 

 

3. application.yaml

필수는 아닌듯?

springdoc:
  swagger-ui:
    disable-swagger-default-url: true

  api-docs:
    path: /api-docs

 

 

4. Security Config

package com.example.studyswaggerjwt.config;

import jakarta.servlet.DispatcherType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private static final String[] AUTH_WHITELIST = {
            "/api/**", "/api-docs/**", "/swagger-ui/**"
    };

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .authorizeHttpRequests(
                        authorize -> authorize
                                .dispatcherTypeMatchers(DispatcherType.ERROR, DispatcherType.ASYNC).permitAll()     // shouldFilterAllDispatcherTypes(false)
                                .requestMatchers(AUTH_WHITELIST).permitAll()
                                .anyRequest().authenticated()
                )
                .httpBasic(HttpBasicConfigurer::disable)    // httpBasic().disable()
                .csrf(CsrfConfigurer::disable)              // csrf().disable()
                .cors(Customizer.withDefaults())            // cors().disable()
                .formLogin(Customizer.withDefaults())       // formLogin().disable()
                .build();


    }
}

시큐리티가 그새 또 엄청 바꼈다....

deprecated 된게 너무 많음

 

 

https://docs.spring.io/spring-security/site/docs/current/api/deprecated-list.html

 

Deprecated List (spring-security-docs 6.1.3 API)

 

docs.spring.io

 

*) 그리고 중요한 점

자꾸 swagger 기본 화면인 petstore 화면만 떠서 왜그러나 했는데

 

스웨거로 가는 경로 중에 /api-docs/ 가 중간에 껴있기 때문에 얘도 permitAll()에 넣어줘야 함

 

 

 

5. Member

 

1) Member 엔티티 

package com.example.studyswaggerjwt.domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    public Member(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public void update(String name, String email) {
        this.name = name;
        this.email = email;
    }

}

 

2) MemberRepository

package com.example.studyswaggerjwt.repository;

import com.example.studyswaggerjwt.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
}

 

3) MemberService

package com.example.studyswaggerjwt.service;

import com.example.studyswaggerjwt.domain.Member;
import com.example.studyswaggerjwt.repository.MemberRepository;
import com.example.studyswaggerjwt.web.dto.MemberRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    // 등록
    @Transactional
    public Member saveMember(MemberRequestDto requestDto) {
        Member member = new Member(requestDto.getName(), requestDto.getEmail());
        return memberRepository.save(member);
    }

    // 전체 조회
    public List<Member> getAllMembers() {
        return memberRepository.findAll();
    }

    // 조회
    @Transactional
    public Member getMemberById(Long id) {
        return memberRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("사용자가 존재하지 않습니다. id={}" + id));
    }

    // 수정
    @Transactional
    public Member updateMember(Long id, MemberRequestDto requestDto) {
        Member member = memberRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("사용자가 존재하지 않습니다. id={}" + id));
        member.update(requestDto.getName(), requestDto.getEmail());

        return member;
    }

    // 삭제
    @Transactional
    public void deleteMember(Long id) {
        Member member = memberRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("사용자가 존재하지 않습니다. id={}" + id));
        memberRepository.delete(member);
    }
}

 

4) MemberController

package com.example.studyswaggerjwt.web;

import com.example.studyswaggerjwt.domain.Member;
import com.example.studyswaggerjwt.service.MemberService;
import com.example.studyswaggerjwt.web.dto.MemberRequestDto;
import com.example.studyswaggerjwt.web.dto.ResponseDto;
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.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "Member", description = "Member API")   // API 그룹 설정
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/members")
@Validated
public class MemberController {

    private final MemberService memberService;

    @Operation(summary = "회원 등록", description = "회원을 등록합니다.")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "등록 성공", content = @Content(schema = @Schema(implementation = ResponseDto.class))),
            @ApiResponse(responseCode = "404", description = "존재하지 않는 리소스 접근", content = @Content(schema = @Schema(implementation = ResponseDto.class)))
    })
    @PostMapping
    public ResponseEntity<Member> save(@RequestBody MemberRequestDto requestDto) {
        Member member = memberService.saveMember(requestDto);
        return new ResponseEntity<>(member, HttpStatus.CREATED);
    }

    @Operation(summary = "전체 회원 조회", description = "전체 회원을 조회합니다.")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "전체 회원 조회 성공", content = @Content(schema = @Schema(implementation = ResponseDto.class))),
            @ApiResponse(responseCode = "404", description = "존재하지 않는 리소스 접근", content = @Content(schema = @Schema(implementation = ResponseDto.class)))
    })
    @GetMapping
    public List<Member> getAllMembers() {
        return memberService.getAllMembers();
    }

    @Operation(summary = "회원 조회", description = "아이디에 해당하는 회원을 조회합니다.")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "조회 성공", content = @Content(schema = @Schema(implementation = ResponseDto.class))),
            @ApiResponse(responseCode = "404", description = "존재하지 않는 리소스 접근", content = @Content(schema = @Schema(implementation = ResponseDto.class)))
    })
    @GetMapping("/{id}")
    public ResponseDto<Member> getMemberById(@PathVariable Long id) {
        Member foundMember = memberService.getMemberById(id);
        return new ResponseDto<>("200", "회원 조회 성공", foundMember);
    }

    @Operation(summary = "회원 수정", description = "아이디에 해당하는 회원 정보를 수정합니다.")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "조회 성공", content = @Content(schema = @Schema(implementation = ResponseDto.class))),
            @ApiResponse(responseCode = "404", description = "존재하지 않는 리소스 접근", content = @Content(schema = @Schema(implementation = ResponseDto.class)))
    })
    @PutMapping("/{id}")
    public ResponseDto<Member> updateMember(@PathVariable Long id, @RequestBody MemberRequestDto requestDto) {
        Member updatedMember = memberService.updateMember(id, requestDto);
        return new ResponseDto<>("200", "회원 수정 성공", updatedMember);
    }

    @Operation(summary = "회원 삭제", description = "회원 정보를 삭제합니다.")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "조회 성공", content = @Content(schema = @Schema(implementation = ResponseDto.class))),
            @ApiResponse(responseCode = "404", description = "존재하지 않는 리소스 접근", content = @Content(schema = @Schema(implementation = ResponseDto.class)))
    })
    @DeleteMapping("/{id}")
    public ResponseDto<Member> deleteMember(@PathVariable Long id) {
        memberService.deleteMember(id);
        return new ResponseDto<>("200", "회원 삭제 성공");
    }

}

 

(1) @Tag : API 그룹 설정

 

@Tag(name = "Member", description = "Member API")   // API 그룹 설정
@RequestMapping("/api/v1/members")
public class MemberController { ... }

 

 

(2) @Operation : 기능 설명

@Operation(summary = "회원 등록", description = "회원을 등록합니다.")

 

(3) @ApiResponse : 응답 설명

없어도 될 것 같긴함...

@ApiResponses({
        @ApiResponse(responseCode = "200", description = "등록 성공", content = @Content(schema = @Schema(implementation = ResponseDto.class))),
        @ApiResponse(responseCode = "404", description = "존재하지 않는 리소스 접근", content = @Content(schema = @Schema(implementation = ResponseDto.class)))
})

 

 

(4) 메소드 기능 별로 보기

 

- POST

 

- GET (전체 회원 조회)

 

- GET (회원 조회)

 

- PUT

 

- DELETE

 

 

5) MemberRequestDto - 등록, 수정 요청 시 사용

package com.example.studyswaggerjwt.web.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Member Request")
public class MemberRequestDto {

    @NotBlank(message = "이름을 입력하세요.")
    @Schema(description = "사용자 이름", example = "Kim Junhee")
    private String name;

    @NotBlank(message = "이메일을 입력하세요.")
    @Schema(description = "사용자 이메일", example = "123@email.com")
    private String email;
}

 

 

6) ResponseDto

package com.example.studyswaggerjwt.web.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ResponseDto<T> {

    @Schema(description = "상태 코드", example = "200")
    private String status;

    @Schema(description = "상태 메시지", example = "성공했습니다.")
    private String message;

    @Schema(description = "데이터")
    private T data;

    public ResponseDto(String status, String message) {
        this.status = status;
        this.message = message;
    }
}

 

굳이 없어도 될 것 같긴 한데

 

만약 ResponseEntity<>(entity, httpstatus) 로 반환 시 

return new ResponseEntity<>(foundMember, HttpStatus.OK);

 

ResponseDto<>(status, message, data) 로 반환 시

return new ResponseDto<>("200", "회원 조회 성공", foundMember);

 

 

 

 

 

 

JWT

 

Spring Session

서버에서 세션 관리, 인증, 인가 수행 -> Spring Security 프레임워크 사용

세션 데이터를 저장하기 위한 다양한 저장소(Redis, JDBC 등) 지원 

 

 

JWT

클라이언트에서 토큰 생성 + 서버에서 검증 (세션 관리 X)

사용자 정보와 권한 정보 포함

서버 - 클라이언트 간 매번 인증 정보를 전송할 필요 없이 토큰만 전송, 인증

 

 

SwaggerConfig

없어도 되지만, jwt를 구현하는 등 뭔가 세밀한 조정이 필요할때 사용

 

package com.example.studyswaggerjwt.config;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
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 java.util.Arrays;

@OpenAPIDefinition(
        info = @Info(
            title = "Member 서비스 API 명세서",
            description = "사용자 서비스 API 명세서",
            version = "v1")
)
@Configuration
public class SwaggerConfig {

    // API 스펙 정의
    @Bean
    public OpenAPI openAPI() {

        SecurityScheme securityScheme = new SecurityScheme()
                .type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")
                .in(SecurityScheme.In.HEADER).name("Authorization");

        SecurityRequirement securityRequirement = new SecurityRequirement().addList("bearerAuth");
        
        return new OpenAPI()
                .components(new Components().addSecuritySchemes("bearerAuth", securityScheme))
                .security(Arrays.asList(securityRequirement));
    }

}

 

 

 - SecurityScheme 보안 스키마 : Bearer 토큰 방식의 보안 정의
(type - Http 스키마 사용, scheme - bearer 토큰 사용, bearerFormat - JWT 형식 토큰 사용,
in - 토큰이 요청 헤더에 위치, name - 헤더 이름 지정)

 


- SecurityRequirement 보안 요구사항 정의
bearerAuth 보안 요구사항 추가


 - OpenAPI 생성, 구성
components - 보안 스키마를 정의한 securityScheme 포함하는 컴포넌트 설정
security - 보안 요구사항을 정의한 securityRequirement 설정