개발환경
Spring Boot 3.1.3
Spring Security 6.1.3
Swagger 3
Swagger
프론트 <-> 백 간 API를 사용할 때 문서로 작성해야 함
- API 문서 자동화
- 간편한 API 확인
1. Swagger 라이브러리
그새 또 버전이 업그레이드 돼서 이전 버전으로 하면 안됩디다..
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 설정
'Spring' 카테고리의 다른 글
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 5. 스프링 시큐리티, OAuth2.0, Thymeleaf로 소셜로그인 기능 구현하기 (0) | 2023.08.21 |
---|---|
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 4. 타임리프로 화면 구성하기 (0) | 2023.08.18 |
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 3. 프로젝트에 Spring Data JPA 적용하기 (0) | 2023.08.11 |
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 2. 테스트코드 작성하기 (0) | 2023.08.01 |
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 1. 인텔리제이로 스프링 부트 시작하기 (0) | 2023.07.31 |