authrization-server(授權服務器)
授權服務配置
-
配置一個授權服務,需要考慮 授權類型(GrantType)、不同授權類型為客戶端(Client)提供了不同的獲取令牌(Token)方式,每一個客戶端(Client)都能夠通過明確的配置以及權限來實現不同的授權訪問機制,也就是說如果你提供了一個 “client_credentials” 授權方式,並不意味着其它客戶端就要采用這種方式來授權
-
使用 @EnableAuthorizationServer 來配置授權服務機制,並繼承 AuthorizationServerConfigurerAdapter 該類重寫 configure 方法定義授權服務器策略
配置客戶端詳情(Client Details)
- ClientDetailsServiceConfigurer 能夠使用內存或 JDBC 方式實現獲取已注冊的客戶端詳情,有幾個重要的屬性:
- clientId:客戶端標識 ID
- secret:客戶端安全碼
- scope:客戶端訪問范圍,默認為空則擁有全部范圍
- authorizedGrantTypes:客戶端使用的授權類型,默認為空
- authorities:客戶端可使用的權限
管理令牌(Managing Token)
- ResourceServerTokenServices 接口定義了令牌加載、讀取方法
- AuthorizationServerTokenServices 接口定義了令牌的創建、獲取、刷新方法
- ConsumerTokenServices 定義了令牌的撤銷方法
- DefaultTokenServices 實現了上述三個接口,它包含了一些令牌業務的實現,如創建令牌、讀取令牌、刷新令牌、獲取客戶端ID。默認的當嘗試創建一個令牌時,是使用 UUID 隨機值進行填充的,除了持久化令牌是委托一個 TokenStore 接口實現以外,這個類幾乎幫你做了所有事情
- 而 TokenStore 接口也有一些實現:
- InMemoryTokenStore:默認采用該實現,將令牌信息保存在內存中,易於調試
- JdbcTokenStore:令牌會被保存近關系型數據庫,可以在不同服務器之間共享令牌
- JwtTokenStore:使用 JWT 方式保存令牌,它不需要進行存儲,但是它撤銷一個已經授權令牌會非常困難,所以通常用來處理一個生命周期較短的令牌以及撤銷刷新令牌
JWT 令牌(JWT Tokens)
- 使用 JWT 令牌需要在授權服務中使用 JWTTokenStore,資源服務器也需要一個解碼 Token 令牌的類 JwtAccessTokenConverter,JwtTokenStore 依賴這個類進行編碼以及解碼,因此授權服務以及資源服務都需要配置這個轉換類
- Token 令牌默認是有簽名的,並且資源服務器中需要驗證這個簽名,因此需要一個對稱的 Key 值,用來參與簽名計算
- 這個 Key 值存在於授權服務和資源服務之中,或者使用非對稱加密算法加密 Token 進行簽名,Public Key 公布在 /oauth/token_key 這個 URL 中
- 默認 /oauth/token_key 的訪問安全規則是 "denyAll()" 即關閉的,可以注入一個標准的 SpingEL 表達式到 AuthorizationServerSecurityConfigurer 配置類中將它開啟,例如 permitAll()
- 需要引入 spring-security-jwt 庫
配置授權類型(Grant Types)
- 授權是使用 AuthorizationEndpoint 這個端點來進行控制的,使用 AuthorizationServerEndpointsConfigurer 這個對象實例來進行配置,默認是支持除了密碼授權外所有標准授權類型,它可配置以下屬性:
- authenticationManager:認證管理器,當你選擇了資源所有者密碼(password)授權類型的時候,請設置這個屬性注入一個 AuthenticationManager 對象
- userDetailsService:可定義自己的 UserDetailsService 接口實現
- authorizationCodeServices:用來設置收取碼服務的(即 AuthorizationCodeServices 的實例對象),主要用於 "authorization_code" 授權碼類型模式
- implicitGrantService:這個屬性用於設置隱式授權模式,用來管理隱式授權模式的狀態
- tokenGranter:完全自定義授權服務實現(TokenGranter 接口實現),只有當標准的四種授權模式已無法滿足需求時
配置授權端點 URL(Endpoint URLs)
- AuthorizationServerEndpointsConfigurer 配置對象有一個 pathMapping() 方法用來配置端點的 URL,它有兩個參數:
- 參數一:端點 URL 默認鏈接
- 參數二:替代的 URL 鏈接
- 下面是一些默認的端點 URL:
- /oauth/authorize:授權端點
- /oauth/token:令牌端點
- /oauth/confirm_access:用戶確認授權提交端點
- /oauth/error:授權服務錯誤信息端點
- /oauth/check_token:用於資源服務訪問的令牌解析端點
- /oauth/token_key:提供公有密匙的端點,如果你使用JWT令牌的話
- 授權端點的 URL 應該被 Spring Security 保護起來只供授權用戶訪問
代碼案例
引入依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
基於內存存儲令牌
配置授權服務類,創建一個類繼承 AuthorizationServerConfigurerAdapter 並添加 @EnableAuthorizationServer 注解,添加客戶端信息
@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //添加客戶端信息 clients.inMemory() // 使用in-memory存儲客戶端信息 .withClient("client") // client_id .secret("secret") // client_secret .authorizedGrantTypes("authorization_code") // 該client允許的授權類型 .scopes("app"); // 允許的授權范圍 } }
修改配置文件,設置 Security 密碼為 password,用戶名為 root,相當於一個資源擁有者(用戶)的賬號密碼
security:
user:
name: root
password: 1234
server:
port: 8081
測試
通過瀏覽器模擬客戶端訪問授權端點 /oauth/authorize
#(該步驟為**授權碼模式中的A**),需要附上客戶端申請認證的參數(**A步驟中所包含的參數**) localhost:8081/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com
進入用戶登陸頁面(該步驟為授權碼模式中的B)

輸入 root 1234 登陸后會進入下面頁面,詢問用戶是否授權客戶端(該步驟為授權碼模式中的C)

勾選授權后點擊按鈕會跳轉到百度
#(**A步驟中包含的參數定義了重定向URL**),並在 URL 中包含一個授權碼 https://www.baidu.com/?code=mhlA24
客戶端拿到授權碼后,附上先前設置的重定向 URL 向服務器申請令牌
# (該步驟為**授權碼模式中的D**),通過令牌端點 /oauth/token
# 使用 CURL 工具發送 POST 命令,授權碼模式不需要 client_sercet,因此該值可以為任意值
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=Li4NZo&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8081/oauth/token"
返回令牌如下
{"access_token":"d0e2f362-3bfd-43bb-a6ca-b6cb1b8ea9ee","token_type":"bearer","expires_in":43199,"scope":"app"}
基於JDBC存儲令牌
- Spring Cloud Security OAuth 已經為我們設計好了一套 Schema 和對應的 DAO 對象
- Spring Cloud Security OAuth2 通過 DefaultTokenServices 類來完成 token 生成、過期等 OAuth2 標准規定的業務邏輯,而 DefaultTokenServices 又是通過 TokenStore 接口完成對生成數據的持久化
- 在上面的 Demo 中,TokenStore 的默認實現為 InMemoryTokenStore 即內存存儲,對於 Client 信息,ClientDetailsService 接口負責從存儲倉庫中讀取數據,在上面的 Demo 中默認使用的也是 InMemoryClientDetailsService 實現類
- 要想使用數據庫存儲,只要提供這些接口的實現類即可,而框架已經為我們寫好 JdbcTokenStore 和 JdbcClientDetailsService
建表
- 框架已提前為我們設計好了數據庫表,但對於 MYSQL 來說,默認建表語句中主鍵為 Varchar(256),這超過了最大的主鍵長度,可改成 128,並用 BLOB 替換語句中的 LONGVARBINARY 類型
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
代碼
編寫 @Configuration 類繼承 AuthorizationServerConfigurerAdapter
@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; @Autowired private TokenStore tokenStore; private ClientDetailsService clientDetailsService; @Bean // 聲明TokenStore實現 public TokenStore tokenStore() { return new JdbcTokenStore(dataSource); } @Bean // 聲明 ClientDetails實現 public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } @Override // 配置框架應用上述實現 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager); endpoints.tokenStore(tokenStore); // 配置TokenServices參數 DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(endpoints.getTokenStore()); tokenServices.setSupportRefreshToken(false); tokenServices.setClientDetailsService(endpoints.getClientDetailsService()); tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer()); tokenServices.setAccessTokenValiditySeconds( (int) TimeUnit.DAYS.toSeconds(30)); // 30天 endpoints.tokenServices(tokenServices); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); } }
修改配置文件,並引入 MYSQL 和 JDBC 依賴庫
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/client?useUnicode=yes&characterEncoding=UTF-8 username: root password: 123456ly
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
往數據庫 oauth_client_details 表添加客戶端信息

基於JWT存儲令牌
對稱加密,對稱加密表示認證服務端和客戶端的共用一個密鑰
@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenStore tokenStore; //告訴Spring Security Token的生成方式 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore) .accessTokenConverter(jwtAccessTokenConverter()) .authenticationManager(authenticationManager); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } //使用同一個密鑰來編碼 JWT 中的 OAuth2 令牌 @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("123"); return converter; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() // 使用in-memory存儲客戶端信息 .withClient("client") // client_id .secret("secret") // client_secret .authorizedGrantTypes("authorization_code") // 該client允許的授權類型 .scopes("app") // 允許的授權范圍 .autoApprove(true); //登錄后繞過批准詢問(/oauth/confirm_access) } }
使用不對稱的密鑰來簽署令牌
生成 JKS Java KeyStore 文件
keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass
導出公鑰
keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey
將公鑰保存為 pubkey.txt,將 mytest.jks()授權服務器) 和 pubkey.txt(資源服務器) 放到 resource 目錄下
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhAF1qpL+8On3rF2M77lR +l3WXKpGXIc2SwIXHwQvml/4SG7fJcupYVOkiaXj4f8g1e7qQCU4VJGvC/gGJ7sW fn+L+QKVaRhs9HuLsTzHcTVl2h5BeawzZoOi+bzQncLclhoMYXQJJ5fULnadRbKN HO7WyvrvYCANhCmdDKsDMDKxHTV9ViCIDpbyvdtjgT1fYLu66xZhubSHPowXXO15 LGDkROF0onqc8j4V29qy5iSnx8I9UIMEgrRpd6raJftlAeLXFa7BYlE2hf7cL+oG hY+q4S8CjHRuiDfebKFC1FJA3v3G9p9K4slrHlovxoVfe6QdduD8repoH07jWULu qQIDAQAB -----END PUBLIC KEY-----
驗證服務器配置
@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenStore tokenStore; //告訴Spring Security Token的生成方式 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore) .accessTokenConverter(jwtAccessTokenConverter()) .authenticationManager(authenticationManager); } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer //允許所有資源服務器訪問公鑰端點(/oauth/token_key) //只允許驗證用戶訪問令牌解析端點(/oauth/check_token) .tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()") // 允許客戶端發送表單來進行權限認證來獲取令牌 .allowFormAuthenticationForClients(); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean //使用私鑰編碼 JWT 中的 OAuth2 令牌 public JwtAccessTokenConverter jwtAccessTokenConverter() { final JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray()); converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest")); return converter; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() // 測試用,將客戶端信息存儲在內存中 .withClient("client") // client_id .secret("secret") // client_secret .authorizedGrantTypes("authorization_code") // 該client允許的授權類型 .scopes("app") // 允許的授權范圍 .autoApprove(true); //登錄后繞過批准詢問(/oauth/confirm_access) } }
自定義令牌聲明,添加額外的屬性
添加一個額外的字段 "組織" 到令牌中
public class CustomTokenEnhancer implements TokenEnhancer {
將把它連接到我們的授權服務器配置
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers( Arrays.asList(tokenEnhancer(), accessTokenConverter())); endpoints.tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager); } @Bean public TokenEnhancer tokenEnhancer() { return new CustomTokenEnhancer(); }
此時令牌如下
{ "user_name": "john", "scope": [ "foo", "read", "write" ], "organization": "johnIiCh", "exp": 1458126622, "authorities": [ "ROLE_USER" ], "jti": "e0ad1ef3-a8a5-4eef-998d-00b26bc2c53f", "client_id": "fooClientIdPassword" }
測試
啟動授權服務器、啟動資源服務器
訪問授權服務器 /oauth/authorize 端點獲取授權碼 code=vT4fY0
localhost:8081/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com
訪問授權服務器 /oauth/token 端點獲取訪問令牌

訪問資源服務器受保護的資源,附上令牌在請求頭,**需加上 Bearer **
