본문으로 바로가기

[Spring] 스프링 기반 REST API 개발 - 3

category Spring 2021. 8. 6. 10:38

https://www.inflearn.com/course/spring_rest-api

 

스프링 기반 REST API 개발 - 인프런 | 강의

다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발하는 강의입니다., 스프링 기반 REST API 개발 이 강좌에...

www.inflearn.com

ㅁ 이벤트 조회 및 수정 REST API 개발

ㅇ 이벤트 목록 조회 API 구현

- page를 PagedResourcesAssembler<Event> 를 통해 Resource로 구현

 > var pagedResources = pagedResourcesAssembler.toModel(page);

- 구현한 30개의 event 들도 EventResource로 만들어서 링크를 만들어 담을 수 있다.

- modelMapper로 객체와 객체DTO 연결

ㅁ REST API 보안 적용

ㅇ Account 도메인 추가

- 스프링 시큐리티 OAuth2 추가

- @Table(“Users”)로 구현할 수 있으나, Account 도메인 생성

 

ㅇ 스프링 시큐리티

- 웹 시큐리티와 메소드 시큐리티로 구분

 > 웹 시큐리티 : 웹 요청에 보안 인증

 > 메소드 시큐리티 : 어떤 메소드가 호출되었을때 인증, 권한을 확인함

 >> 공통된 SecurityIntercepter 인터페이스를 가짐

 >> 구현체가 2개 : Method Security Intercepter, Filter SecurityIntercepter

- 스프링 5부터 웹기반이 서블릿과 webflux로 나눠짐

- SecuritContext Holder (자바 쓰레드 로컬 - 한 쓰레드 내에서 공유하는 자원(저장소) )에 인증정보를 담아옴

 > 쓰레드 로컬은 자원을 넘겨줄때 파라미터로 넘겨주지 않아도 됨

 > 인증된 정보를 꺼내서 있을 경우,  Authentication Manager (로그인 담당) 이 로그인을 시킴

  >> 인증 요청 헤더에 Authentication, basic, username, password를 합쳐서 인코딩 한걸로 입력을 받음

  >> UserDetailsService를 통해 DB에서 읽어 온 패스워드와 사용자가 입력한 password가 매칭되는지 passwordencorder로 비교

  >> 이후 로그인이 되었으면, AccessDecisionManager를 통해 Account 의 Role을 확인

 

- UserDetailService의 구현체 서비스에서 우리가 로직을 짜는데, 여기서 UserDetails라는 형식으로 반환을 해줘야함

 > 그러기 위해서 스프링 시큐리티의 authorities(account.getRoles()) 구현

- fail() 이라는 메소드도 있음

 - @Rule은 Junit4 애노테이션인데, 아래 디펜던시 추가하여 Junit5에서 사용

 > Junit3, Junit4 애노테이션 지원

       <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <scope>test</scope>
        </dependency>

- 스프링 시큐리티 디펜던시를 추가하면, 스프링 부트는 스프링시큐리티 자동설정을 적용하게 되고, 모든 요청은 인증을 필요하게 됨

 > 인메모리에 사용자 계정이 추가됨

 > 아래 추가시, 시큐리티 자동설정은 해지되고 아래에서 설정한 것만 적용됨

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

- PasswordEncorder는 hashmap을 만들고, 인코딩을 진행하여 인코딩타입, 인코딩값 으로 해시맵 저장

- HttpSecurity와 WebSecurity 필터로 나누는걸 실습했는데,

  http 보다는 web에서 필터링 하는게 더 부하를 적게 준다.

 > web 필터링 -> http 필터링 순으로 진행

- 아래와 같이 작성하면, 특정 url에 인증정보가 없을 경우 로그인 창으로 가게 된다. (폼 인증)

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.anonymous()
                .and()
                .formLogin()
                .and()
                .authorizeRequests()
                .mvcMatchers(HttpMethod.GET,"/api/**").authenticated()
                .anyRequest().authenticated();
    }

ㅇ 스프링 시큐리티 oAuth2 설정 : 인증 서버 설정

- Grant Type : Password 방식의 토큰 인증 방식 구성

 > 토큰 발급시, 1홉으로 진행됨

 > 실제 여러홉으로 진행해야하는 토큰발급 방식이 많음

 > 인증정보를 가지고 있는 쪽에서 사용해야함

