請注意,此內容已過時,並且使用了舊版本的 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. 身份令牌 (Id Token)
在深入瞭解實現細節之前,我們先快速瞭解一下 OpenID 的工作原理以及我們如何與之交互。此時,當然需要已經對 OAuth2 有一定的理解,因為 OpenID 是建立在 OAuth 之上的。
為了使用身份功能,我們將使用一個新的 OAuth2 範圍,即 openid。 這將導致我們的 Access Token 中多出一個字段 – id_token。
id_token 是一個 JWT (JSON Web Token),其中包含由身份提供者 (在本例中為 Google) 簽名,關於用户身份的信息。
server(授權碼) 和 implicit 流程是獲取 id_token 的最常見方式,在本例中,我們將使用 server flow。
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 獲取 OAuth 2.0 憑據。
- 我們使用 openid 範圍獲取 id_token。
- 我們還使用額外的範圍 email 以將用户電子郵件包含在 id_token 的身份信息中。
- 重定向 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 令牌
在上面的示例中,我們使用了 <em >decodeAndVerify()</em> 方法來自 <em >JwtHelper</em> 中提取信息,同時也對其進行驗證。
此過程的第一步是驗證其是否使用 Google Discovery 文檔中指定的證書進行簽名。
這些證書每天大約會發生一次變化,因此我們將使用名為 <a href="https://mvnrepository.com/search?q=jwks" target="_blank" rel="noopener noreferrer">jwks-rsa</a> 的實用工具庫來讀取它們。
<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();
}
}請注意:
- 我們添加了自定義的 OpenIdConnectFilter,位於 OAuth2ClientContextFilter 之後
- 我們使用了簡單的安全配置,將用户重定向到 “google-login” 以供 Google 進行身份驗證
6. 用户控制器
以下是一個簡單的控制器,用於測試我們的應用程序:
@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接下來,我們將替換 Access Token 和 <em id_token 的 <em code:
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"
}<p>最後,這是實際 <em id_token</em> 的信息:</p>
{
"iss":"accounts.google.com",
"at_hash":"AccessTokenHash",
"sub":"12345678",
"email_verified":true,
"email":"[email protected]",
...
}你可以立即看到,令牌內部的用户信息對於向我們的應用程序提供身份信息有多麼有用。
8. 結論
在本次快速入門教程中,我們學習瞭如何使用 Google 的 OpenID Connect 實現對用户進行身份驗證。