1. 개요

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

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

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

2. Maven 의존성

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 컨테이너에 Jersey 등록

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

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

등록함으로써 JerseyResource을 A의 ResourceConfig의 서브 클래스, 우리는 자원 클래스의 모든 엔드 포인트의 서블릿 컨테이너를 알렸다.

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

4. 스프링 Security 설정

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

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
          .authorizeRequests()
          .antMatchers("/login")
          .permitAll()
          .anyRequest()
          .authenticated()
          .and()
          .oauth2Login()
          .loginPage("/login");
    }
}

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

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

기본 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로 인증하고 애플리케이션에 대한 액세스 권한을 부여한 후 브라우저가 리디렉션하는 경로입니다.

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

 

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

5.2. Spring Boot 애플리케이션 구성

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_id , scope 같은 많은 쿼리 매개 변수를 전달했음을 알 수 있습니다 .

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 테스트에서 공통적이므로이 예제에서는 다루지 않습니다. 이러한 어노테이션 중 하나라도 명확하지 않은 경우에는 Testing in Spring Boot , Integration Testing in Spring 또는 Exploring the 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 GitHub");
}

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 Social Login을 설정했습니다. 이 사용방법(예제)에는 GitHub OAuth2 공급자에 애플리케이션을 등록하는 단계도 포함되어 있습니다.

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