- 인증서버 설정에

 > passwordEncorder 설정

 > 클라이언트디테일서비스 설정 : 클라이언트 아이디, 토큰 타입, 토큰 시간 등

 >  엔드포인트 설정 : authenticationmanager, userdetailsservice, tokenstore

 +) 클라이언트 아이디, 시크릿은 어플리케이션 기준으로 제공됨

 

좋은 질문 & 답글

더보기

Token 발급 TEST 코드를 작성하면서  "/oauth/token" URL을 POST 방식으로 요청하는 것을 볼 수 있었습니다.

저희가 직접 "/oauth/token" URL에 대해 매핑을 하지 않아도 처리가 가능했던 이유는

pom.xml에서 "spring-security-oauth2-autoconfigure" 의존성 설정을 함으로써 가능했던 것이고,

더 나아가 실제로 어떤 지점에서 "/oauth/token" URL이 매핑되어 처리가 되는지 디버깅을 통해 찾아본 결과,

org.springframewirk.security.oauth2.config.annotaion.web.configuration 패키지의

WebSecurityConfigurerAdapter를 상속받은 AuthorizationServerSecurityConfiguration 클래스의 configure(HttpSecurity http) 메소드가 재정의 됨으로써 

"/oauth/token" URL이 매핑이 되었다는 것을 알 수 있었습니다

 

ㅇ 리소스 서버 설정

- 토큰 기반으로 인증을 확인하고, 접근을 제한하는 서버

- 테스트 전에 db를 비워준다

 > 인메모리 h2 이지만, 테스트 동시 실행시 테스트간에 디비가 공유됨

 > 위 문제로 토큰을 가져오지 못하는 문제 발생

- 아래 코드로 해결

    //@Before - Junit4
    @BeforeEach
    public void setUp(){
        this.eventRepository.deleteAll();
        this.accountRepository.deleteAll();
    }

 

ㅇ 문자열을 외부 설정으로 빼내기

- AppProperties 클래스 생성하여, 

@ConfigurationProperties(prefix = "my-app")

 설정 후, application.properties 에서 my-app.xxx 로 설정함

 이후 빈 설정 및 호출하여 필요한 곳에 설정

 > 테스트 쪽 application.properties에 my-app 을 적었는데, 이러면 오류남

 

ㅇ 현재 사용자 조회

- 인증된 사용자를 통해, 

- Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

 위에 코드 통해,  request의 header에 담긴 토큰에 대한 authentication 정보를 받을 수 있다.

 > 이후 User principal = (User) authentication.getPrincipal(); 로 스프링 시큐리티의 User로 객체를 받을 수 있음

 > @AuthenticationPrincipal User user 로 아규먼트로도 주입 가능

 > UserDetailsService의 구현체에 loadUserByUsername 메소드의 리턴을 내가 원하는 값의 adpator로 만든다 (이것은 시큐리티의 User 상속)

> @AuthenticationPrincipal(expression = "account") Account account 로 만들면 SPel에 의해 adaptor객체에 있는, account가 추출되어 담긴다.

> 위 내용을 아래와 같이 애노테이션을 만들어 적용

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public @interface CurrentUser {
}

ㅇ 출력값 제한하기

- createEvent 발생시, Account 객체의 password 도 나와 제한 필요

- JsonSerializer 사용

 > 이후 Event 객체의 Account manager property에도 @JsonSerialize(using= AccountSerializer.class) 를 통해 ID만 가지게 수정

public class AccountSerializer extends JsonSerializer<Account> {
    @Override
    public void serialize(Account account, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeNumberField("id",account.getId());

        jsonGenerator.writeEndObject();
    }
}

 

ㅇ 깨진 테스트 살펴보기 & 스프링 부트 업그레이드

- DemoInflearnRestApiApplicationTests 가 실패하는 이유?

 > 기본적으로 생성된 테스트인데, 프로파일이 테스트로 설정되어있지 않음

 > @ActiveProfile(value="test" ) 로 설정하면 application-test.properties 가 application.properties를 오버라이딩하면서 적용됨

- @TestDescription (커스터마이징 ) -> @DisplayName