Spring Security OAuth2 Demo
項目使用的是MySql存儲, 需要先創建以下表結構:
CREATE SCHEMA IF NOT EXISTS `alan-oauth` DEFAULT CHARACTER SET utf8 ;
USE `alan-oauth` ;
-- -----------------------------------------------------
-- Table `alan-oauth`.`clientdetails`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `alan-oauth`.`clientdetails` (
`appId` VARCHAR(128) NOT NULL,
`resourceIds` VARCHAR(256) NULL DEFAULT NULL,
`appSecret` VARCHAR(256) NULL DEFAULT NULL,
`scope` VARCHAR(256) NULL DEFAULT NULL,
`grantTypes` VARCHAR(256) NULL DEFAULT NULL,
`redirectUrl` VARCHAR(256) NULL DEFAULT NULL,
`authorities` VARCHAR(256) NULL DEFAULT NULL,
`access_token_validity` INT(11) NULL DEFAULT NULL,
`refresh_token_validity` INT(11) NULL DEFAULT NULL,
`additionalInformation` VARCHAR(4096) NULL DEFAULT NULL,
`autoApproveScopes` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`appId`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `alan-oauth`.`oauth_access_token`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `alan-oauth`.`oauth_access_token` (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication_id` VARCHAR(128) NOT NULL,
`user_name` VARCHAR(256) NULL DEFAULT NULL,
`client_id` VARCHAR(256) NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL,
`refresh_token` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`authentication_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `alan-oauth`.`oauth_approvals`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `alan-oauth`.`oauth_approvals` (
`userId` VARCHAR(256) NULL DEFAULT NULL,
`clientId` VARCHAR(256) NULL DEFAULT NULL,
`scope` VARCHAR(256) NULL DEFAULT NULL,
`status` VARCHAR(10) NULL DEFAULT NULL,
`expiresAt` DATETIME NULL DEFAULT NULL,
`lastModifiedAt` DATETIME NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `alan-oauth`.`oauth_client_details`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `alan-oauth`.`oauth_client_details` (
`client_id` VARCHAR(128) NOT NULL,
`resource_ids` VARCHAR(256) NULL DEFAULT NULL,
`client_secret` VARCHAR(256) NULL DEFAULT NULL,
`scope` VARCHAR(256) NULL DEFAULT NULL,
`authorized_grant_types` VARCHAR(256) NULL DEFAULT NULL,
`web_server_redirect_uri` VARCHAR(256) NULL DEFAULT NULL,
`authorities` VARCHAR(256) NULL DEFAULT NULL,
`access_token_validity` INT(11) NULL DEFAULT NULL,
`refresh_token_validity` INT(11) NULL DEFAULT NULL,
`additional_information` VARCHAR(4096) NULL DEFAULT NULL,
`autoapprove` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`client_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `alan-oauth`.`oauth_client_token`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `alan-oauth`.`oauth_client_token` (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication_id` VARCHAR(128) NOT NULL,
`user_name` VARCHAR(256) NULL DEFAULT NULL,
`client_id` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`authentication_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `alan-oauth`.`oauth_code`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `alan-oauth`.`oauth_code` (
`code` VARCHAR(256) NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `alan-oauth`.`oauth_refresh_token`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `alan-oauth`.`oauth_refresh_token` (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
然后在oauth_client_details
表中插入記錄:
# client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove
'client', NULL, 'secret', 'app', 'authorization_code', 'http://www.baidu.com', NULL, NULL, NULL, NULL, NULL
這時就可以訪問授權頁面了:
localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com
訪問時Spring讓你登陸,隨便輸入一個用戶名密碼即可。 注意, 如果每次登陸時輸入的用戶名不一樣,那么Spring Security會認為是不同的用戶,因此訪問/token/authorize會再次顯示授權頁面。如果用戶名一致, 則只需要授權一次
數據庫連接信息在application.properties
中配置。
Spring Cloud Security OAuth2 是 Spring 對 OAuth2 的開源實現,優點是能與Spring Cloud技術棧無縫集成,如果全部使用默認配置,開發者只需要添加注解就能完成 OAuth2 授權服務的搭建。
博文
1. 添加依賴
授權服務是基於Spring Security的,因此需要在項目中引入兩個依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
前者為 Security,后者為Security的OAuth2擴展。
2. 添加注解和配置
在啟動類中添加@EnableAuthorizationServer
注解:
@SpringBootApplication
@EnableAuthorizationServer
public class AlanOAuthApplication {
public static void main(String[] args) {
SpringApplication.run(AlanOAuthApplication.class, args);
}
}
完成這些我們的授權服務最基本的骨架就已經搭建完成了。但是要想跑通整個流程,我們必須分配 client_id
, client_secret
才行。Spring Security OAuth2的配置方法是編寫@Configuration
類繼承AuthorizationServerConfigurerAdapter
,然后重寫void configure(ClientDetailsServiceConfigurer clients)
方法,如:
@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"); // 允許的授權范圍 }
3. 授權流程
訪問授權頁面:
localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com
此時瀏覽器會讓你輸入用戶名密碼,這是因為 Spring Security 在默認情況下會對所有URL添加Basic Auth認證。默認的用戶名為user
, 密碼是隨機生成的,在控制台日志中可以看到。
畫風雖然很簡陋,但是基本功能都具備了。點擊Authorize
后,瀏覽器就會重定向到百度,並帶上code
參數:
拿到code
以后,就可以調用
POST/GET http://client:secret@localhost:8080/oauth/token
來換取access_token
了:
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:8080/oauth/token"
注意,URL中的client為上文中通過
ClientDetailsServiceConfigurer
類指定的clientId。由於authorization_code的授權方式不需要 client_secret, 因此secret可以填寫任意值
返回如下:
{
"access_token": "32a1ca28-bc7a-4147-88a1-c95abcc30556", // 令牌 "token_type": "bearer", "expires_in": 2591999, "scope": "app" }
到此我們最最基本的授權服務就搭建完成了。然而,這僅僅是個demo,如果要在生產環境中使用,還需要做更多的工作。
4. 使用MySQL存儲access_token和client信息
在上面的例子中,所有的token信息都是保存在內存中的,這顯然無法在生產環境中使用(進程結束后所有token丟失, 用戶需要重新授權),因此我們需要將這些信息進行持久化操作。 把授權服務器中的數據存儲到數據庫中並不難,因為 Spring Cloud Security OAuth 已經為我們設計好了一套Schema和對應的DAO對象。但在使用之前,我們需要先對相關的類有一定的了解。
4.1 相關接口
Spring Cloud Security OAuth2通過DefaultTokenServices
類來完成token生成、過期等 OAuth2 標准規定的業務邏輯,而DefaultTokenServices
又是通過TokenStore
接口完成對生成數據的持久化。在上面的demo中,TokenStore
的默認實現為InMemoryTokenStore
,即內存存儲。 對於Client信息,ClientDetailsService
接口負責從存儲倉庫中讀取數據,在上面的demo中默認使用的也是InMemoryClientDetialsService
實現類。說到這里就能看出,要想使用數據庫存儲,只需要提供這些接口的實現類即可。慶幸的是,框架已經為我們寫好JDBC實現了,即JdbcTokenStore
和JdbcClientDetailsService
。
4.2 建表
要想使用這些JDBC實現,首先要建表。框架為我們提前設計好了schema, 在github上:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
在使用這套表結構之前要注意的是,對於MySQL來說,默認建表語句中主鍵是varchar(255)類型,在mysql中執行會報錯,原因是mysql對varchar主鍵長度有限制。所以這里改成128即可。其次,語句中會有某些字段為LONGVARBINARY
類型,它對應mysql的blob
類型,也需要修改一下。
4.3 配置
數據庫建好后,下一步就是配置框架使用JDBC實現。方法還是編寫@Configuration
類繼承AuthorizationServerConfigurerAdapter
:
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@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);
}
完成這些后,框架就會將中間產生的數據寫到mysql中了。oauth_client_details
是client表,可以直接在該表中添加記錄來添加client:
4.4 需要注意的地方
這里不得不說 Spring 設計有一個奇葩地的方。注意看oauth_access_token
表是存放訪問令牌的,但是並沒有直接在字段中存放token。Spring 使用OAuth2AccessToken
來抽象與令牌有關的所有屬性,在寫入到數據庫時,Spring將該對象通過JDK自帶的序列化機制序列成字節 直接保存到了該表的token
字段中。也就是說,如果只看數據表你是看不出access_token
的值是多少,過期時間等信息的。這就給資源服務器的實現帶來了麻煩。我們的資源提供方並沒有使用Spring Security,也不想引入 Spring Security 的任何依賴,這時候就只能將 DefaultOAuth2AccessToken
的源碼copy到資源提供方的項目中,然后讀取token
字段並反序列化還原對象來獲取token信息。但是如果這樣做還會遇到反序列化兼容性的問題,具體解決方法參考我另一篇博文: http://blog.csdn.net/neosmith/article/details/52539614
5. 總結
至此一個能在生產環境下使用的授權服務就搭建好了。其實我們在實際使用時應該適當定制JdbcTokenStore
或ClientDetailsService
來實適應業務需要,甚至可以直接從0開始實現接口,完全不用框架提供的實現。另外,Spring 直接將DefaultOAuth2AccessToken
序列化成字節保存到數據庫中的設計,我認為是非常不合理的。或許設計者的初衷是保密access_token
,但是通過加密的方法也可以實現,完全不應該直接扔字節。不過通過定制TokenStore
接口,我們可以使用自己的表結構而不拘泥於默認實現。
6. 個人看法
Spring的OAuth2實現有些過於復雜了,oauth2本身只是個非常簡單的協議,完全可以自己在SpringMVC的基礎上自由實現,沒有難度,也不復雜。我想很多人去用框架應該是擔心oauth2協議復雜實現起來健壯性不足,其實是多慮了。如果是開發我個人的項目,我肯定會不使用任何框架。
github地址: https://github.com/wanghongfei/spring-security-oauth2-example