1. 개요

Security은 Spring 생태계의 일류 시민입니다. 따라서 OAuth2가 거의 구성 없이 Spring Web MVC와 함께 작동할 수 있다는 것은 놀라운 일이 아닙니다.

그러나 기본 Spring 솔루션이 프레젠테이션 계층을 구현하는 유일한 방법은 아닙니다. JAX-RS 호환 구현인 Jersey 는 Spring OAuth2와 함께 작동할 수도 있습니다.

이 예제에서는 OAuth2 표준을 사용하여 구현된 Spring Social Login으로 Jersey 애플리케이션을 보호하는 방법을 알아봅니다 .

2. 메이븐 의존성

Jersey를 Spring Boot 애플리케이션에 통합하기 위해 spring-boot-starter-jersey 아티팩트를 추가해 보겠습니다 .

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jersey</artifactId>
</dependency>

Security OAuth2를 구성하려면 spring-boot-starter-securityspring-security-oauth2-client 가 필요합니다 .

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
</dependency>

Spring Boot Starter Parent 버전 2 를 사용하여 이러한 모든 의존성을 관리합니다 .

3. 저지 프레젠테이션 레이어

Jersey를 프레젠테이션 계층으로 사용하려면 몇 개의 끝점이 있는 리소스 클래스가 필요합니다.

3.1. 리소스 클래스

다음은 끝점 정의가 포함된 클래스입니다.

@Path("/")
public class JerseyResource {
    // endpoint definitions
}

클래스 자체는 매우 간단합니다 . @Path 어노테이션만 있습니다. 이 어노테이션의 값은 클래스 본문의 모든 끝점에 대한 기본 경로를 식별합니다.

이 리소스 클래스는 구성 요소 스캔을 위한 스테레오타입 어노테이션을 포함하지 않는다는 점을 언급할 가치가 있습니다. 사실 Spring bean일 필요도 없다. 그 이유는 요청 매핑을 처리하기 위해 Spring에 의존하지 않기 때문입니다.

3.2. 로그인 페이지

로그인 요청을 처리하는 방법은 다음과 같습니다.

@GET
@Path("login")
@Produces(MediaType.TEXT_HTML)
public String login() {
    return "Log in with <a href=\"/oauth2/authorization/github\">GitHub</a>";
}

이 메서드는 /login 끝점 을 대상으로 하는 GET 요청에 대한 문자열을 반환합니다 . text/html 콘텐츠 유형 은 사용자의 브라우저에 클릭 가능한 링크와 함께 응답을 표시하도록 지시합니다.

GitHub를 OAuth2 공급자로 사용할 것이므로 링크는 /oauth2/authorization/github 입니다. 이 링크는 GitHub 인증 페이지로의 리디렉션을 트리거합니다.

3.3. 홈 페이지

루트 경로에 대한 요청을 처리하는 다른 방법을 정의해 보겠습니다.

@GET
@Produces(MediaType.TEXT_PLAIN)
public String home(@Context SecurityContext securityContext) {
    OAuth2AuthenticationToken authenticationToken = (OAuth2AuthenticationToken) securityContext.getUserPrincipal();
    OAuth2AuthenticatedPrincipal authenticatedPrincipal = authenticationToken.getPrincipal();
    String userName = authenticatedPrincipal.getAttribute("login");
    return "Hello " + userName;
}

이 메서드는 로그인한 사용자 이름을 포함하는 문자열인 홈 페이지를 반환합니다. 이 경우 로그인 속성 에서 사용자 이름을 추출했습니다 . 그러나 다른 OAuth2 공급자는 사용자 이름에 대해 다른 속성을 사용할 수 있습니다.

분명히 위의 방법은 인증된 요청에 대해서만 작동합니다. 요청이 인증되지 않은 경우 로그인 엔드포인트 로 리디렉션됩니다 . 섹션 4에서 이 리디렉션을 구성하는 방법을 살펴보겠습니다.

3.4. Spring Container에 Jersey 등록하기

Jersey 서비스를 활성화하기 위해 서블릿 컨테이너에 리소스 클래스를 등록해 보겠습니다. 다행히도 매우 간단합니다.

@Component
public class RestConfig extends ResourceConfig {
    public RestConfig() {
        register(JerseyResource.class);
    }
}

ResourceConfig 하위 클래스에 JerseyResource등록 하여 해당 리소스 클래스의 모든 엔드포인트를 서블릿 컨테이너에 알렸습니다.

마지막 단계는 이 경우 RestConfig 인 ResourceConfig 서브클래스를 Spring 컨테이너에 등록하는 것이다. @Component 어노테이션 으로 이 등록을 구현했습니다 .

4. 스프링 시큐리티 설정하기

일반 Spring 애플리케이션과 마찬가지로 Jersey에 대한 Security을 구성할 수 있습니다.

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/login")
            .permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .oauth2Login()
            .loginPage("/login");
        return http.build();
    }
}

주어진 체인에서 가장 중요한 메소드는 oauth2Login 입니다. 이 방법 은 OAuth 2.0 공급자를 사용하여 인증 지원을 구성합니다. 이 사용방법(예제)에서 공급자는 GitHub입니다.

또 다른 눈에 띄는 구성은 로그인 페이지입니다. "/login" 문자열 loginPage 메소드에 제공함으로써 인증되지 않은 요청을 /login 엔드포인트 로 리디렉션하도록 Spring에 알립니다 .

기본 Security 구성은 /login 에서 자동 생성된 페이Map 제공합니다 . 따라서 로그인 페이지를 구성하지 않은 경우에도 인증되지 않은 요청은 여전히 ​​해당 엔드포인트로 리디렉션됩니다.

기본 구성과 명시적 설정의 차이점은 기본 경우 애플리케이션이 사용자 지정 문자열이 아닌 생성된 페이지를 반환한다는 것입니다.

5. 애플리케이션 구성

OAuth2로 보호되는 애플리케이션을 사용하려면 OAuth2 공급자에 클라이언트를 등록해야 합니다. 그런 다음 클라이언트의 자격 증명을 애플리케이션에 추가합니다.

5.1. OAuth2 클라이언트 등록

GitHub 앱을 등록 하여 등록 프로세스를 시작하겠습니다 . GitHub 개발자 페이지에 방문한 후 새 OAuth 앱 버튼을 눌러 새 OAuth 신청서 등록 양식을 엽니다.

그런 다음 표시된 양식을 적절한 값으로 채우십시오. 애플리케이션 이름에 앱을 인식할 수 있도록 하는 문자열을 입력합니다. 홈페이지 URL은 http://localhost:8083, 인증 콜백 URL은 http://localhost:8083/login/oauth2/code/github 입니다.

콜백 URL은 사용자가 GitHub에서 인증하고 애플리케이션에 대한 액세스 권한을 부여한 후 브라우저가 리디렉션하는 경로입니다.

등록 양식은 다음과 같습니다.

 

이제 신청 등록 버튼을 클릭합니다. 그런 다음 브라우저는 클라이언트 ID와 클라이언트 암호를 표시하는 GitHub 앱의 홈페이지로 리디렉션되어야 합니다.

5.2. 스프링 부트 애플리케이션 구성

jersey-application.properties 라는 속성 파일 을 클래스 경로에 추가해 보겠습니다.

server.port=8083
spring.security.oauth2.client.registration.github.client-id=<your-client-id>
spring.security.oauth2.client.registration.github.client-secret=<your-client-secret>

자리 표시자 <your-client-id><your-client-secret> 를 자체 GitHub 애플리케이션의 값 으로 바꾸는 것을 잊지 마십시오.

마지막으로 이 파일을 Spring Boot 애플리케이션에 속성 소스로 추가합니다.

@SpringBootApplication
@PropertySource("classpath:jersey-application.properties")
public class JerseyApplication {
    public static void main(String[] args) {
        SpringApplication.run(JerseyApplication.class, args);
    }
}

6. 실제 인증

GitHub에 등록한 후 애플리케이션에 로그인하는 방법을 살펴보겠습니다.

6.1. 애플리케이션 액세스

응용 프로그램을 시작한 다음 localhost:8083 주소의 홈페이지에 액세스해 보겠습니다 . 요청이 인증되지 않았으므로 로그인 페이지 로 리디렉션됩니다 .

 

이제 GitHub 링크를 클릭하면 브라우저가 GitHub 인증 페이지로 리디렉션됩니다.

 

URL을 보면 리디렉션된 요청에 response_type , client_idscope 와 같은 많은 쿼리 매개변수가 포함되어 있음을 알 수 있습니다 .

https://github.com/login/oauth/authorize?response_type=code&client_id=c30a16c45a9640771af5&scope=read:user&state=dpTme3pB87wA7AZ--XfVRWSkuHD3WIc9Pvn17yeqw38%3D&redirect_uri=http://localhost:8083/login/oauth2/code/github

response_type 의 값 code 이며, 이는 OAuth2 부여 유형이 인증 코드임을 의미합니다. 한편 client_id 매개변수는 애플리케이션을 식별하는 데 도움이 됩니다. 모든 매개변수의 의미는 GitHub 개발자 페이지 로 이동하십시오 .

승인 페이지가 나타나면 계속하려면 애플리케이션을 승인해야 합니다. 승인이 성공하면 브라우저는 몇 가지 쿼리 매개변수와 함께 애플리케이션의 사전 정의된 엔드포인트로 리디렉션됩니다.

http://localhost:8083/login/oauth2/code/github?code=561d99681feeb5d2edd7&state=dpTme3pB87wA7AZ--XfVRWSkuHD3WIc9Pvn17yeqw38%3D

그런 다음 배후에서 애플리케이션은 액세스 토큰에 대한 인증 코드를 교환합니다. 그런 다음 이 토큰을 사용하여 로그인한 사용자에 대한 정보를 가져옵니다.

localhost:8083/login/oauth2/code/github 에 대한 요청이 반환된 후 브라우저는 홈페이지로 돌아갑니다. 이번에 는 우리 고유의 사용자 이름이 포함된 인사말 메시지가 표시되어야 합니다 .

 

