Spring Cloud OAuth2.0 微服务中配置 Jwt Token 签名/验证
关于 Jwt Token 的签名与安全性前面已经做了几篇介绍,在 IdentityServer4 中定义了 Jwt Token 与 Reference Token 两种验证方式(https://www.cnblogs.com/Irving/p/9357539.html),理论上 Spring Security OAuth 中也可以实现,在资源服务器使用 RSA 公钥(/oauth/token_key 获得公钥)验签或调用接口来验证(/oauth/check_token 缓存调用频率),思路是一样的,这篇主要说一下 Spring Security OAuth 中 Token 签名的相关实现 。
spring security oauth2 中的 endpoint(聊聊spring security oauth2的几个endpoint的认证)
- /oauth/authorize(授权端,授权码模式使用)
- /oauth/token(令牌端,获取 token)
- /oauth/check_token(资源服务器用来校验token)
- /oauth/confirm_access(用户发送确认授权)
- /oauth/error(认证失败)
- /oauth/token_key(如果使用JWT,可以获的公钥用于 token 的验签)
授权服务器配置 Token 签名
Token 可以使用对称加密算法进行签名,因此需要使用一个对称的 Key 值,用来参与签名计算,这个 Key 值存在于授权服务以及资源服务之中,并且资源服务需要验证这个签名。或者使用非对称加密算法来对 Token 进行签名,Public Key 公布在 /oauth/token_key 这个URL连接中,默认的访问安全规则是"denyAll()",即在默认的情况下它是关闭的,你可以注入一个标准的 SpEL 表达式到 AuthorizationServerSecurityConfigurer 这个配置中来将它开启(例如使用"permitAll()"来开启可能比较合适,因为它是一个公共密钥)。实现的方式主要是重写 AuthorizationServerConfigurerAdapter 的实现,签名算法可以配置对称加密方式(HS256)与非对称加密方式(RS256)两种签名的方式。
@Configuration
@EnableAuthorizationServer
//@Order(2)
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { //认证管理器
@Autowired
private AuthenticationManager authenticationManager; @Autowired
private BCryptPasswordEncoder passwordEncoder; /*
// redis
@Autowired
private RedisConnectionFactory connectionFactory; @Bean
public RedisTokenStore tokenStore() {
return new RedisTokenStore(connectionFactory);
}
*/ @Autowired
@Qualifier("dataSource")
private DataSource dataSource; // @Bean(name = "dataSource")
// @ConfigurationProperties(prefix = "spring.datasource")
// public DataSource dataSource() {
// return DataSourceBuilder.create().build();
// } /**
* 令牌存储
* @return Jdbc 令牌存储对象
*/
@Bean("jdbcTokenStore")
public JdbcTokenStore getJdbcTokenStore() {
return new JdbcTokenStore(dataSource);
} // @Bean
// public UserDetailsService userDetailsService(){
// return new UserService();
// } /*
* 配置客户端详情信息(内存或JDBC来实现)
*
* */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//初始化 Client 数据到 DB
// clients.jdbc(dataSource)
clients.inMemory()
.withClient("client_1")
.authorizedGrantTypes("client_credentials")
.scopes("all","read", "write")
.authorities("client_credentials")
.accessTokenValiditySeconds(7200)
.secret(passwordEncoder.encode("123456")) .and().withClient("client_2")
.authorizedGrantTypes("password", "refresh_token")
.scopes("all","read", "write")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(10000)
.authorities("password")
.secret(passwordEncoder.encode("123456")) .and().withClient("client_3").authorities("authorization_code","refresh_token")
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("authorization_code")
.scopes("all","read", "write")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(10000)
.redirectUris("http://localhost:8080/callback","http://localhost:8080/signin") .and().withClient("client_test")
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("all flow")
.authorizedGrantTypes("authorization_code", "client_credentials", "refresh_token","password", "implicit")
.redirectUris("http://localhost:8080/callback","http://localhost:8080/signin")
.scopes("all","read", "write")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(10000); //https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
// clients.withClientDetails(new JdbcClientDetailsService(dataSource));
} @Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(getJdbcTokenStore())
//.tokenStore(new RedisTokenStore(redisConnectionFactory))
.accessTokenConverter(jwtAccessTokenConverter())
//refresh_token 需要 UserDetailsService is required
//.userDetailsService(userDetailsService)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.authenticationManager(authenticationManager);
} @Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
//curl -i -X POST -H "Accept: application/json" -u "client_1:123456" http://localhost:5000/oauth/check_token?token=a1478d56-ebb8-4f21-b4b6-8a9602df24ec
oauthServer.tokenKeyAccess("permitAll()") //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
.checkTokenAccess("isAuthenticated()") //url:/oauth/check_token allow check token
.allowFormAuthenticationForClients();
} /**
* 定义token 签名的方式(非对称加密算法来对 Token 进行签名,也可以使用对称加密方式)
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
//对称加密方式
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("micosrv_signing_key");
return converter; // JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// KeyPair keyPair = new KeyStoreKeyFactory(
// new ClassPathResource("keystore.jks"), "foobar".toCharArray())
// .getKeyPair("test");
// converter.setKeyPair(keyPair);
// return converter;
}
}
上述在 JwtAccessTokenConverter 中使用对称密钥来签署我们的令牌,这意味着我们需要在资源服务器使用同样的密钥(micosrv_signing_key)。当然也可以使用非对称加密的方式,在授权服务端生成公钥和密钥,客户端使用获取到的公钥到服务器做签名验证, Google 了一番很多都是使用 keytool 生成 JKS 证书的方式去做,通过查看 JwtAccessTokenConverter 的源码了解到,最终赋值是 signer 与 verifierKey ,verifierKey 是对 publicKey 进行 Base64 编码后得到的一个字符串,本质还是读取证书里面的公钥与私钥。
public void setKeyPair(KeyPair keyPair) {
PrivateKey privateKey = keyPair.getPrivate();
Assert.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA ");
signer = new RsaSigner((RSAPrivateKey) privateKey);
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
verifier = new RsaVerifier(publicKey);
verifierKey = "-----BEGIN PUBLIC KEY-----\n" + new String(Base64.encode(publicKey.getEncoded()))
+ "\n-----END PUBLIC KEY-----";
}
当然也可以通过 openssl 来生成。查看源码可以发现setSigningKey 方法中通过字符是否包含 "-----BEGIN" 来判断是 RSA key 还是 MAC key 的。
/**
* Sets the JWT signing key. It can be either a simple MAC key or an RSA key. RSA keys
* should be in OpenSSH format, as produced by <tt>ssh-keygen</tt>.
*
* @param key the key to be used for signing JWTs.
*/
public void setSigningKey(String key) {
Assert.hasText(key);
key = key.trim(); this.signingKey = key; if (isPublic(key)) {
signer = new RsaSigner(key);
logger.info("Configured with RSA signing key");
}
else {
// Assume it's a MAC key
this.verifierKey = key;
signer = new MacSigner(key);
}
} /**
* @return true if the key has a public verifier
*/
private boolean isPublic(String key) {
return key.startsWith("-----BEGIN");
}
openssl 生成公钥私钥
[root@021rjsh00199s mnt]# openssl genrsa -out jwt.pem 2048
Generating RSA private key, 2048 bit long modulus
..........+++
.+++
e is 65537 (0x10001)
[root@021rjsh00199s mnt]# openssl rsa -in jwt.pem
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAm4irSNcR7CSSfXconxL4g4M4j34wTWdTv93ocMn4VmdB7rCB
U/BlxXtBUf/cgLIgQhQrAPszSZSmxiEXCOkGPr4aQBQuPgmNIR95Dhbzw/ZN0Bne
cAt3ZfkkDBHv8kH3kR/jYGTdwrxKeDgXGljNsTRhbjuASxPG/Z6gU1yRPCsgc2r8
NYnztWGcDWqaobqjG3/yzFmusoAboyV7asIpo4yk378LmonDNwxnOOTb2Peg5Pee
lwfOwJPbftK1VOOt18zA0cchw6dHUzq9NlB8clps/VdBap9BxU3/0YoFXRIc18ny
zrWo2BcY2KQqX//AJC3OAfrfDmo+BGK8E0mp8wIDAQABAoIBAENp64P45GXMPEpx
eYPpfxnRqJRZh6olHSHOl087243n16YTjxrI2fPMxrU6B2Mo0d6SS0lzl/lOmzLJ
aOiNyA0t7MbVeG2fSjKPJ7M5s5K+kV+fttAtyCTE5iDtLWl9ukaG4dEIJy6e2lBd
T3Y2A4HJSGm1FJh2DAwl0ywOtUy0X6ki9DgXVAaCGDuoU25Rhun64dh802DZbEEJ
LdorIyeJ0ovCZyNvhlZRYkAOPy3k88smYl2jE/AbZ7pCKz/XggDcjNsERm2llaa3
pNTAZQUlHu0BQrCn6J9BxtMPyduiyrE+JYqTwnYhWQ5QRe/2J8O3t0eIK9TfUQpJ
DrZf00ECgYEAy/sLX8UCmERwMuaQSwoM0BHTZIc0iAsgiXbVOLua9I3Tu/mXOVdH
TikjdoWLqM62bA9dN/oqzHDwvqCy6zwamjFVSmJUejf5v+52Qj64leOmDX/RC4ne
L08N1nP/Y4X24Y/5zq18qvVlhOMDdydzayJFrGhkQKhJg58pRUIdenECgYEAwzLC
Awr3LeUlHa+d2O6siJVmljTc8lT+qX4TvqTDH8rAC/EyKMNaTjaX6mWosZZ7qYXv
EMxvQzTEzUHRXrCGlhbX8xiBlWnvpghF2GJEvP9WaU/+OCr0gItRSLPDuZ6ctzKb
3QkBEiC8ODyPRKzlA67D23S3KJB067IUV81h9KMCgYBXUqmT3is2NFYz9DBhb3P8
vyTYLGl4tArBznWJTAcSGoVCO59ZlNuZwlLEMnePVK8To6AsjpQz4UWu1ezCd4CL
8gKpTV8M01m/qL5HrcInqMU1kjpTzjmn1xf9brsuR/NgrNoseGieZ1+GfAjHwcPP
YWSiYi5I38JY7pIkbCFigQKBgAnVtty8YrPXRcV3IbbaX6sKC/8pbrBvA926Unha
iNJDPuXbIzHWleg26/SNZrB76oMiEmeARWLXd8r3s/rXXhCV2g+PfofurHprFEnQ
ubHkE5B+zUo7L9KCMng9RnFFwpOgYyYB3CHzsEgNFRLauzcySP/3o3rRvHJbqJa7
7GGNAoGBAKSBn4zq0iNWI2BUBb90icMsHEneiydGtFcEl3/Sz8vmjFZn0sjRbGoY
gmP9LlQ+o7xRiJ/LTesi5BA6zCGrcdp0aeyJzCRbFc3WqjGeyLbfx1sJVVB6PnvS
iKvvCOJq6kl3/opO+ybqJ8dzkEyoj8K4+fcX1+U6eW2w+vSpOosG
-----END RSA PRIVATE KEY-----
[root@021rjsh00199s mnt]# openssl rsa -in jwt.pem -pubout
writing RSA key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm4irSNcR7CSSfXconxL4
g4M4j34wTWdTv93ocMn4VmdB7rCBU/BlxXtBUf/cgLIgQhQrAPszSZSmxiEXCOkG
Pr4aQBQuPgmNIR95Dhbzw/ZN0BnecAt3ZfkkDBHv8kH3kR/jYGTdwrxKeDgXGljN
sTRhbjuASxPG/Z6gU1yRPCsgc2r8NYnztWGcDWqaobqjG3/yzFmusoAboyV7asIp
o4yk378LmonDNwxnOOTb2Peg5PeelwfOwJPbftK1VOOt18zA0cchw6dHUzq9NlB8
clps/VdBap9BxU3/0YoFXRIc18nyzrWo2BcY2KQqX//AJC3OAfrfDmo+BGK8E0mp
8wIDAQAB
-----END PUBLIC KEY-----
[root@021rjsh00199s mnt]#
application.yml
config:
oauth2:
# openssl genrsa -out jwt.pem 2048
# openssl rsa -in jwt.pem
privateKey: |
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAm4irSNcR7CSSfXconxL4g4M4j34wTWdTv93ocMn4VmdB7rCB
U/BlxXtBUf/cgLIgQhQrAPszSZSmxiEXCOkGPr4aQBQuPgmNIR95Dhbzw/ZN0Bne
cAt3ZfkkDBHv8kH3kR/jYGTdwrxKeDgXGljNsTRhbjuASxPG/Z6gU1yRPCsgc2r8
NYnztWGcDWqaobqjG3/yzFmusoAboyV7asIpo4yk378LmonDNwxnOOTb2Peg5Pee
lwfOwJPbftK1VOOt18zA0cchw6dHUzq9NlB8clps/VdBap9BxU3/0YoFXRIc18ny
zrWo2BcY2KQqX//AJC3OAfrfDmo+BGK8E0mp8wIDAQABAoIBAENp64P45GXMPEpx
eYPpfxnRqJRZh6olHSHOl087243n16YTjxrI2fPMxrU6B2Mo0d6SS0lzl/lOmzLJ
aOiNyA0t7MbVeG2fSjKPJ7M5s5K+kV+fttAtyCTE5iDtLWl9ukaG4dEIJy6e2lBd
T3Y2A4HJSGm1FJh2DAwl0ywOtUy0X6ki9DgXVAaCGDuoU25Rhun64dh802DZbEEJ
LdorIyeJ0ovCZyNvhlZRYkAOPy3k88smYl2jE/AbZ7pCKz/XggDcjNsERm2llaa3
pNTAZQUlHu0BQrCn6J9BxtMPyduiyrE+JYqTwnYhWQ5QRe/2J8O3t0eIK9TfUQpJ
DrZf00ECgYEAy/sLX8UCmERwMuaQSwoM0BHTZIc0iAsgiXbVOLua9I3Tu/mXOVdH
TikjdoWLqM62bA9dN/oqzHDwvqCy6zwamjFVSmJUejf5v+52Qj64leOmDX/RC4ne
L08N1nP/Y4X24Y/5zq18qvVlhOMDdydzayJFrGhkQKhJg58pRUIdenECgYEAwzLC
Awr3LeUlHa+d2O6siJVmljTc8lT+qX4TvqTDH8rAC/EyKMNaTjaX6mWosZZ7qYXv
EMxvQzTEzUHRXrCGlhbX8xiBlWnvpghF2GJEvP9WaU/+OCr0gItRSLPDuZ6ctzKb
3QkBEiC8ODyPRKzlA67D23S3KJB067IUV81h9KMCgYBXUqmT3is2NFYz9DBhb3P8
vyTYLGl4tArBznWJTAcSGoVCO59ZlNuZwlLEMnePVK8To6AsjpQz4UWu1ezCd4CL
8gKpTV8M01m/qL5HrcInqMU1kjpTzjmn1xf9brsuR/NgrNoseGieZ1+GfAjHwcPP
YWSiYi5I38JY7pIkbCFigQKBgAnVtty8YrPXRcV3IbbaX6sKC/8pbrBvA926Unha
iNJDPuXbIzHWleg26/SNZrB76oMiEmeARWLXd8r3s/rXXhCV2g+PfofurHprFEnQ
ubHkE5B+zUo7L9KCMng9RnFFwpOgYyYB3CHzsEgNFRLauzcySP/3o3rRvHJbqJa7
7GGNAoGBAKSBn4zq0iNWI2BUBb90icMsHEneiydGtFcEl3/Sz8vmjFZn0sjRbGoY
gmP9LlQ+o7xRiJ/LTesi5BA6zCGrcdp0aeyJzCRbFc3WqjGeyLbfx1sJVVB6PnvS
iKvvCOJq6kl3/opO+ybqJ8dzkEyoj8K4+fcX1+U6eW2w+vSpOosG
-----END RSA PRIVATE KEY-----
# openssl rsa -in jwt.pem -pubout
publicKey: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm4irSNcR7CSSfXconxL4
g4M4j34wTWdTv93ocMn4VmdB7rCBU/BlxXtBUf/cgLIgQhQrAPszSZSmxiEXCOkG
Pr4aQBQuPgmNIR95Dhbzw/ZN0BnecAt3ZfkkDBHv8kH3kR/jYGTdwrxKeDgXGljN
sTRhbjuASxPG/Z6gU1yRPCsgc2r8NYnztWGcDWqaobqjG3/yzFmusoAboyV7asIp
o4yk378LmonDNwxnOOTb2Peg5PeelwfOwJPbftK1VOOt18zA0cchw6dHUzq9NlB8
clps/VdBap9BxU3/0YoFXRIc18nyzrWo2BcY2KQqX//AJC3OAfrfDmo+BGK8E0mp
8wIDAQAB
-----END PUBLIC KEY-----
AuthorizationServerConfiguration
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { private final Logger logger = LoggerFactory.getLogger(this.getClass()); //RSA配置
@Value("${config.oauth2.privateKey}")
private String privateKey ;
@Value("${config.oauth2.publicKey}")
private String publicKey;
... /**
* 定义token 签名的方式(非对称加密算法来对 Token 进行签名,也可以使用对称加密方式)
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//converter.setSigningKey("micosrv_signing_key");
logger.info("jwtAccessTokenConverter privateKey :" + privateKey);
converter.setSigningKey(privateKey);
converter.setVerifierKey(publicKey);
return converter; // JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// KeyPair keyPair = new KeyStoreKeyFactory(
// new ClassPathResource("mytest.jks"), "mypasss".toCharArray())
// .getKeyPair("mytest");
// converter.setKeyPair(keyPair);
// return converter;
}
}
报文
POST http://localhost:5000/oauth/token HTTP/1.1
Authorization: Basic Y2xpZW50XzE6MTIzNDU2
cache-control: no-cache
Postman-Token: bc7e9113-fde5-4f29-8b98-ba256d94c8d2
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: localhost:5000
content-type: application/x-www-form-urlencoded
accept-encoding: gzip, deflate
content-length: 39
Connection: keep-alive grant_type=client_credentials&scope=all HTTP/1.1 200
Cache-Control: no-store
Pragma: no-cache
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Date: Mon, 06 Aug 2018 08:06:23 GMT
Content-Length: 684 {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhbGwiXSwiZXhwIjoxNTMzNTQ5OTgzLCJhdXRob3JpdGllcyI6WyJjbGllbnRfY3JlZGVudGlhbHMiXSwianRpIjoiYjE0MzE4MWEtNzhlMi00MWNlLWI1MWYtMjY0OWE1MjQxMDg4IiwiY2xpZW50X2lkIjoiY2xpZW50XzEifQ.eIWdOMs1vJW8PVYOU3c6d4qqqdDm4OVsBOs4PGI_P_13yi4Ldst5I7Gk5BG5L16xHVDJ_g7lSet5WkUVm6pj6J1fHrzDQTr2Ni74901lewWNG2UonQUX0Bry1lObHolWKr5zDOds7E1fTFOkCVCMrS_8PNgN569rQlZhqAmV0J287XYb_7WVs4CRn1B9GrgdlSQX42Pryo1KJ5dMewIGKA9WAt_9-lKxOl1wvawJ9M1UQGXfn2xbgHhLiwb9-K61v0uV3kBC0J0gvV-b4hBzcHboOvf2Gy-o7rz0Pnuew5vltnFeIWdbptGTTpqVGbphXJoM2KZpoNy0xqpPNW9Q0g","token_type":"bearer","expires_in":7199,"scope":"all","jti":"b143181a-78e2-41ce-b51f-2649a5241088"}
上述 access_token 就是一个 RS256 签名的 Jwt Token, 可以在 https://jwt.io/ 使用公钥进行验签。
备注:keytool 是一个Java 数据证书的管理工具,对应 .NET 有 makecert 相应的工具。上述也可以使用 keytool 来生成密钥文件
生成JKS文件(包含公钥和私钥):keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass
导出公钥 :keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey
自定义额外信息
Payload 是 JWT 存储信息的主体,有时候需要额外的信息加到 Token 返回中,可以自定义一个 TokenEnhancer
public class TokenEnhancerConfiguration implements TokenEnhancer { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("client_name", authentication.getName());
additionalInfo.put("ext_name", "irving");
// User user = (User) authentication.getUserAuthentication().getPrincipal();
// additionalInfo.put("username", user.getUsername());
// additionalInfo.put("authorities", user.getAuthorities());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
最后把这个 TokenEnhancer 加入到 TokenEnhancer 链中
@Bean
public TokenEnhancer tokenEnhancer() {
return new TokenEnhancerConfiguration();
} @Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter())); endpoints.tokenStore(getJdbcTokenStore())
//.tokenStore(new RedisTokenStore(redisConnectionFactory))
.tokenEnhancer(tokenEnhancerChain)
.accessTokenConverter(jwtAccessTokenConverter())
//refresh_token 需要 UserDetailsService is required
//.userDetailsService(userDetailsService)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.authenticationManager(authenticationManager);
}
报文(/oauth/token)
POST http://localhost:5000/oauth/token HTTP/1.1
Authorization: Basic Y2xpZW50XzE6MTIzNDU2
cache-control: no-cache
Postman-Token: 36abf6fa-a60e-4537-9584-df8d2b256be8
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: localhost:5000
cookie: JSESSIONID=61E921C362A386DD340B695E9C8FD6B5
content-type: application/x-www-form-urlencoded
accept-encoding: gzip, deflate
content-length: 39
Connection: keep-alive grant_type=client_credentials&scope=all HTTP/1.1 200
Set-Cookie: JSESSIONID=58B603BA4BECFA193249EC5098B83C4C; Path=/; HttpOnly
Cache-Control: no-store
Pragma: no-cache
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Date: Mon, 06 Aug 2018 09:48:18 GMT
Content-Length: 791 {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhbGwiXSwib3JnYW5pemF0aW9uIjoiY2xpZW50XzEiLCJleHRfbmFtZSI6ImlydmluZyIsImV4cCI6MTUzMzU1MzcwNSwiYXV0aG9yaXRpZXMiOlsiY2xpZW50X2NyZWRlbnRpYWxzIl0sImp0aSI6IjNiOGU5ZTliLTg2NWYtNDU4OS05YmE2LWM2OTQ4MDRiZmUwMSIsImNsaWVudF9pZCI6ImNsaWVudF8xIn0.JFhZ0KJzQtUxMYnGjPryC_pAkKFMgg9u1fHqOLVlGhhP_8Tx-OVcsiNQSVl_-ZkHg0lTsBikr_Gtoun2fHKug7KPhLoKNvimbFdvbZjbp2SAT1TrccGNr6EZ8i1LJUjXzeroXVjLvgr2W6vwEwPaKA4M5oamujtqG86wsRDLmuFfDiWDSbUl41AH4wKJ3whPJixNPyETZes_vUeRa0tXgazRkKiP8o8SSqt39RaLGanbPqI5-2V8O_SoVQ-eFcmZxK7OkPtp-kciF1ZEKvs0nDe3RNGEo3l7KYmCSC7vuFhBD8ChmT-Kvaj-leNOMVDaNM8ob6VkYuLWY75or_Onbw","token_type":"bearer","expires_in":4806,"scope":"all","organization":"client_1","ext_name":"irving","jti":"3b8e9e9b-865f-4589-9ba6-c694804bfe01"}
报文(/oauth/token_key)
GET http://localhost:5000/oauth/token_key HTTP/1.1
Authorization: Basic Y2xpZW50XzE6MTIzNDU2
cache-control: no-cache
Postman-Token: ae6b2774-77bd-4d6b-b266-1d5dc1347edc
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: localhost:5000
cookie: JSESSIONID=C2FE8C3C08EAE46C65B51E3BAFD740FC
accept-encoding: gzip, deflate
Connection: keep-alive HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Date: Mon, 06 Aug 2018 12:05:57 GMT
Content-Length: 494 {"alg":"SHA256withRSA","value":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm4irSNcR7CSSfXconxL4\ng4M4j34wTWdTv93ocMn4VmdB7rCBU/BlxXtBUf/cgLIgQhQrAPszSZSmxiEXCOkG\nPr4aQBQuPgmNIR95Dhbzw/ZN0BnecAt3ZfkkDBHv8kH3kR/jYGTdwrxKeDgXGljN\nsTRhbjuASxPG/Z6gU1yRPCsgc2r8NYnztWGcDWqaobqjG3/yzFmusoAboyV7asIp\no4yk378LmonDNwxnOOTb2Peg5PeelwfOwJPbftK1VOOt18zA0cchw6dHUzq9NlB8\nclps/VdBap9BxU3/0YoFXRIc18nyzrWo2BcY2KQqX//AJC3OAfrfDmo+BGK8E0mp\n8wIDAQAB\n-----END PUBLIC KEY-----\n"}
报文(/oauth/check_token)
POST http://localhost:5000/oauth/check_token HTTP/1.1
Authorization: Basic Y2xpZW50XzE6MTIzNDU2
cache-control: no-cache
Postman-Token: e6834301-2063-4e41-b3ae-02b121f9b946
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: localhost:5000
cookie: JSESSIONID=58B603BA4BECFA193249EC5098B83C4C
accept-encoding: gzip, deflate
content-type: multipart/form-data; boundary=--------------------------981103212895481753436742
content-length: 787
Connection: keep-alive ----------------------------981103212895481753436742
Content-Disposition: form-data; name="token" eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhbGwiXSwib3JnYW5pemF0aW9uIjoiY2xpZW50XzEiLCJleHRfbmFtZSI6ImlydmluZyIsImV4cCI6MTUzMzU1MzcwNSwiYXV0aG9yaXRpZXMiOlsiY2xpZW50X2NyZWRlbnRpYWxzIl0sImp0aSI6IjNiOGU5ZTliLTg2NWYtNDU4OS05YmE2LWM2OTQ4MDRiZmUwMSIsImNsaWVudF9pZCI6ImNsaWVudF8xIn0.JFhZ0KJzQtUxMYnGjPryC_pAkKFMgg9u1fHqOLVlGhhP_8Tx-OVcsiNQSVl_-ZkHg0lTsBikr_Gtoun2fHKug7KPhLoKNvimbFdvbZjbp2SAT1TrccGNr6EZ8i1LJUjXzeroXVjLvgr2W6vwEwPaKA4M5oamujtqG86wsRDLmuFfDiWDSbUl41AH4wKJ3whPJixNPyETZes_vUeRa0tXgazRkKiP8o8SSqt39RaLGanbPqI5-2V8O_SoVQ-eFcmZxK7OkPtp-kciF1ZEKvs0nDe3RNGEo3l7KYmCSC7vuFhBD8ChmT-Kvaj-leNOMVDaNM8ob6VkYuLWY75or_Onbw
----------------------------981103212895481753436742-- HTTP/1.1 200
Set-Cookie: JSESSIONID=E7E593CD4A4505AA4457325668F67D56; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Date: Mon, 06 Aug 2018 09:49:14 GMT
Content-Length: 199 {"scope":["all"],"organization":"client_1","active":true,"ext_name":"irving","exp":1533553705,"authorities":["client_credentials"],"jti":"3b8e9e9b-865f-4589-9ba6-c694804bfe01","client_id":"client_1"}
Token 验签
使用上述的公钥,单元测试代码如下
public class TestJwtToken {
@Test
public void testJwt() {
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhbGwiXSwiZXh0X25hbWUiOiJpcnZpbmciLCJleHAiOjE1MzM1NjQwMzQsImNsaWVudF9uYW1lIjoiY2xpZW50XzEiLCJhdXRob3JpdGllcyI6WyJjbGllbnRfY3JlZGVudGlhbHMiXSwianRpIjoiMzgyMTJjNzktMDdmOS00ZGIzLTg0ZDUtNWIwNzY2ZTA4M2Y5IiwiY2xpZW50X2lkIjoiY2xpZW50XzEifQ.LzDGv2YvWWyK9x4Ks88PjvYNVzjOu3Ofce8ipWv9sUdqzRHA1vX0kYltw4tDh6sSCuSDMXXLZVnq6VvHunQpLm2B51hm33C0HX31UqpYKOqM_QKeQabRWZlSrVy5CSS3wpXlF032eM2WIKwnFnFNajVoegCF_ddWuqiyuvlu7gpbsYsQTfSev8HIrRN7xmFL6UKX-FAB---MMBIaLeURCYEmPe9e-o2elxo6B1Y0PdOxBQQp6GCXT8z30iD015v7hgtnIYhu-0r5PE001qGP2DVPnJ2k7rzEhxdRIcFZwOm5bxie3MQMI53yEi6_1a3Vi2XiAGtU1OrMU1ddfjisDQ";
// Jwt jwt = JwtHelper.decode(token);
// System.out.println(jwt.toString());
String publicKey = "-----BEGIN PUBLIC KEY-----\n" +
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm4irSNcR7CSSfXconxL4\n" +
"g4M4j34wTWdTv93ocMn4VmdB7rCBU/BlxXtBUf/cgLIgQhQrAPszSZSmxiEXCOkG\n" +
"Pr4aQBQuPgmNIR95Dhbzw/ZN0BnecAt3ZfkkDBHv8kH3kR/jYGTdwrxKeDgXGljN\n" +
"sTRhbjuASxPG/Z6gU1yRPCsgc2r8NYnztWGcDWqaobqjG3/yzFmusoAboyV7asIp\n" +
"o4yk378LmonDNwxnOOTb2Peg5PeelwfOwJPbftK1VOOt18zA0cchw6dHUzq9NlB8\n" +
"clps/VdBap9BxU3/0YoFXRIc18nyzrWo2BcY2KQqX//AJC3OAfrfDmo+BGK8E0mp\n" +
"8wIDAQAB\n" +
"-----END PUBLIC KEY-----";
System.out.println(JwtHelper.decodeAndVerify(token, new RsaVerifier(publicKey)));
}
}
运行结果
{"alg":"RS256","typ":"JWT"} {"scope":["all"],"ext_name":"irving","exp":1533564034,"client_name":"client_1","authorities":["client_credentials"],"jti":"38212c79-07f9-4db3-84d5-5b0766e083f9","client_id":"client_1"} [256 crypto bytes]
在 Zuul 网关上添加授权认证方式
- 上述可以知道在资源服务端(Zuul)访问授权服务端 /oauth/token_key 获得公钥进行验签,后续也可以通过 scope 或自定义的字段来实现权限等功能。
- 资源服务端(Zuul)访问授权服务端 /oauth/check_token 来验证 Token 的合法性,但是得注意控制频率。
如果还是觉得 Spring cloud OAuth2 设计过于复杂(比如 token 的二进制序列化,密码的 BCryptPasswordEncoder 加密),也可以基于 Spring boot 来根据 OAuth2.0 与 JWT 的相关规范实现自己想要的功能,这样扩展性与定制化的功能可能会强一些,可以参考这篇文章:https://blog.csdn.net/neosmith/article/details/52539927。
注意:
spring-security-oauth2-autoconfigure 项目是由于版本问题为了支持 Spring Boot 2.x 的,具体看官方的文档说明。作为 Resource Server 端验证的方式需要注意的是当配置了 prefer-token-info=true (默认),资源端是验证方式是调用 /check_token 接口;当配置了 JwtToken 时,需要配置 security.oauth2.resource.jwt.key-uri(/token_key) 来获取公钥。资源端其他配置如下:
# SECURITY OAUTH2 CLIENT (OAuth2ClientProperties)
security.oauth2.client.client-id= # OAuth2 client id.
security.oauth2.client.client-secret= # OAuth2 client secret. A random secret is generated by default # SECURITY OAUTH2 RESOURCES (ResourceServerProperties)
security.oauth2.resource.id= # Identifier of the resource.
security.oauth2.resource.jwt.key-uri= # The URI of the JWT token. Can be set if the value is not available and the key is public.
security.oauth2.resource.jwt.key-value= # The verification key of the JWT token. Can either be a symmetric secret or PEM-encoded RSA public key.
security.oauth2.resource.jwk.key-set-uri= # The URI for getting the set of keys that can be used to validate the token.
security.oauth2.resource.prefer-token-info=true # Use the token info, can be set to false to use the user info.
security.oauth2.resource.service-id=resource #
security.oauth2.resource.token-info-uri= # URI of the token decoding endpoint.
security.oauth2.resource.token-type= # The token type to send when using the userInfoUri.
security.oauth2.resource.user-info-uri= # URI of the user endpoint. # SECURITY OAUTH2 SSO (OAuth2SsoProperties)
security.oauth2.sso.login-path=/login # Path to the login page, i.e. the one that triggers the redirect to the OAuth2 Authorization Server
REFER:
https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/htmlsingle/
如何构建安全的微服务应用
https://www.cnblogs.com/exceptioneye/p/9341011.html
使用 OAuth 2 和 JWT 为微服务提供安全保障
https://segmentfault.com/a/1190000009164779
证书及证书管理(keytool工具实例)
https://www.cnblogs.com/benwu/articles/4891758.html
https://www.jianshu.com/p/4089c9cc2dfd
https://www.cnblogs.com/xingxueliao/p/5911292.html
http://www.baeldung.com/spring-security-oauth-jwt
https://www.jianshu.com/p/2c231c96a29b
https://www.base64encode.org/
https://jwt.io/
https://stackoverflow.com/questions/29650495/how-to-verify-a-jwt-using-python-pyjwt-with-public-key
Spring Cloud OAuth2.0 微服务中配置 Jwt Token 签名/验证的更多相关文章
- SpringCloud(9)使用Spring Cloud OAuth2保护微服务系统
一.简介 OAth2是一个标准的授权协议. 在认证与授权的过程中,主要包含以下3种角色. 服务提供方 Authorization Server. 资源持有者 Resource Server. 客户端 ...
- 基于spring cloud OAuth2的微服务授权验证服务搭建的一些坑, 包括401,client_secret,invalid_scope等问题
一 先贴成功图,用的是springcloud Finchley.SR1版本,springboot版本2.0.6 问题一: 返回401, Unauthorized 出现这个问题原因很多:首先确保方法开启 ...
- 《Spring Cloud与Docker微服务架构实战》配套代码
不才写了本使用Spring Cloud玩转微服务架构的书,书名是<Spring Cloud与Docker微服务架构实战> - 周立,已于2017-01-12交稿.不少朋友想先看看源码,现将 ...
- Spring Cloud与Docker微服务架构实战 PDF
电子版百度云下载 链接: https://pan.baidu.com/s/115u011CJ8MZzJx_NqutyTQ 提取码: 关注公众号[GitHubCN]回复2019获取 本书的代码 共计70 ...
- Spring Cloud与Docker微服务架构实战 PDF版 内含目录
Spring Cloud与Docker微服务架构实战 目录 1 微服务架构概述 1 1.1 单体应用架构存在的问题1 1.2 如何解决单体应用架构存在的问题3 1.3 什么是微服务3 1.4 微服务 ...
- Spring Cloud与Docker——微服务架构概述
Spring Cloud与Docker--微服务架构概述 单体应用架构概述 微服务概述 微服务的特性 微服务架构的优点 微服务面临的挑战 微服务的设计原则 单体应用架构概述 传统的服务发布都是采用单体 ...
- 基于 Spring Cloud 完整的微服务架构实战
本项目是一个基于 Spring Boot.Spring Cloud.Spring Oauth2 和 Spring Cloud Netflix 等框架构建的微服务项目. @作者:Sheldon地址:ht ...
- 最新最简洁Spring Cloud Oauth2.0 Jwt 的Security方式
因为Spring Cloud 2020.0.0和Spring Boot2.4.1版本升级比较大,所以把我接入过程中的一些需要注意的地方告诉大家 我使用的版本是Spring boot 2.4.1+Spr ...
- spring cloud + mybatis 分布式 微服务 b2b2c 多商户商城 全球部署方案
用java实施的电子商务平台太少了,使用spring cloud技术构建的b2b2c电子商务平台更少,大型企业分布式互联网电子商务平台,推出PC+微信+APP+云服务的云商平台系统,其中包括B2B.B ...
随机推荐
- 【python-dict】dict的使用及实现原理
以下内容是针对:python源码剖析中的第五章——python中Dict对象 的读书笔记(针对书中讲到的内容进行了自己的整理,并且针对部分内容根据自己的需求进行了扩展) 一.Dict的用法 Dict的 ...
- 694. Number of Distinct Islands 形状不同的岛屿数量
[抄题]: Given a non-empty 2D array grid of 0's and 1's, an island is a group of 1's (representing land ...
- yum -y install php-mysql 版本冲突
yum -y install php-mysql 版本冲突 2018年09月02日 19:16:59 乐于技术分享 阅读数:640 [root@itop yum.repos.d]# yum -y ...
- 这里有一篇简单易懂的webSocket 快到碗里来~
这篇文章是我在学习的时候看到的 刚开始还不是很理解 后来自己百度 又问了一些人 回过头在看这篇文章 真的挺好的 但是原创已经不知道是谁了 转载哦~~~ -------------------- ...
- 过滤器(Filter)与拦截器(Interceptor)区别
过滤器(Filter)与拦截器(Interceptor)区别 过滤器(Filter) Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序,主要的用途 ...
- 《笨方法学Python》加分题6
types_of_people = 10 x = f"There are {types_of_people} types of peoples." binary = "b ...
- Python:每日一题006
题目:斐波那契数列. 程序分析:斐波那契数列(Fibonacci sequence),又称黄金分割数列,指的是这样一个数列:0.1.1.2.3.5.8.13.21.34.…… 个人思路及代码: # 方 ...
- ubuntu16.04 下安装 visual studio code 以及利用 g++ 运行 c++程序
参考链接:1. http://www.linuxidc.com/Linux/2016-07/132798.htm(安装vs code) 2.https://blog.csdn.net/qq_28598 ...
- 上传input中file文件到云端,并返回链接
有的文件.图片等信息可以上传到云端上,然后使用链接调用,这样会更加的方便和快捷. <form id="form"> <input type="file& ...
- Codeforces Round #512 (Div. 2) D. Vasya and Triangle
参考了别人的思路:https://blog.csdn.net/qq_41608020/article/details/82827632 http://www.cnblogs.com/qywhy/p/9 ...