TIL - day 62

정상화·2023년 5월 18일
0

TIL

목록 보기
42/46
post-thumbnail

RestApi


http 상태코드의 다양성 부재

http 에는 100번대부터 500번대까지의 상태코드가 있지만,
이것만으로는 서버의 구체적인 상황을 이용자들에게 전달해주기 어렵다.

권장되는 응답 형식의 예

{
  "resultCode": "S-1",
  "msg": "로그인에 성공했습니다."
}

api 버전

api 의 규격을 바꾸는 상황이면 기존 api를 변경하는 것이 아닌 새로운 버전의 api를 만드는 것이 좋다.

/api/v1/member/login -> /api/v2/member/login


consumes

// 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번대 에러가 발생한다.


DTO

앞에서 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와 스프링 시큐리티

rest api를 사용해야하는 환경이면 클라이언트는 쿠키를 사용하지 못하는 상황일 확률이 높다. 그말은 서버는 세션을 사용하지 않고 인증이 구현돼야한다는 것이다.

jwt인증방식으로 스프링시큐리티를 설정하기 위해서는

  1. 세션기능을 끈다.
  2. 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);
    }
}

Swagger

포스트맨으로 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 로 진입

profile
백엔드 희망

0개의 댓글