6.2. 사용자 이름을 얻는 방법?

인사말 메시지의 사용자 이름이 GitHub 사용자 이름임이 분명합니다. 이 시점에서 질문이 생길 수 있습니다. 인증된 사용자로부터 사용자 이름과 기타 정보를 어떻게 얻을 수 있습니까?

이 예에서는 로그인 속성에서 사용자 이름을 추출했습니다. 그러나 이는 모든 OAuth2 제공업체에서 동일하지 않습니다. 즉, 공급자는 자체 재량에 따라 특정 속성의 데이터를 제공할 수 있습니다. 따라서 우리는 이와 관련하여 단순히 표준이 없다고 말할 수 있습니다.

GitHub의 경우 참조 문서 에서 필요한 속성을 찾을 수 있습니다 . 마찬가지로 다른 OAuth2 공급자도 자체 참조를 제공합니다.

또 다른 솔루션은 디버그 모드에서 애플리케이션을 시작하고 OAuth2AuthenticatedPrincipal 객체가 생성된 후 중단점을 설정할 수 있다는 것입니다. 이 개체의 모든 속성을 살펴볼 때 사용자 정보에 대한 통찰력을 갖게 됩니다.

7. 테스트

애플리케이션의 동작을 확인하기 위해 몇 가지 테스트를 작성해 보겠습니다.

7.1. 환경 설정

다음은 테스트 메서드를 담을 클래스입니다.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@TestPropertySource(properties = "spring.security.oauth2.client.registration.github.client-id:test-id")
public class JerseyResourceUnitTest {
    @Autowired
    private TestRestTemplate restTemplate;

    @LocalServerPort
    private int port;

    private String basePath;

    @Before
    public void setup() {
        basePath = "http://localhost:" + port + "/";
    }

    // test methods
}

실제 GitHub 클라이언트 ID를 사용하는 대신 OAuth2 클라이언트에 대한 테스트 ID를 정의했습니다. 그런 다음 이 ID는 spring.security.oauth2.client.registration.github.client-id 속성으로 설정됩니다.

이 테스트 클래스의 모든 어노테이션은 Spring Boot 테스트에서 일반적이므로 이 사용방법(예제)에서는 다루지 않습니다. 이러한 어노테이션 중 하나라도 명확하지 않은 경우 Spring Boot 에서 테스트, Spring 에서 통합 테스트 또는 Spring Boot TestRestTemplate 탐색으로 이동하십시오 .

7.2. 홈 페이지

인증되지 않은 사용자가 홈 페이지에 액세스하려고 하면 인증 을 위해 로그인 페이지로 리디렉션됩니다.

@Test
public void whenUserIsUnauthenticated_thenTheyAreRedirectedToLoginPage() {
    ResponseEntity<Object> response = restTemplate.getForEntity(basePath, Object.class);
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FOUND);
    assertThat(response.getBody()).isNull();

    URI redirectLocation = response.getHeaders().getLocation();
    assertThat(redirectLocation).isNotNull();
    assertThat(redirectLocation.toString()).isEqualTo(basePath + "login");
}

7.3. 로그인 페이지

로그인 페이지에 액세스하면 인증 경로가 반환 되는지 확인합니다 .

@Test
public void whenUserAttemptsToLogin_thenAuthorizationPathIsReturned() {
    ResponseEntity response = restTemplate.getForEntity(basePath + "login", String.class);
    assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML);
    assertThat(response.getBody()).isEqualTo("Log in with <a href="\"/oauth2/authorization/github\"">GitHub</a>");
}

7.4. 권한 부여 끝점

마지막으로 권한 부여 끝점에 요청을 보낼 때 브라우저는 적절한 매개변수를 사용하여 OAuth2 공급자의 권한 부여 페이지로 리디렉션합니다.

@Test
public void whenUserAccessesAuthorizationEndpoint_thenTheyAresRedirectedToProvider() {
    ResponseEntity response = restTemplate.getForEntity(basePath + "oauth2/authorization/github", String.class);
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FOUND);
    assertThat(response.getBody()).isNull();

    URI redirectLocation = response.getHeaders().getLocation();
    assertThat(redirectLocation).isNotNull();
    assertThat(redirectLocation.getHost()).isEqualTo("github.com");
    assertThat(redirectLocation.getPath()).isEqualTo("/login/oauth/authorize");

    String redirectionQuery = redirectLocation.getQuery();
    assertThat(redirectionQuery.contains("response_type=code"));
    assertThat(redirectionQuery.contains("client_id=test-id"));
    assertThat(redirectionQuery.contains("scope=read:user"));
}

8. 결론

이 예제에서는 Jersey 애플리케이션으로 Spring 소셜 로그인을 설정했습니다. 이 사용방법(예제)에는 GitHub OAuth2 공급자에 애플리케이션을 등록하는 단계도 포함되어 있습니다.

전체 소스 코드는 GitHub 에서 찾을 수 있습니다 .

Security footer banner