关于 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 签名/验证的更多相关文章

  1. SpringCloud(9)使用Spring Cloud OAuth2保护微服务系统

    一.简介 OAth2是一个标准的授权协议. 在认证与授权的过程中,主要包含以下3种角色. 服务提供方 Authorization Server. 资源持有者 Resource Server. 客户端 ...

  2. 基于spring cloud OAuth2的微服务授权验证服务搭建的一些坑, 包括401,client_secret,invalid_scope等问题

    一 先贴成功图,用的是springcloud Finchley.SR1版本,springboot版本2.0.6 问题一: 返回401, Unauthorized 出现这个问题原因很多:首先确保方法开启 ...

  3. 《Spring Cloud与Docker微服务架构实战》配套代码

    不才写了本使用Spring Cloud玩转微服务架构的书,书名是<Spring Cloud与Docker微服务架构实战> - 周立,已于2017-01-12交稿.不少朋友想先看看源码,现将 ...

  4. Spring Cloud与Docker微服务架构实战 PDF

    电子版百度云下载 链接: https://pan.baidu.com/s/115u011CJ8MZzJx_NqutyTQ 提取码: 关注公众号[GitHubCN]回复2019获取 本书的代码 共计70 ...

  5. Spring Cloud与Docker微服务架构实战 PDF版 内含目录

    Spring Cloud与Docker微服务架构实战  目录 1 微服务架构概述 1 1.1 单体应用架构存在的问题1 1.2 如何解决单体应用架构存在的问题3 1.3 什么是微服务3 1.4 微服务 ...

  6. Spring Cloud与Docker——微服务架构概述

    Spring Cloud与Docker--微服务架构概述 单体应用架构概述 微服务概述 微服务的特性 微服务架构的优点 微服务面临的挑战 微服务的设计原则 单体应用架构概述 传统的服务发布都是采用单体 ...

  7. 基于 Spring Cloud 完整的微服务架构实战

    本项目是一个基于 Spring Boot.Spring Cloud.Spring Oauth2 和 Spring Cloud Netflix 等框架构建的微服务项目. @作者:Sheldon地址:ht ...

  8. 最新最简洁Spring Cloud Oauth2.0 Jwt 的Security方式

    因为Spring Cloud 2020.0.0和Spring Boot2.4.1版本升级比较大,所以把我接入过程中的一些需要注意的地方告诉大家 我使用的版本是Spring boot 2.4.1+Spr ...

  9. spring cloud + mybatis 分布式 微服务 b2b2c 多商户商城 全球部署方案

    用java实施的电子商务平台太少了,使用spring cloud技术构建的b2b2c电子商务平台更少,大型企业分布式互联网电子商务平台,推出PC+微信+APP+云服务的云商平台系统,其中包括B2B.B ...

随机推荐

  1. FortiGate下视频会议等语音相关配置

    关闭老的基于会话的alg机制(即删除session-helper中的SIP条目) config system session-helper delete 13  #删除sip end

  2. [leetcode]19. Remove Nth Node From End of List删除链表倒数第N个节点

    Given a linked list, remove the n-th node from the end of list and return its head. Example: Given l ...

  3. ImportError: No module named MySQLdb问题的解决

    今天在windows上撸python代码,遇到ImportError: No module named MySQLdb的问题,遂赶紧pip install mysql-python,结果还是不行,查看 ...

  4. kubernetes promethues预警、报警

    k8s addon中prometheus为测试事例,官方推荐生产环境使用Prometheus Operator and kube-prometheus. 1.clone 源码 git clone ht ...

  5. Scatter 散点图

    散点图 首先,先引入matplotlib.pyplot简写作plt,再引入模块numpy用来产生一些随机数据.生成1024个呈标准正态分布的二维数据组 (平均数是0,方差为1) 作为一个数据集,并图像 ...

  6. Difference Among Mercedes Star Diagnostic Tool MB Star C3 C4 C5 C6

    Mercedes Star Diagnostic Tool newly update to MB Star C6.There are many star diangostic tool in the ...

  7. how to adjust PKG_CONFIG_PATH environment-variable

    PKG_CONFIG_PATH is a environment variable that specifies additional paths in which pkg-config will s ...

  8. Delphi过程和函数中变量的作用域

    变量的作用域是指变量能被某一子程序识别的范围. 全局变量和局部变量.全局变量是指在程序的type区定义的变量,而局部变量是在过程或函数的定义部分声明的变量.全局变量在整个程序中都有意义,局部变量只在它 ...

  9. hadoop启动

    安装完hadoop集群之后,第一次启动之前必须初始化,之后就可以不用再初始化(注意:初始化操作只可以一次) hdfs namenode -format (hadoop namenode -format ...

  10. POJ 1328 Radar Installation 贪心 A

    POJ 1328 Radar Installation https://vjudge.net/problem/POJ-1328 题目: Assume the coasting is an infini ...