Social Login with Spring Security in a Jersey Application – 泽西岛应用程序中使用Spring安全的社交登录

最后修改: 2020年 9月 17日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Security is a first-class citizen in the Spring ecosystem. Therefore, it’s not surprising that OAuth2 can work with Spring Web MVC with almost no configuration.

在Spring生态系统中,安全是一流的公民。因此,OAuth2几乎不需要配置就能与Spring Web MVC一起工作,这并不令人惊讶。

However, a native Spring solution isn’t the only way to implement the presentation layer. Jersey, a JAX-RS compliant implementation, can also work in tandem with Spring OAuth2.

然而,本地 Spring 解决方案并不是实现呈现层的唯一方法。Jersey,一个符合 JAX-RS 的实现,也可以与 Spring OAuth2 协同工作。

In this tutorial, we’ll find out how to protect a Jersey application with Spring Social Login, which is implemented using the OAuth2 standard.

在本教程中,我们将了解如何使用Spring Social Login来保护Jersey应用程序,这是用OAuth2标准实现的。

2. Maven Dependencies

2.Maven的依赖性

Let’s add the spring-boot-starter-jersey artifact to integrate Jersey into a Spring Boot application:

让我们添加spring-boot-starter-jersey工件,将Jersey集成到Spring Boot应用程序中。

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

To configure Security OAuth2, we need spring-boot-starter-security and spring-security-oauth2-client:

要配置安全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>

We’ll manage all these dependencies using the Spring Boot Starter Parent version 2.

我们将使用Spring Boot Starter Parent version 2.管理所有这些依赖关系。

3. Jersey Presentation Layer

3.泽西岛表现层

We’ll need a resource class with a couple of endpoints to use Jersey as the presentation layer.

我们需要一个有几个端点的资源类来使用Jersey作为展示层。

3.1. Resource Class

3.1.资源类

Here’s the class that contains endpoint definitions:

这里是包含端点定义的类。

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

The class itself is very simple – it has just a @Path annotation. The value of this annotation identifies the base path for all endpoints in the class’s body.

该类本身非常简单,它只有一个@Path注解。这个注解的值确定了该类主体中所有端点的基本路径

It may be worth mentioning that this resource class doesn’t carry a stereotype annotation for component scanning. In fact, it doesn’t even need to be a Spring bean. The reason is that we don’t rely on Spring to handle the request mapping.

值得一提的是,这个资源类并没有携带组件扫描的定型注解。事实上,它甚至不需要是一个Spring Bean。原因是我们并不依赖Spring来处理请求映射。

3.2. Login Page

3.2.登录页面

Here’s the method that handles login requests:

这里是处理登录请求的方法。

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

This method returns a string for GET requests that target the /login endpoint. The text/html content type instructs the user’s browser to display the response with a clickable link.

该方法为针对/login端点的GET请求返回一个字符串。text/html内容类型指示用户的浏览器用一个可点击的链接来显示响应。

We’ll use GitHub as the OAuth2 provider, hence the link /oauth2/authorization/github. This link will trigger a redirection to the GitHub authorize page.

我们将使用GitHub作为OAuth2的提供者,因此链接/oauth2/authorization/github这个链接将触发一个重定向到GitHub授权页面。

3.3. Home Page

3.3.主页

Let’s define another method to handle requests to the root path:

让我们定义另一个方法来处理对根路径的请求。

@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;
}

This method returns the home page, which is a string containing the logged-in username. Notice, in this case, we extracted the username from the login attribute. Another OAuth2 provider may use a different attribute for the username, though.

这个方法返回主页,它是一个包含登录的用户名的字符串。注意,在这种情况下,我们从login属性中提取了用户名。不过,另一个OAuth2提供者可能会使用不同的属性作为用户名。

Obviously, the above method works for authenticated requests only. If a request is unauthenticated, it’ll be redirected to the login endpoint. We’ll see how to configure this redirection in section 4.

很明显,上述方法只适用于经过认证的请求。如果一个请求是未经认证的,它将被重定向到login端点。我们将在第4节看到如何配置这种重定向。

3.4. Registering Jersey with the Spring Container

3.4.在Spring容器中注册泽西岛

Let’s register the resource class with a servlet container to enable Jersey services. Fortunately, it’s pretty simple:

让我们在Servlet容器中注册资源类,以启用Jersey服务。幸运的是,这非常简单。

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

By registering JerseyResource in a ResourceConfig subclass, we informed the servlet container of all the endpoints in that resource class.

通过在ResourceConfig子类中注册JerseyResource,我们将该资源类中的所有端点告知servlet容器。

The last step is to register the ResourceConfig subclass, which is RestConfig in this case, with the Spring container. We implemented this registration with the @Component annotation.

