Spring Security and OpenID Connect (Legacy) – Spring安全和OpenID连接(遗留问题)

最后修改: 2019年 12月 29日

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

Note that this content is outdated and using the legacy OAuth stack. Take a look at Spring Security’s latest OAuth support.

请注意,该内容已经过时,并且使用了传统的OAuth协议栈。请看Spring Security最新的OAuth支持

1. Overview

1.概述

In this quick tutorial, we’ll focus on setting up OpenID Connect with a Spring Security OAuth2 implementation.

在这个快速教程中,我们将专注于用Spring Security OAuth2实现来设置OpenID Connect。

OpenID Connect is a simple identity layer built on top of the OAuth 2.0 protocol.

OpenID Connect是一个建立在OAuth 2.0协议之上的简单身份层。

And, more specifically, we’ll learn how to authenticate users using the OpenID Connect implementation from Google.

而且,更具体地说,我们将学习如何使用OpenID Connect实现来验证用户。

2. Maven Configuration

2.Maven配置

First, we need to add the following dependencies to our Spring Boot application:

首先,我们需要在我们的Spring Boot应用程序中添加以下依赖项。

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

3. The Id Token

3.身份令牌

Before we dive into the implementation details, let’s have a quick look at how OpenID works, and how we’ll interact with it.

在我们深入了解实施细节之前,让我们先看看OpenID是如何工作的,以及我们将如何与它互动。

At this point, it’s, of course, important to already have an understanding of OAuth2, since OpenID is built on top of OAuth.

在这一点上,当然,重要的是已经对OAuth2有了了解,因为OpenID是建立在OAuth之上的。

First, in order to use the identity functionality, we’ll make use of a new OAuth2 scope called openid. This will result in an extra field in our Access Token – “id_token“.

首先,为了使用身份功能,我们将利用一个新的OAuth2范围,称为openid这将导致我们的访问令牌中出现一个额外的字段 – “id_token“。

The id_token is a JWT (JSON Web Token) that contains identity information about the user, signed by the identity provider (in our case Google).

id_token是一个JWT(JSON网络令牌),包含用户的身份信息,由身份提供者(在我们的例子中是谷歌)签署。

Finally, both server(Authorization Code) and implicit flows are the most commonly used ways of obtaining id_token, in our example, we will use server flow.

最后,server(Authorization Code)implicit流都是获得id_token的最常用方式,在我们的例子中,我们将使用server流

3. OAuth2 Client Configuration

3.OAuth2客户端配置

Next, let’s configure our OAuth2 client – as follows:

接下来,让我们来配置我们的OAuth2客户端–如下所示。

@Configuration
@EnableOAuth2Client
public class GoogleOpenIdConnectConfig {
    @Value("${google.clientId}")
    private String clientId;

    @Value("${google.clientSecret}")
    private String clientSecret;

    @Value("${google.accessTokenUri}")
    private String accessTokenUri;

    @Value("${google.userAuthorizationUri}")
    private String userAuthorizationUri;

    @Value("${google.redirectUri}")
    private String redirectUri;

    @Bean
    public OAuth2ProtectedResourceDetails googleOpenId() {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setClientId(clientId);
        details.setClientSecret(clientSecret);
        details.setAccessTokenUri(accessTokenUri);
        details.setUserAuthorizationUri(userAuthorizationUri);
        details.setScope(Arrays.asList("openid", "email"));
        details.setPreEstablishedRedirectUri(redirectUri);
        details.setUseCurrentUri(false);
        return details;
    }

    @Bean
    public OAuth2RestTemplate googleOpenIdTemplate(OAuth2ClientContext clientContext) {
        return new OAuth2RestTemplate(googleOpenId(), clientContext);
    }
}

And here is application.properties:

而这里是application.properties

