1. 概要

このチュートリアルでは、SpringセキュリティOAuth2実装を取得してJSONWebトークンを利用する方法について説明します。

また、このOAuthシリーズの前の記事の上に構築を続けています。

 

始める前に–1つの重要な注意事項。 Spring Securityコアチームは新しいOAuth2スタックを実装している最中であり、いくつかの側面はすでに公開されており、いくつかはまだ進行中であることに注意してください。

新しいSpringSecurity5スタックを使用したこの記事のバージョンについては、記事 Spring SecurityOAuthでのJWTの使用をご覧ください。

さて、すぐに飛び込みましょう。

2. Maven構成

まず、spring-security-jwt依存関係をpom.xmlに追加する必要があります。

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
</dependency>

spring-security-jwt依存関係を承認サーバーとリソースサーバーの両方に追加する必要があることに注意してください。

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

これらの2つのサーバーは、完全に分離され、独立してデプロイ可能であると定義していることに注意してください。 これが、新しい構成で同じ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();
}

この新しい構成が稼働していると、トークントークンペイロードは次のようになります。

{
    "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クライアントでアクセストークンを使用する

最後に、AngualrJSクライアントアプリケーションでトークン情報を利用したいと思います。 そのためにangular-jwtライブラリを使用します。

したがって、これから行うことは、index.htmlの「organization」クレームを利用することです。

<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 を使用して、認証の詳細にその追加情報を追加する方法について説明します。

6.1. カスタムAccessTokenConverter

CustomAccessTokenConverter を作成し、アクセストークンクレームを使用して認証の詳細を設定しましょう。

@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を構成します

次に、CustomAccessTokenConverterを使用するようにJwtTokenStoreを構成します。

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

注:承認サーバーから追加のクレームを含むアクセストークンを取得し、そこから Authentication オブジェクトを読み取ります。このオブジェクトには、詳細オブジェクトに追加情報「組織」が含まれています。

7. 非対称キーペア

以前の構成では、対称鍵を使用してトークンに署名しました。

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("123");
    return converter;
}

非対称鍵(公開鍵と秘密鍵)を使用して署名プロセスを実行することもできます。

7.1. JKSJavaKeyStoreファイルを生成します

まず、コマンドラインツール keytool:を使用して、キー、より具体的には.jksファイルを生成しましょう。

keytool -genkeypair -alias mytest 
                    -keyalg RSA 
                    -keypass mypass 
                    -keystore mytest.jks 
                    -storepass mypass

このコマンドは、 mytest.jks というファイルを生成します。このファイルには、公開キーと秘密キーが含まれています。

また、keypassstorepassが同じであることを確認してください。

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 -noout

7.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. 承認サーバー

次に、 mytest.jksのKeyPairを使用するようにJwtAccessTokenConverterを構成します–次のようになります。

@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. 結論

このクイック記事では、JSONWebトークンを使用するようにSpringSecurityOAuth2プロジェクトを設定することに焦点を当てました。

このチュートリアルの完全実装は、 githubプロジェクトにあります。これはEclipseベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。