最后一步是在Spring容器中注册ResourceConfig子类,这里是RestConfig我们用@Component注解来实现这一注册。

4. Configuring Spring Security

4.配置Spring安全

We can configure security for Jersey just like we would for a normal Spring application:

我们可以为Jersey配置安全,就像为普通的Spring应用配置一样。

@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();
    }
}

The most important method in the given chain is oauth2Login. This method configures authentication support using an OAuth 2.0 provider. In this tutorial, the provider is GitHub.

给定链中最重要的方法是oauth2Login。该方法使用OAuth 2.0提供者配置认证支持。在本教程中,提供者是GitHub。

Another noticeable configuration is the login page. By providing string “/login” to the loginPage method, we tell Spring to redirect unauthenticated requests to the /login endpoint.

另一个值得注意的配置是登录页面。通过向loginPage方法提供字符串“/login”,我们告诉Spring 将未经认证的请求重定向到/login端点。

Note that the default security configuration also provides an auto-generated page at /login. Therefore, even if we didn’t configure the login page, an unauthenticated request would still be redirected to that endpoint.

请注意,默认的安全配置也提供了一个自动生成的页面/login。因此,即使我们没有配置登录页面,一个未经认证的请求仍然会被重定向到该端点。

The difference between the default configuration and the explicit setting is that in the default case, the application returns the generated page rather than our custom string.

默认配置和明确设置之间的区别是,在默认情况下,应用程序返回生成的页面,而不是我们的自定义字符串。

5. Application Configuration

5.应用配置

In order to have an OAuth2-protected application, we’ll need to register a client with an OAuth2 provider. After that, add the client’s credentials to the application.

为了拥有一个受OAuth2保护的应用程序,我们需要在一个OAuth2提供者那里注册一个客户端。之后,将客户端的凭证添加到应用程序中。

5.1. Registering OAuth2 Client

5.1.注册OAuth2客户端

Let’s start the registration process by registering a GitHub app. After landing on the GitHub developer page, hit the New OAuth App button to open the Register a new OAuth application form.

让我们从注册一个GitHub应用程序开始注册过程。登陆GitHub开发者页面后,点击新OAuth应用程序按钮,打开注册新OAuth应用程序表单。

Next, fill out the displayed form with appropriate values. For the application name, enter any string that makes the app recognizable. The homepage URL can be http://localhost:8083, and the authorization callback URL is http://localhost:8083/login/oauth2/code/github.

接下来,在显示的表格中填入适当的值。对于应用程序的名称,输入任何使应用程序可识别的字符串。主页URL可以是http://localhost:8083,,授权回调URL是http://localhost:8083/login/oauth2/code/github

The callback URL is the path to which the browser redirects after the user authenticates with GitHub and grants access to the application.

回调URL是用户通过GitHub认证并授予应用程序访问权后浏览器重定向的路径。

This is how the registration form may look like:

这就是注册表的样子。

 

Now, click on the Register application button. The browser should then redirect to the GitHub app’s homepage, which shows up the client ID and client secret.

现在,点击注册应用程序按钮。然后浏览器应该重定向到GitHub应用程序的主页,显示出客户端ID和客户端秘密。

5.2. Configuring Spring Boot Application

5.2.配置Spring Boot应用程序

Let’s add a properties file, named jersey-application.properties, to the classpath:

让我们在classpath中添加一个名为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>

Remember to replace the placeholders <your-client-id> and <your-client-secret> with values from our own GitHub application.

记得将占位符<your-client-id><your-client-secret>替换为我们自己GitHub应用程序的值。

Lastly, add this file as a property source to a Spring Boot application:

最后,把这个文件作为一个属性源添加到Spring Boot应用程序中。

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

6. Authentication in Action

6.行动中的认证

Let’s see how we can log in to our application after registering with GitHub.

让我们看看在GitHub注册后如何登录我们的应用程序。

6.1. Accessing the Application

6.1.访问该应用程序

Let’s start the application, then access the homepage at the address localhost:8083. Since the request is unauthenticated, we’ll be redirected to the login page:

让我们启动应用程序,然后访问地址为localhost:8083的主页。由于该请求未经认证,我们将被重定向到login页面。

 

Now, when we hit the GitHub link, the browser will redirect to the GitHub authorize page:

现在,当我们点击GitHub的链接时,浏览器会重定向到GitHub的授权页面。

 

By looking at the URL, we can see that the redirected request carried many query parameters, such as response_type, client_id, and scope:

通过查看URL,我们可以看到重定向的请求带有许多查询参数,如response_typeclient_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

The value of response_type is code, meaning the OAuth2 grant type is authorization code.  Meanwhile, the client_id parameter helps identifies our application. For the meanings of all the parameters, please head over to the GitHub Developer page.

