Fork me on GitHub

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/html5/

https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide
如何构建安全的微服务应用
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

posted @ 2018-08-06 15:27  花儿笑弯了腰  阅读(31316)  评论(0编辑  收藏  举报