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 ...
随机推荐
- Mac Eclipse 配置 SDK Manager Proxy (代理)
默认的下载地址非常慢,可以换成东软的代理. 顶部任务栏中选择SDK Manager -> 偏好设置 : 可以看到下载速度快了很多,出现类很多安装选项: 安装好后,在偏好设置窗口中,选择Clear ...
- 698. Partition to K Equal Sum Subsets 数组分成和相同的k组
[抄题]: Given an array of integers nums and a positive integer k, find whether it's possible to divide ...
- ios tableView的header高度不对
tableView的header高度不对,一般都是header是从xib加载出来的 第一步: 新建xib的时候选择的是View,当选择 Size 为 Freeform 时,view的约束就变成这样了, ...
- Linux驱动之异常处理体系结构简析
异常的概念在单片机中也接触过,它的意思是让CPU可以暂停当前的事情,跳到异常处理程序去执行.以前写单片机裸机程序属于前后台程序,前台指的就是mian函数里的while(1)大循环,后台指的就是产生异常 ...
- JVM 字节码(三)异常在字节码中的处理(catch 和 throws)
JVM 字节码(三)异常在字节码中的处理(catch 和 throws) 在 ClassFile 中到底是如何处理异常的呢? 一.代码块异常 catch catch 中的异常代码块在异常是如何处理的呢 ...
- 注解@ResponseBody的作用
@ResponseBody通常是放在方法上,主要是在前端页面异步请求的时候,返回数据使用.直白点说就是加上这个注解之后,return的数据不会解析成返回跳转路径,而是会默认放在 response b ...
- 设计模式 工厂模式 使用shared_ptr
参考http://blog.csdn.net/calmreason/article/details/50903729 所有产品继承同一基本类 由工厂保存基类指针 产生各类产品 代码 // 002.cp ...
- rabbit初学之连接测试2
com.rabbitmq.client.ShutdownSignalException: connection error 发现,port是5672,不是15672(15672是后台管理平台的端口)
- Codeforces 835C-Star sky
题目链接:http://codeforces.com/problemset/problem/835/C 题意:天上有很多星星,每个星星有他自己的坐标和初始亮度,然后每个星星的亮度在一秒内会加一如果大于 ...
- pyhton 核心编程 正则表达式习题
方案一 import re #1. 识别下列字符串:“bat,” “bit,” “but,” “hat,” “hit,” 或 “hut” import re def test1(self): bt = ...