google.clientId=<your app clientId>
google.clientSecret=<your app clientSecret>
google.accessTokenUri=https://www.googleapis.com/oauth2/v3/token
google.userAuthorizationUri=https://accounts.google.com/o/oauth2/auth
google.redirectUri=http://localhost:8081/google-login

Note that:

请注意,。

  • You first need to obtain OAuth 2.0 credentials for your Google web app from Google Developers Console.
  • We used scope openid to obtain id_token.
  • we also used an extra scope email to include user email in id_token identity information.
  • The redirect URI http://localhost:8081/google-login is the same one used in our Google web app.

4. Custom OpenID Connect Filter

4.自定义OpenID连接过滤器

Now, we need to create our own custom OpenIdConnectFilter to extract authentication from id_token – as follows:

现在,我们需要创建我们自己的自定义OpenIdConnectFilter,从id_token中提取认证 – 如下所示。

public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {

    public OpenIdConnectFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
        setAuthenticationManager(new NoopAuthenticationManager());
    }
    @Override
    public Authentication attemptAuthentication(
      HttpServletRequest request, HttpServletResponse response) 
      throws AuthenticationException, IOException, ServletException {
        OAuth2AccessToken accessToken;
        try {
            accessToken = restTemplate.getAccessToken();
        } catch (OAuth2Exception e) {
            throw new BadCredentialsException("Could not obtain access token", e);
        }
        try {
            String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
            String kid = JwtHelper.headers(idToken).get("kid");
            Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier(kid));
            Map<String, String> authInfo = new ObjectMapper()
              .readValue(tokenDecoded.getClaims(), Map.class);
            verifyClaims(authInfo);
            OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
            return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        } catch (InvalidTokenException e) {
            throw new BadCredentialsException("Could not obtain user details from token", e);
        }
    }
}

And here is our simple OpenIdConnectUserDetails:

这里是我们简单的OpenIdConnectUserDetails

public class OpenIdConnectUserDetails implements UserDetails {
    private String userId;
    private String username;
    private OAuth2AccessToken token;

    public OpenIdConnectUserDetails(Map<String, String> userInfo, OAuth2AccessToken token) {
        this.userId = userInfo.get("sub");
        this.username = userInfo.get("email");
        this.token = token;
    }
}

Note that:

请注意,。

  • Spring Security JwtHelper to decode id_token.
  • id_token always contains “sub” field which is a unique identifier for the user.
  • id_token will also contain “email” field as we added email scope to our request.

4.1. Verifying the ID Token

4.1.验证ID令牌

In the example above, we used the decodeAndVerify() method of JwtHelper to extract information from the id_token, but also to validate it.

在上面的例子中,我们使用了JwtHelperdecodeAndVerify()方法,从id_token中提取信息,但也验证了它。

The first step for this is verifying that it was signed with one of the certificates specified in the Google Discovery document.

这方面的第一步是验证它是用Google Discovery文件中指定的一个证书签署的。

These change about once per day, so we’ll use a utility library called jwks-rsa to read them:

这些内容大约每天变化一次,所以我们将使用一个名为jwks-rsa的实用库来读取它们。

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>jwks-rsa</artifactId>
    <version>0.3.0</version>
</dependency>

Let’s add the URL that contains the certificates to the application.properties file:

让我们在application.properties文件中添加包含证书的URL。

google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs

Now we can read this property and build the RSAVerifier object:

现在我们可以读取这个属性并建立RSAVerifier对象。

@Value("${google.jwkUrl}")
private String jwkUrl;    

private RsaVerifier verifier(String kid) throws Exception {
    JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
    Jwk jwk = provider.get(kid);
    return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}

Finally, we’ll also verify the claims in the decoded id token:

最后,我们还将验证解码后的id token中的说法。

public void verifyClaims(Map claims) {
    int exp = (int) claims.get("exp");
    Date expireDate = new Date(exp * 1000L);
    Date now = new Date();
    if (expireDate.before(now) || !claims.get("iss").equals(issuer) || 
      !claims.get("aud").equals(clientId)) {
        throw new RuntimeException("Invalid claims");
    }
}