response_type的值是code,意味着OAuth2的授予类型是授权代码。 同时,client_id参数有助于识别我们的应用程序。关于所有参数的含义,请前往GitHub开发者页面

When the authorize page shows up, we need to authorize the application to continue. After the authorization is successful, the browser will redirect to a predefined endpoint in our application, together with a few query parameters:

当授权页面出现时,我们需要授权该应用程序继续进行。授权成功后,浏览器将重定向到我们应用程序中的一个预定义的端点,同时还有一些查询参数。

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

Behind the scenes, the application will then exchange the authorization code for an access token. Afterward, it uses this token to get information on the logged-in user.

在幕后,应用程序将用授权码交换一个访问令牌。之后,它使用这个令牌来获取登录用户的信息。

After the request to localhost:8083/login/oauth2/code/github returns, the browser goes back to the homepage. This time, we should see a greeting message with our own username:

在对localhost:8083/login/oauth2/code/github的请求返回后,浏览器回到了主页。这一次,我们应该看到一个带有我们自己用户名的问候信息

 

6.2. How to Obtain the Username?

6.2.如何获得用户名?

It’s clear that the username in the greeting message is our GitHub username. At this point, a question may arise: how can we get the username and other information from an authenticated user?

很明显,问候语中的用户名就是我们的GitHub用户名。这时,可能会出现一个问题:我们怎样才能从一个已认证的用户那里获得用户名和其他信息呢?

In our example, we extracted the username from the login attribute. However, this isn’t the same across all OAuth2 providers. In other words, a provider may provide data in certain attributes at its own discretion. Therefore, we can say there’re simply no standards in this regard.

在我们的例子中,我们从login属性中提取了用户名。然而,这并不是所有的OAuth2提供商都是这样的。换句话说,提供商可以自行决定在某些属性中提供数据。因此,我们可以说在这方面根本没有标准。

In the case of GitHub, we can find which attributes we need in the reference documentation. Likewise, other OAuth2 providers provide their own references.

就GitHub而言,我们可以在参考文档中找到我们需要的属性。同样地,其他OAuth2提供商也提供了他们自己的参考资料。

Another solution is that we can launch the application in the debug mode and set a breakpoint after an OAuth2AuthenticatedPrincipal object is created. When going through all attributes of this object, we’ll have insight into the user’s information.

另一个解决方案是,我们可以在调试模式下启动应用程序,并在OAuth2AuthenticatedPrincipal对象创建后设置一个断点。当浏览这个对象的所有属性时,我们将深入了解用户的信息。

7. Testing

7.测试

Let’s write a few tests to verify the application’s behavior.

让我们写几个测试来验证该应用程序的行为。

7.1. Setting Up Environment

7.1.设置环境

Here’s the class that will hold our test methods:

这里是将容纳我们的测试方法的类。

@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
}

Instead of using the real GitHub client ID, we defined a test ID for the OAuth2 client. This ID is then set to the spring.security.oauth2.client.registration.github.client-id property.

我们没有使用真正的GitHub客户端ID,而是为OAuth2客户端定义了一个测试ID。这个ID然后被设置为spring.security.oauth2.client.registration.github.client-id属性。

All annotations in this test class are common in Spring Boot testing, hence we won’t cover them in this tutorial. In case any of these annotations are unclear, please head over to Testing in Spring Boot, Integration Testing in Spring, or Exploring the Spring Boot TestRestTemplate.

这个测试类中的所有注解都是Spring Boot测试中常见的,因此我们不会在本教程中介绍它们。如果这些注解中有任何不清楚的地方,请前往Spring Boot中的测试Spring中的集成测试,或探索Spring Boot TestRestTemplate

7.2. Home Page

7.2.主页

We’ll prove that when an unauthenticated user attempts to access the home page, they’ll be redirected to the login page for authentication:

我们将证明,当一个未认证的用户试图访问主页时,他们将被重定向到登录页面进行认证:

@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. Login Page

7.3.登录页面

Let’s verify that accessing the login page will lead to the authorization path being returned:

让我们验证一下,访问登录页面将导致授权路径被返回:

@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. Authorization Endpoint

7.4.授权端点

Finally, when sending a request to the authorization endpoint, the browser will redirect to the OAuth2 provider’s authorize page with appropriate parameters:

最后,当向授权端点发送请求时,浏览器将重定向到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. Conclusion

8.结语

In this tutorial, we have set up Spring Social Login with a Jersey application. The tutorial also included steps for registering an application with the GitHub OAuth2 provider.

在本教程中,我们已经用Jersey应用程序设置了Spring Social Login。该教程还包括向GitHub OAuth2提供商注册应用程序的步骤。

The complete source code can be found over on GitHub.

完整的源代码可以在GitHub上找到over