JWT 签名与验签 公钥与私钥生成 使用 JDK 提供的 keytool
工具生成 JKS 密钥库 (Java Key Store),认证授权服务器会使用私钥对 Token 进行签名,一般将生成的 shop.jks
文件放在 resources
目录下
1 keytool -genkey -alias shop -keyalg RSA -keypass 123456 -keystore shop.jks -storepass 123456
根据私钥生成公钥,将其保存在 public.crt
文件中,用于对 Token 进行验签,一般将其放 resources
目录下
1 keytool -list -rfc --keystore shop.jks | openssl x509 -inform pem -pubkey -noout
1 2 3 4 5 6 7 8 9 -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtXKXj3JGNJNWVXg4+++4 FtNTJre+8kHLdPLwHJJcRw4aV7oMMjI1nesyj75w/kjRZImhbNo0poEu1jj+sDO9 UbLUHSy59zoDDMZTYmbkboDEpkFq3ZUhAoLtt5DtAgI8DkOK22RlSxXpcMvkeL8X ziFizWf/HatSgAat/SfX+5dH3KX40piPv9kI5YVJz1GyD8xO4dN95tr0Ld7FDmdK JBPWfkM+CMlKRhYqB+sAlaQW5/L3xb3WNftucC/RhdKT8/mmgMsIBhUZOS/1iFnD KuPsEwU5xEQxK9pWX2bWsSkeOgQYJmQa6hiWBuujPUyOs4rICvniopxsW2yyPOFX ZQIDAQAB -----END PUBLIC KEY-----
认证授权服务器加载 JKS 秘钥库 认证授权服务器加载 JKS 秘钥库,从中获取密钥对(公钥 + 私钥),Java 示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 @Bean public KeyPair keyPair () { KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("shop.jks" ), "123456" .toCharArray()); KeyPair keyPair = factory.getKeyPair("shop" , "123456" .toCharArray()); return keyPair; }
认证授权服务器暴露获取公钥的接口 对外暴露 JWK Set URI 接口,让其他应用系统可以获取到公钥
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RestController @RequestMapping("/oauth") public class JwkSetController { @Autowired private KeyPair keyPair; @GetMapping("/.well-known/jwks.json") public Map<String, Object> publicKey () { RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); } }
或者通过 KeyPair
来获取公钥
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestController @RequestMapping("/oauth") public class PublicKeyController { @Autowired private KeyPair keyPair; @GetMapping("/publicKey") public String publicKey () { return Base64.encode(new String(keyPair.getPublic().getEncoded())); } }
或者直接使用 OAuth 2.0 内置的接口 /oauth/token_key
来获取公钥
1 2 $ curl --request GET 'http://127.0.0.1:8080/oauth/token_key
1 2 3 4 { "alg": "SHA256withRSA", "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtXKXj3JGNJNWVXg4+++4FtNTJre+8kHLdPLwHJJcRw4aV7oMMjI1nesyj75w/kjRZImhbNo0poEu1jj+sDO9p8n5oYXn3qU8bsmqLa/vttq7Ubi4a5eaoP8ASjoD+dnQ0I7ZdpH/fiiHfriGI4tFziFizWf/HatSgAat/SfX+5dk3KX40piPv9kI5YVJz1GyD8xO4dN9dtr0Ld7FDmdKJBPWfkM+CMlKRhYqB+sAlaQW5/L3xb3WNftucC/RhdKT8/mmgMsIBhUZOS/1iFnDKaPsEwU5xEQxK9pWX2bWsSkeOgQYJmQa6hiWBuujPUyOs4rICvniopxsW2yyPOFXZQIDAQAB\n-----END PUBLIC KEY-----" }
资源服务器指定公钥文件的路径 在 YML 配置里指定认证授权服务器暴露的 JWK Set URI 接口,以此来获取公钥,值得一提的是,默认情况下 jwk-set-uri
指定的 URL 无法使用 Ribbon 来实现负载均衡访问(除非利用 DNS 的域名解析,即单个域名绑定多个 IP,通过 DNS 服务器做负载均衡)
1 2 3 4 5 6 7 8 spring: application: name: gateway-server security: oauth2: resourceserver: jwt: jwk-set-uri: http://127.0.0.1:8080/oauth/.well-known/jwks.json
或者将上面通过 keytool
工具获取到的公钥拷贝到 src/main/resources/public.crt
文件中,然后在 YML 配置里指定公钥文件的路径
1 2 3 4 5 6 7 8 spring: application: name: gateway-server security: oauth2: resourceserver: jwt: public-key-location: classpath:public.crt
Cannot convert access token to JSON 错误 应用启动后,出现 Cannot convert access token to JSON
这个错误,主要是 OAuth 2.0 的资源服务器缺少了加载公钥的配置,解决方法如下:
1 2 3 4 5 6 7 8 9 @Bean public JwtAccessTokenConverter accessTokenConverter () { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); String publicKey = getPublicKey(); converter.setVerifier(new RsaVerifier(publicKey)); return converter; }
资源服务器加载公钥的完整示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.75</version > </dependency > <dependency > <groupId > xom</groupId > <artifactId > xom</artifactId > <version > 1.3.7</version > </dependency > <dependency > <groupId > org.jdom</groupId > <artifactId > jdom</artifactId > <version > 2.0.2</version > </dependency > <dependency > <groupId > net.sf.json-lib</groupId > <artifactId > json-lib</artifactId > <version > 2.1</version > <classifier > jdk15</classifier > </dependency > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > <version > 5.5.8</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 import cn.hutool.core.io.FileUtil;import cn.hutool.core.util.StrUtil;import com.alibaba.fastjson.JSONObject;import net.sf.json.xml.XMLSerializer;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;import org.springframework.core.io.Resource;import org.springframework.security.jwt.crypto.sign.RsaVerifier;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;import org.springframework.web.client.RestTemplate;import java.io.BufferedReader;import java.io.InputStreamReader;import java.util.stream.Collectors;@Configuration public class OAuthTokenConfig { @Value("${spring.security.oauth2.resourceserver.jwt.key-set-uri:}") private String keySetUri; private OAuth2ResourceServerProperties resourceServerProperties; private static final Logger logger = LoggerFactory.getLogger(OAuthTokenConfig.class); public OAuthTokenConfig (OAuth2ResourceServerProperties resourceServerProperties) { this .resourceServerProperties = resourceServerProperties; } @Bean public TokenStore tokenStore () { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter () { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); String publicKey = getPublicKey(); converter.setVerifier(new RsaVerifier(publicKey)); logger.info("success to load public key" ); return converter; } private String getPublicKey () { if (StrUtil.isBlank(keySetUri)) { return getKeyFromLocal(); } else { return getKeyFromAuthorizationServer(); } } private String getKeyFromAuthorizationServer () { try { XMLSerializer xmlSerializer = new XMLSerializer(); String xmlPubKey = new RestTemplate().getForObject(keySetUri, String.class); String jsonPubKey = xmlSerializer.read(xmlPubKey).toString(); JSONObject json = JSONObject.parseObject(jsonPubKey); return json.get("value" ).toString(); } catch (Exception e) { logger.error("failed to load public key from authorization server: {}" , e.getLocalizedMessage()); } return null ; } private String getKeyFromLocal () { Resource resource = getPublicKeyFile(); try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { return br.lines().collect(Collectors.joining("\n" )); } catch (Exception e) { logger.error("failed to load public key from local: {}" , e.getLocalizedMessage()); } return null ; } private Resource getPublicKeyFile () { try { Resource resource = resourceServerProperties.getJwt().getPublicKeyLocation(); if (FileUtil.exist(resource.getFile())) { return resource; } } catch (Exception e) { logger.error("failed to read public key file from local: {}" , e.getLocalizedMessage()); } return new ClassPathResource("public.crt" ); } }
1 2 3 4 5 6 7 8 9 spring: application: name: provider-service security: oauth2: resourceserver: jwt: key-set-uri: http://127.0.0.1:8080/oauth/token_key
特别注意:在上述代码中,若在 YML 文件里配置了从认证授权服务器获取公钥,那么必须使用 OAuth 2.0 内置的接口 /oauth/token_key
来获取公钥,同时使用的配置项是 key-set-uri
,而不再是 jwk-set-uri
OAuth 2.0 资源服务器 资源服务器鉴权配置 默认情况下,OAuth 2.0 的权限是从 Client 的 scope
中获取,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Configuration @EnableResourceServer public class OAuthResouceServer extends ResourceServerConfigurerAdapter { @Autowired private TokenStore tokenStore; @Override public void configure (ResourceServerSecurityConfigurer resources) { resources.resourceId("school" ) .tokenStore(tokenStore) .stateless(true ) .accessDeniedHandler(new CustomAccessDeniedHandler()); } @Override public void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/**" ).access("#oauth2.hasScope('teacher')" ) .and().csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }
若权限存在于 authorities
中,需要替代 OAuth2ResourceServerWebSecurityConfiguration
的配置,示例代码如下:
弃用方法安全 通过自定义 Converter 来指定权限,Converter 是函数接口,当前上下问参数为 JWT 对象 获取 JWT 中的 authorities
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwt -> { Collection<SimpleGrantedAuthority> authorities = ((Collection<String>) jwt.getClaims() .get("authorities" )).stream() .map(SimpleGrantedAuthority::new ) .collect(Collectors.toSet()); return new JwtAuthenticationToken(jwt, authorities); }); } }
参考博客