在本教程中,我们将讨论将 Spring Security OAuth2 与 JSON Web Token 整合。
我们将在 上一篇 OAuth 系列文章 的基础上开展。
首先,我们需要在 pom.xml 中添加 spring-security-jwt 依赖:
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> </dependency>
请注意,我们需要向授权服务器和资源服务器同时添加 spring-security-jwt 依赖。
接下来,我们将配置授权服务器以使用 JwtTokenStore ,如下所示:
@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfigextends 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 中使用了一个 对称密钥 来签署我们的令牌 — 这意味着我们还需要对资源服务器使用同样的密钥。
现在,让我们来看看资源服务器配置,它与授权服务器的配置非常相似:
@Configuration @EnableResourceServer public class OAuth2ResourceServerConfigextends 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 的原因。
现在我们来设置一些基础设施,以便能够在 Access Token 中添加一些自定义的 claims 。框架提供的标准 claims 很好,但在大多数情况下,我们需要向令牌中添加一些额外的信息才能在客户端使用。
我们定义一个 TokenEnhancer 来定制 Access Token 与这些额外的 claims。
在以下示例中,我们将使用此 CustomTokenEnhancer 向 Access Toekn 添加一个额外的字段 organization :
public class CustomTokenEnhancerimplements 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(); }
使用这个新配置来启动和运行,令牌的 payload 可能类似于:
{ "user_name": "john", "scope": [ "foo", "read", "write" ], "organization": "johnIiCh", "exp": 1458126622, "authorities": [ "ROLE_USER" ], "jti": "e0ad1ef3-a8a5-4eef-998d-00b26bc2c53f", "client_id": "fooClientIdPassword" }
最后,我们将在 AngualrJS 客户端应用中使用令牌信息。我们将使用 angular-jwt 库。
因此我们要做的是在 index.html
中使用 organization
claim:
<pclass="navbar-text navbar-right">{{organization}}</p> <scripttype="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; } ... });
在之前的配置中,我们使用了对称密钥来签署我们的令牌:
@Bean public JwtAccessTokenConverter accessTokenConverter(){ JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("123"); return converter; }
我们还可以使用非对称密钥(公钥和私钥)来完成签名过程。
让我们首先使用命令行工具 keytool 生成密钥 — 尤其是 .jks 文件。
keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass
该命令将生成一个名为 mytest.jks 的文件,其中包含我们的密钥 — 公钥和私钥。
此外,还要确保 keypass 和 storepass 是一致的。
接下来,我们需要从生成的 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-----
接下来,我们不希望 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 添加到应用程序 classpath 中:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <addResources>true</addResources> </configuration> </plugin> </plugins> </build>
现在,我们将配置 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; }
最后,我们需要将资源服务器配置为使用公钥,如下所示:
@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; }
在本文中,我们主要配置了 Spring Security OAuth2 项目以整合 JSON Web Token。
本教程的 完整实现 可以在 项目 github 中找到,这是一个基于 Eclipse 的项目,因此应该很容易导入和运行。