http 에는 100번대부터 500번대까지의 상태코드가 있지만,
이것만으로는 서버의 구체적인 상황을 이용자들에게 전달해주기 어렵다.
권장되는 응답 형식의 예
{
"resultCode": "S-1",
"msg": "로그인에 성공했습니다."
}
api 의 규격을 바꾸는 상황이면 기존 api를 변경하는 것이 아닌 새로운 버전의 api를 만드는 것이 좋다.
/api/v1/member/login -> /api/v2/member/login
// ALL_VALUE JSON 요청을 고집하지 않음
@GetMapping(value = "/me", consumes = ALL_VALUE)
public RsData<MeResponse> me(@AuthenticationPrincipal User user) {
Member member = memberService.findByUsername(user.getUsername()).get();
return RsData.of(
"S-1",
"성공",
new MeResponse(MemberDto.of(member))
);
}
@GetMapping 애노테이션의 매개변수 consuems는 메서드가 처리할 수 있는 요청의 Content-type을 지정한다. 요청헤더의 Content-type값이 consumes에 포함되지 않을 경우 500번대 에러가 발생한다.
앞에서 api의 규격이 바뀌는 예시가 있었다. 만약 api 통신의 데이터가 엔티티 그 자체라면 규격의 변화에 민감해진다.
엔티티의
createDate라는 필드가 있는데 api규격변화로 클라이언트는 이제부터regDate라는 이름으로 전달을 받는다면, 엔티티와 그에 종속적인 모든것을 수정해야한다.
엔티티를 통신에서 직접적으로 사용하는 것을 피하기 위해 dto를 사용하자.
MemberDto
@Data
public class MemberDto {
private Long id;
private LocalDateTime regDate;
private String username;
public MemberDto(Member member) {
this.id = member.getId();
this.regDate = member.getCreateDate();
this.username = member.getUsername();
}
public static MemberDto of(Member member) {
return new MemberDto(member);
}
}
rest api를 사용해야하는 환경이면 클라이언트는 쿠키를 사용하지 못하는 상황일 확률이 높다. 그말은 서버는 세션을 사용하지 않고 인증이 구현돼야한다는 것이다.
jwt인증방식으로 스프링시큐리티를 설정하기 위해서는
securityFilterChain()
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(
authHttpRequests -> authHttpRequests.requestMatchers("/api/*/member/login")
.permitAll()
.anyRequest()
.authenticated()
)
.cors().disable()
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.sessionManagement(sessionManageMent -> sessionManageMent.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션끄기
.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class); // jwt인증 필터가 UsernamePasswordAuthorizationFilter보다 먼저 로그인을 진행하게 한다.
return http.build();
}
jwt인증필터가 인증처리를 하면 usernamePasswordAuthenticationFilter로 넘어간다.
jwt인증필터
@Component
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
private final MemberService memberService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 헤더에서 Authorization 값을 가져온다.
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null) {
String token = bearerToken.substring("Bearer ".length());
if (jwtProvider.verify(token)) {
Map<String, Object> claims = jwtProvider.getClaims(token);
Long id = Long.parseLong(claims.get("id").toString());
Member member = memberService.findById(id).orElseThrow();
forceAuthentication(member);
}
}
filterChain.doFilter(request, response);
}
// 강제로 로그인 처리하는 메소드
private void forceAuthentication(Member member) {
User user = new User(member.getUsername(), member.getPassword(), member.getAuthorities());
// 스프링 시큐리티 객체에 저장할 authentication 객체를 생성
UsernamePasswordAuthenticationToken authentication =
UsernamePasswordAuthenticationToken.authenticated(
user,
null,
member.getAuthorities()
);
// 스프링 시큐리티 내에 우리가 만든 authentication 객체를 저장할 context 생성
SecurityContext context = SecurityContextHolder.createEmptyContext();
// context에 authentication 객체를 저장
context.setAuthentication(authentication);
// 스프링 시큐리티에 context를 등록
SecurityContextHolder.setContext(context);
}
}
포스트맨으로 api가 정상적으로 실행되는지 확인하는 방법도 있지만 swagger를 사용하면 api명세와 테스트를 더욱 쉽게 할 수 있다.
의존성
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
스프링독 설정
@Configuration
@OpenAPIDefinition(info = @Info(title = "REST API", version = "v1"))
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
scheme = "bearer"
)
public class SpringDocConfig {
}
컨트롤러에서 스웨거 세부설정
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/api/v1/member", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
@Tag(name = "ApiV1MemberController", description = "로그인, 로그인된 회원 정보")
public class ApiV1MemberController {
/*
...
*/
@GetMapping(value = "/me", consumes = ALL_VALUE)
@Operation(summary = "로그인된 사용자 정보",security = @SecurityRequirement(name = "bearerAuth"))
public RsData<MeResponse> me(@AuthenticationPrincipal User user) {
Member member = memberService.findByUsername(user.getUsername()).get();
return RsData.of(
"S-1",
"성공",
new MeResponse(MemberDto.of(member))
);
}
}
/swagger-ui/index.html 로 진입
