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/ 使用公鑰進行驗簽。

image

備注: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM