Spring Cloud Oauth2實現分佈式權限認證(JWT版)

目錄

環境:

摘要說明:

步驟:

一、什麼是JWT

二、實現JWT版授權服務

1、公共模塊(oauth2-common)

2、授權服務(oauth2-server)

3、應用服務(oauth2-client)

三、測試

四、jwt的優缺點

jwt的優點:

jwt的缺點:

五、源碼地址

環境:

JDK1.8,spring-boot(2.0.3.RELEASE),spring cloud(Finchley.RELEASE)

摘要說明:

       上一節中我們oauht2+redis實現了分佈式授權服務,但反過來總結下就會發現該模式還是中心化授權;即任何應用服務的接口訪問都必須通過token去訪問授權服務(oauth2-server)是否滿足授權,即所有應用服務都必須配置授權服務獲取用戶信息接口;

       但考慮到微服務往往意味着高併發,高流量;故這種中心化的授權服務會成爲瓶頸,所以如何實現去中心化的授權服務就需要依賴本章節的JWT;

步驟:

一、什麼是JWT

       Json web token (JWT), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準(RFC 7519).該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。

       簡單點說就是一種固定格式的字符串,通常是加密的;它由三部分組成,頭部載荷簽名,這三個部分都是json格式。

  • Header 頭部:JSON方式描述JWT基本信息,如類型和簽名算法。使用Base64編碼爲字符串
  • Payload 載荷: JSON方式描述JWT信息,除了標準定義的,還可以添加自定義的信息。同樣使用Base64編碼爲字符串。
  1. iss: 簽發者
  2. sub: 用戶
  3. aud: 接收方
  4. exp(expires): unix時間戳描述的過期時間
  5. iat(issued at): unix時間戳描述的簽發時間
  • Signature 簽名: 將前兩個字符串用 . 連接後,使用頭部定義的加密算法,利用密鑰進行簽名,並將簽名信息附在最後。

     JWT可以使用對稱的加密密鑰,但更安全的是使用非對稱的密鑰;下面就讓我們使用jdk自帶的keytool生成非對稱的公鑰、私鑰;

生成公鑰:

keytool -genkeypair -alias spring-jwt -validity 3650 -keyalg RSA -dname "CN=Victor,OU=Karonda,O=Karonda,L=Shenzhen,S=Guangdong,C=CN" -keypass admin123456 -storepass admin123456 -keystore spring-jwt.jks

這裏面主要是-keypass 密鑰 -storepass 密鑰

生產私鑰:

keytool -list -rfc --keystore spring-jwt.jks | openssl x509 -inform pem -pubkey

會提示輸入上述私鑰的密碼:

接着加下面這部分cope出生成public.cert;

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgsMnUy3qsvv9RypsSH/t
4/Kqo6xAW0yegdTBnXxbw/FJiTt9FSEg17yIm7Emg09vUJTKjUMFUMMT9fJ+r29j
LZuG88yTgQXOkqk64tUYh56M8GMKSXKRhyQdgdLQVi3lhaBh977/3aCtDerjzbkk
kFGDm8psDf26sqHj6Derka+M4V+/f4fz7CRu4QSfMGUePbT2V7mI3Y2kS0DHRl0f
K7SzbFkBr+MCBe7fO0wPklhx2W18/V7dYQe2ssd8Et+AzVI+yjbreqM+775b7Imw
bgxv1iJbi5j1JZ3AzsuViZ6OktLyCpRBGNZX7C/8xyNv9m/QjTsHJy/nSrLdbJQ5
2wIDAQAB
-----END PUBLIC KEY-----

將生成的spring-jwt.jks和 public.cert分別放到授權服務(oauth2-server)和應用服務(oauth2-client)的resource下

二、實現JWT版授權服務

         在上一節的基礎上想實現JWT授權服務其實很簡單,只需要修改以下幾點

1、公共模塊(oauth2-common)

添加JWT轉換器配置(JwtConfig)用戶應用服務解析前端傳入的jwt形式token:

@Configuration
public class JwtConfig {
    @Autowired
    JwtAccessTokenConverter jwtAccessTokenConverter;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    @Bean
    protected JwtAccessTokenConverter jwtTokenEnhancer() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        Resource resource = new ClassPathResource("public.cert"); // 公鑰

        String publicKey;
        try {
            publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
        converter.setVerifierKey(publicKey);

        return converter;
    }
}

2、授權服務(oauth2-server)

修改授權服務配置(AuthorizationServerConfiguration):

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    /**
     * 認證管理器
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 
     * @方法名:tokenStore
     * @方法描述:自定義儲存策略
     * @return
     * @修改描述:
     * @版本:1.0
     * @創建人:cc
     * @創建時間:2019年11月19日 下午1:56:02
     * @修改人:cc
     * @修改時間:2019年11月19日 下午1:56:02
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtTokenEnhancer());
    }

    /**
     * 定義令牌端點上的安全性約 束
     * 
     * (non-Javadoc)
     * 
     * @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer)
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients().tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

    /**
     * 用於定義客戶端詳細信息服務的配置程序。可以初始化客戶端詳細信息;
     * 
     * (non-Javadoc)
     * 
     * @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer)
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // clients.withClientDetails(clientDetails());
        clients.inMemory().withClient("android").scopes("read").secret(DigestUtil.encrypt("android"))
                .authorizedGrantTypes("password", "authorization_code", "refresh_token").and().withClient("webapp")
                .scopes("read").authorizedGrantTypes("implicit").and().withClient("browser")
                .authorizedGrantTypes("refresh_token", "password").scopes("read");
    }

    @Bean
    public WebResponseExceptionTranslator webResponseExceptionTranslator() {
        return new MssWebResponseExceptionTranslator();
    }

    /**
     * 定義授權和令牌端點以及令牌服務
     * 
     * (non-Javadoc)
     * 
     * @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer)
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer())
                .authenticationManager(authenticationManager).exceptionTranslator(webResponseExceptionTranslator());
    }

    /**
     * <p>
     * 注意,自定義TokenServices的時候,需要設置@Primary,否則報錯,
     * </p>
     * 
     * @return
     */
    @Primary
    @Bean
    public DefaultTokenServices defaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setTokenEnhancer(jwtTokenEnhancer());
        // tokenServices.setClientDetailsService(clientDetails());
        // token有效期自定義設置,默認12小時
        tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24 * 7);
        // tokenServices.setAccessTokenValiditySeconds(60 * 60 * 12);
        // refresh_token默認30天
        tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24 * 7);
        // tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
        return tokenServices;
    }

    /**
     * 定義jwt的生成方式
     *
     * @return JwtAccessTokenConverter
     */
    @Bean
    public JwtAccessTokenConverter jwtTokenEnhancer() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 非對稱加密,但jwt長度過長
        KeyPair keyPair = new KeyStoreKeyFactory(new ClassPathResource("spring-jwt.jks"), "admin123456".toCharArray())
                .getKeyPair("spring-jwt");
        converter.setKeyPair(keyPair);
        // 對稱加密
        // converter.setSigningKey("admin123");
        return converter;
    }
}

修改資源服務配置(ResourceServerConfig):


@Configuration
@EnableResourceServer
@Order(3)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().exceptionHandling()
                .authenticationEntryPoint(
                        (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and().requestMatchers().antMatchers("/api/**").and().authorizeRequests().antMatchers("/api/**")
                .authenticated().and().httpBasic();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore);
    }
}

3、應用服務(oauth2-client)

去除配置裏授權服務的鏈接配置:

修改資源服務配置(ResourceServerConfig),只能有上述公共模塊(oauth2-common)生成的tokenStore進行解析

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().exceptionHandling()
                .authenticationEntryPoint(
                        (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and().requestMatchers().antMatchers("/api/**").and().authorizeRequests().antMatchers("/api/**")
                .authenticated().and().httpBasic();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore);
    }
}

三、測試

1、先後啓動服務:

註冊中心(eureka-server),

授權服務(oauth2-server),

網關服務(oauth2-gateway),

應用服務(auth2-client)

2、進行登錄,輸入授權類型,用戶名,密碼。並設置client的名稱及密碼,返回token:

登錄成功後我們可以看到token的長度明顯比上一章

這裏面要說明的是需要註明授權類型,並輸入客戶端用戶名及密碼:

3、先後測試應用服務(oauth2-cliant)的三個接口/api/current、/api/hello、/api/query效果如下,與redis版一致

四、jwt的優缺點

基於session和基於jwt的方式的主要區別就是用戶的狀態保存的位置,session是保存在服務端的,而jwt是保存在客戶端的。

jwt的優點:

1. 可擴展性好

應用程序分佈式部署的情況下,session需要做多機數據共享,通常可以存在數據庫或者redis裏面。而jwt不需要。

2. 無狀態

jwt不在服務端存儲任何狀態。RESTful API的原則之一是無狀態,發出請求時,總會返回帶有參數的響應,不會產生附加影響。用戶的認證狀態引入這種附加影響,這破壞了這一原則。另外jwt的載荷中可以存儲一些常用信息,用於交換信息,有效地使用 JWT,可以降低服務器查詢數據庫的次數。

 

jwt的缺點:

1. 安全性

由於jwt的payload是使用base64編碼的,並沒有加密,因此jwt中不能存儲敏感數據。而session的信息是存在服務端的,相對來說更安全。

2. 性能

jwt太長。由於是無狀態使用JWT,所有的數據都被放到JWT裏,如果還要進行一些數據交換,那載荷會更大,經過編碼之後導致jwt非常長,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage裏面。並且用戶在系統中的每一次http請求都會把jwt攜帶在Header裏面,http請求的Header可能比Body還要大。而sessionId只是很短的一個字符串,因此使用jwt的http請求比使用session的開銷大得多。

3. 一次性

無狀態是jwt的特點,但也導致了這個問題,jwt是一次性的。想修改裏面的內容,就必須簽發一個新的jwt。

(1)無法廢棄

通過上面jwt的驗證機制可以看出來,一旦簽發一個jwt,在到期之前就會始終有效,無法中途廢棄。例如你在payload中存儲了一些信息,當信息需要更新時,則重新簽發一個jwt,但是由於舊的jwt還沒過期,拿着這個舊的jwt依舊可以登錄,那登錄後服務端從jwt中拿到的信息就是過時的。爲了解決這個問題,我們就需要在服務端部署額外的邏輯,例如設置一個黑名單,一旦簽發了新的jwt,那麼舊的就加入黑名單(比如存到redis裏面),避免被再次使用。

(2)續簽

如果你使用jwt做會話管理,傳統的cookie續簽方案一般都是框架自帶的,session有效期30分鐘,30分鐘內如果有訪問,有效期被刷新至30分鐘。一樣的道理,要改變jwt的有效時間,就要簽發新的jwt。最簡單的一種方式是每次請求刷新jwt,即每個http請求都返回一個新的jwt。這個方法不僅暴力不優雅,而且每次請求都要做jwt的加密解密,會帶來性能問題。另一種方法是在redis中單獨爲每個jwt設置過期時間,每次訪問時刷新jwt的過期時間。

五、源碼地址

https://github.com/cc6688211/oauth2/tree/jwt

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章