Spring Security 與 OpenID Connect (舊版)

Spring Security
Remote
0
06:33 AM · Nov 30 ,2025

注意:此內容已過時,使用遺留 OAuth 堆棧。請查看 Spring Security 的最新 OAuth 支持。

注意:此內容已過時,使用遺留 OAuth 堆棧。請查看 Spring Security 的最新 OAuth 支持。

1. 概述

在本快速教程中,我們將重點介紹使用 Spring Security OAuth2 實現 OpenID Connect 的設置。

OpenID Connect 是建立在 OAuth 2.0 協議之上的一個簡單身份層。

並且,更具體地説,我們將學習如何使用 來自 Google 的 OpenID Connect 實現 來驗證用户。

2. Maven 配置

首先,我們需要將以下依賴項添加到我們的 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. 身份令牌在深入探討實現細節之前,我們先快速瞭解一下 OpenID 的工作原理以及我們如何與之交互。

此時,當然需要已經對 OAuth2 有一定的理解,因為 OpenID 是建立在 OAuth 之上的。

首先,為了使用身份功能,我們將使用一個新的 OAuth2 範圍,即 openid這將導致我們的 Access Token 中多出一個字段 – id_token

id_token 是一個 JWT(JSON Web Token),其中包含由身份提供者(在本例中為 Google)簽名的一方身份信息。

最後,server(授權碼)implicit 流是獲取 id_token 的最常見方式,在本例中,我們將使用 server 流

3. OAuth2 客户端配置

接下來,讓我們配置我們的 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);
    }
}

以下是 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

注意:

  • 您首先需要從 Google Developers Console 獲取 Google Web 應用程序的 OAuth 2.0 憑據。
  • 我們使用了 scope openid 來獲取 id_token
  • 我們還使用了額外的 scope email 來在 id_token 身份信息中包含用户電子郵件。
  • redirect URI http://localhost:8081/google-login 與我們在 Google Web 應用程序中使用的相同。

4. 自定義OpenID Connect過濾器

現在,我們需要創建一個自定義的 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);
        }
    }
}

以下是我們的簡單 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;
    }
}

注意:

  • Spring Security JwtHelper 用於解碼 id_token。
  • id_token 始終包含 “sub” 字段,它是用户的唯一標識符。
  • id_token 也會包含 “email” 字段,因為我們添加了 email 範圍到我們的請求。
  • 4.1. 驗證ID令牌

    在上面的示例中,我們使用了 decodeAndVerify() 方法(來自 JwtHelper)來從 id_token 中提取信息,但同時也對其進行驗證。

    對該令牌進行驗證的第一步是確認它是由 Google 簽名,這可以通過 Google Discovery 文檔中的證書列表進行驗證。

    這些證書會大約每天更新一次,因此我們將使用一個名為 jwks-rsa 的實用庫來讀取它們:

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

    讓我們將包含證書的 URL 添加到 application.properties 文件中:

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

    現在我們可以讀取此屬性並構建 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());
    }

    最後,我們還會驗證解碼後的 id 令牌中的聲明:

    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");
        }
    }

    verifyClaims() 方法正在檢查 id 令牌是否由 Google 簽發,並且它尚未過期。

    您可以在 Google 文檔 中找到更多信息。

    5. 安全配置

    接下來,我們討論一下我們的安全配置:

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

    請注意:

    • 我們添加了自定義的 OpenIdConnectFilterOAuth2ClientContextFilter 之後
    • 我們使用了簡單的安全配置,將用户重定向到 “/google-login” 以通過 Google 進行身份驗證

    6. User Controller

    接下來,這裏是一個簡單的控制器,用於測試我們的應用:

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

    示例響應 (在 Google 批准應用權限後):

    Welcome, [email protected]

    7. 樣本 OpenID Connect 流程

    最後,讓我們來查看一個樣本 OpenID Connect 身份驗證流程。

    首先,我們將發送一個 身份驗證請求:

    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

    響應(在用户批准後)是重定向到:

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

    接下來,我們將用 code 交換為訪問令牌和 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

    這是一個樣本響應:

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

    最後,以下是實際 id_token 的外觀:

    {
        "iss":"accounts.google.com",
        "at_hash":"AccessTokenHash",
        "sub":"12345678",
        "email_verified":true,
        "email":"[email protected]",
         ...
    }

    因此,您可以看到令牌中包含的用户信息對於向我們的應用程序提供身份信息有多麼有用。

    8. 結論

    在本快速入門教程中,我們學習瞭如何使用谷歌提供的 OpenID Connect 實現來認證用户。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.