The verifyClaims() method is checking that the id token was issued by Google and that it’s not expired.

verifyClaims()方法正在检查id token是否由谷歌签发,并且没有过期。

You can find more information on this in the Google documentation.

你可以在Google文档中找到更多相关信息。

5. Security Configuration

5.安全配置

Next, let’s discuss our security configuration:

接下来,让我们讨论一下我们的安全配置。

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private OAuth2RestTemplate restTemplate;

    @Bean
    public OpenIdConnectFilter openIdConnectFilter() {
        OpenIdConnectFilter filter = new OpenIdConnectFilter("/google-login");
        filter.setRestTemplate(restTemplate);
        return filter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
        .addFilterAfter(new OAuth2ClientContextFilter(), 
          AbstractPreAuthenticatedProcessingFilter.class)
        .addFilterAfter(OpenIdConnectFilter(), 
          OAuth2ClientContextFilter.class)
        .httpBasic()
        .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/google-login"))
        .and()
        .authorizeRequests()
        .anyRequest().authenticated();
        return http.build();
    }
}

Note that:

请注意,。

  • We added our custom OpenIdConnectFilter after OAuth2ClientContextFilter
  • We used a simple security configuration to redirect users to “/google-login” to get authenticated by Google

6. User Controller

6.用户控制器

Next, here is a simple controller to test our app:

接下来,这里有一个简单的控制器来测试我们的应用程序。

@Controller
public class HomeController {
    @RequestMapping("/")
    @ResponseBody
    public String home() {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        return "Welcome, " + username;
    }
}

Sample response (after redirect to Google to approve app authorities) :

响应样本(在重定向到谷歌批准应用程序授权后)。

Welcome, example@gmail.com

7. Sample OpenID Connect Process

7.OpenID连接过程示例

Finally, let’s take a look at a sample OpenID Connect authentication process.

最后,让我们看一下OpenID Connect认证过程的样本。

First, we’re going to send an Authentication Request:

首先,我们要发送一个认证请求

https://accounts.google.com/o/oauth2/auth?
    client_id=sampleClientID
    response_type=code&
    scope=openid%20email&
    redirect_uri=http://localhost:8081/google-login&
    state=abc

The response (after user approval) is a redirect to:

响应(用户批准后)是一个重定向到。

http://localhost:8081/google-login?state=abc&code=xyz

Next, we’re going to exchange the code for an Access Token and id_token:

接下来,我们要把代码换成访问令牌和id_token

POST https://www.googleapis.com/oauth2/v3/token 
    code=xyz&
    client_id= sampleClientID&
    client_secret= sampleClientSecret&
    redirect_uri=http://localhost:8081/google-login&
    grant_type=authorization_code

Here’s a sample Response:

这里有一个回复样本。

{
    "access_token": "SampleAccessToken",
    "id_token": "SampleIdToken",
    "token_type": "bearer",
    "expires_in": 3600,
    "refresh_token": "SampleRefreshToken"
}

Finally, here’s what the information of the actual id_token looks like:

最后,这里是实际id_token的信息。

{
    "iss":"accounts.google.com",
    "at_hash":"AccessTokenHash",
    "sub":"12345678",
    "email_verified":true,
    "email":"example@gmail.com",
     ...
}

So you can immediately see just how useful the user information inside the token is for providing identity information to our own application.

因此,你可以立即看到令牌内的用户信息对于向我们自己的应用程序提供身份信息是多么有用。

8. Conclusion

8.结论

In this quick intro tutorial, we learned how to authenticate users using the OpenID Connect implementation from Google.

在这个快速入门教程中,我们学习了如何使用谷歌的OpenID Connect实现来验证用户。

And, as always, you can find the source code over on GitHub.

而且,像往常一样,你可以在GitHub上找到源代码