1. 概述
在本教程中,我們將討論如何使我們的 Spring Security OAuth2 實現利用 JSON Web Tokens。
我們還將繼續在 OAuth 教程系列中進行擴展。
在開始之前 – 一點重要説明。請注意,Spring Security 核心團隊正在實施新的 OAuth2 堆棧 – 其中一些方面已經實現,而另一些方面仍在進行中。
對於使用新版本的 Spring Security 5 堆棧的這篇文章,請參考我們的文章 使用 JWT 與 Spring Security OAuth。
現在,讓我們直接開始吧。
2. Maven 配置
首先,我們需要將 spring-security-jwt 依賴添加到我們的 pom.xml 中:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>請注意,我們需要將 spring-security-jwt 依賴項添加到 Authorization Server 和 Resource Server 兩個地方。
3. 授權服務器
接下來,我們將配置我們的授權服務器使用 JwtTokenStore,具體步驟如下:
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.authenticationManager(authenticationManager);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
}請注意,我們在 JwtAccessTokenConverter 中使用了對稱密鑰來對我們的令牌進行簽名——這意味着我們還需要在資源服務器上使用相同的確切密鑰。
4. 資源服務器
現在,讓我們來查看一下我們的資源服務器配置,它與授權服務器的配置非常相似:
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(tokenServices());
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}請注意,我們定義這兩個服務器為完全獨立且可獨立部署的。因此,我們需要在此新的配置中再次聲明一些相同的 Bean。
5. 自定義令牌中的自定義聲明
現在,我們將設置一些基礎設施,以便能夠向訪問令牌添加自定義聲明。 框架提供的標準聲明都很好,但大多數時候我們需要在令牌中包含額外的信息,以便在客户端進行利用。
我們將定義一個TokenEnhancer,用於自定義我們的訪問令牌,並添加這些額外聲明。
在以下示例中,我們將向我們的訪問令牌添加一個額外的字段“organization”,使用CustomTokenEnhancer:
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(
OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put(
"organization", authentication.getName() + randomAlphabetic(4));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(
additionalInfo);
return accessToken;
}
}然後,我們將將其連接到我們的 身份驗證服務器配置中,如下所示:
@Override
public void configure(
AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}使用新的配置後,以下是一個 token token 負載的示例:
{
"user_name": "john",
"scope": [
"foo",
"read",
"write"
],
"organization": "johnIiCh",
"exp": 1458126622,
"authorities": [
"ROLE_USER"
],
"jti": "e0ad1ef3-a8a5-4eef-998d-00b26bc2c53f",
"client_id": "fooClientIdPassword"
}5.1. 在 JS 客户端中使用訪問令牌
最後,我們需要在我們的 AngularJs 客户端應用程序中使用令牌信息。我們將使用 angular-jwt 庫來實現。
因此,我們將利用“”聲明,並在我們的 中使用:
<p class="navbar-text navbar-right">{{organization}}</p>
<script type="text/javascript"
src="https://cdn.rawgit.com/auth0/angular-jwt/master/dist/angular-jwt.js">
</script>
<script>
var app =
angular.module('myApp', ["ngResource","ngRoute", "ngCookies", "angular-jwt"]);
app.controller('mainCtrl', function($scope, $cookies, jwtHelper,...) {
$scope.organiztion = "";
function getOrganization(){
var token = $cookies.get("access_token");
var payload = jwtHelper.decodeToken(token);
$scope.organization = payload.organization;
}
...
});6. 訪問資源服務器上的額外聲明
但是,我們如何在資源服務器端訪問這些信息?
我們將在這裏執行的操作是 – 從訪問令牌中提取額外的聲明:
public Map<String, Object> getExtraInfo(OAuth2Authentication auth) {
OAuth2AuthenticationDetails details =
(OAuth2AuthenticationDetails) auth.getDetails();
OAuth2AccessToken accessToken = tokenStore
.readAccessToken(details.getTokenValue());
return accessToken.getAdditionalInformation();
}在下面的部分,我們將討論如何通過使用自定義的 AccessTokenConverter,將額外信息添加到我們的 Authentication 詳情中。
6.1. 自定義 AccessTokenConverter
讓我們創建一個 AccessTokenConverter,並使用訪問令牌聲明設置身份驗證詳情:
@Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication =
super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
}注意:DefaultAccessTokenConverter 用於將身份驗證詳情設置為 Null。
6.2. 配置 JwtTokenStore
接下來,我們將配置我們的 JwtTokenStore,使其使用我們的 CustomAccessTokenConverter。
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfigJwt
extends ResourceServerConfigurerAdapter {
@Autowired
private CustomAccessTokenConverter customAccessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setAccessTokenConverter(customAccessTokenConverter);
}
// ...
}6.3. 身份驗證對象中可用的額外聲明
現在,授權服務器在令牌中添加了額外的聲明,我們可以在資源服務器端,直接在身份驗證對象中訪問它們:
public Map<String, Object> getExtraInfo(Authentication auth) {
OAuth2AuthenticationDetails oauthDetails =
(OAuth2AuthenticationDetails) auth.getDetails();
return (Map<String, Object>) oauthDetails
.getDecodedDetails();
}6.4. 身份驗證詳情測試
讓我們確保我們的身份驗證對象包含額外信息:
@RunWith(SpringRunner.class)
@SpringBootTest(
classes = ResourceServerApplication.class,
webEnvironment = WebEnvironment.RANDOM_PORT)
public class AuthenticationClaimsIntegrationTest {
@Autowired
private JwtTokenStore tokenStore;
@Test
public void whenTokenDoesNotContainIssuer_thenSuccess() {
String tokenValue = obtainAccessToken("fooClientIdPassword", "john", "123");
OAuth2Authentication auth = tokenStore.readAuthentication(tokenValue);
Map<String, Object> details = (Map<String, Object>) auth.getDetails();
assertTrue(details.containsKey("organization"));
}
private String obtainAccessToken(
String clientId, String username, String password) {
Map<String, String> params = new HashMap<>();
params.put("grant_type", "password");
params.put("client_id", clientId);
params.put("username", username);
params.put("password", password);
Response response = RestAssured.given()
.auth().preemptive().basic(clientId, "secret")
.and().with().params(params).when()
.post("http://localhost:8081/spring-security-oauth-server/oauth/token");
return response.jsonPath().getString("access_token");
}
}注意:我們從授權服務器獲取了包含額外聲明的訪問令牌,然後從中讀取 身份驗證 對象,該對象包含“組織”信息在詳細信息對象中。
7. 非對稱密鑰對
在之前的配置中,我們使用對稱密鑰對我們的令牌進行簽名:
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}我們還可以使用非對稱密鑰(公鑰和私鑰)來進行簽名過程。
7.1. 生成 JKS Java 密鑰庫文件
首先,我們使用命令行工具 keytool 生成密鑰 – 更具體地説,生成 .jks 文件。
keytool -genkeypair -alias mytest
-keyalg RSA
-keypass mypass
-keystore mytest.jks
-storepass mypass該命令將生成一個名為 mytest.jks 的文件,其中包含我們的公鑰和私鑰。
請確保 keypass 和 storepass 相同。
7.2. 導出公鑰
接下來,我們需要從生成的 JKS 中導出公鑰。可以使用以下命令執行此操作:
keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey以下是一個示例響應將呈現如下:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1
czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2
MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV
BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj
Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM
urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX
eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj
iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn
WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD
VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3
1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0
yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp
/J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN
hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V
FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF
lLFCUGhA7hxn2xf3x1JW
-----END CERTIFICATE-----我們僅複製我們的公鑰,並將其複製到我們的 資源服務器,文件路徑為 src/main/resources/public.txt。
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----當然,請看以下翻譯:
或者,我們可以通過添加 -noout 參數僅導出公鑰:
keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey -noout7.3. Maven 配置
接下來,我們不想讓 JKS 文件被 Maven 過濾過程所拾取——因此,我們將確保在 pom.xml 中排除它:
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>*.jks</exclude>
</excludes>
</resource>
</resources>
</build>如果使用 Spring Boot,我們需要確保將我們的 JKS 文件通過 Spring Boot Maven 插件 – addResources 添加到應用程序類路徑:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>7.4. 授權服務器
現在,我們將配置 JwtAccessTokenConverter 使用我們從 mytest.jks 獲得的密鑰對,具體步驟如下:
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
return converter;
}7.5. 資源服務器
最後,我們需要配置資源服務器使用公鑰,具體步驟如下:
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}8. 結論
在本文中,我們重點介紹瞭如何設置我們的 Spring Security OAuth2 項目以使用 JSON Web Tokens。