项目使用的是MySql存储, 须要先建立如下表结构:java
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
表中插入记录:mysql
# 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
这时就能够访问受权页面了:git
localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com
访问时Spring让你登录,随便输入一个用户名密码便可。 注意, 若是每次登录时输入的用户名不同,那么Spring Security会认为是不一样的用户,所以访问/token/authorize会再次显示受权页面。若是用户名一致, 则只须要受权一次github
数据库链接信息在application.properties
中配置。web
Spring Cloud Security OAuth2 是 Spring 对 OAuth2 的开源实现,优势是能与Spring Cloud技术栈无缝集成,若是所有使用默认配置,开发者只须要添加注解就能完成 OAuth2 受权服务的搭建。spring
受权服务是基于Spring Security的,所以须要在项目中引入两个依赖:sql
<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扩展。数据库
在启动类中添加@EnableAuthorizationServer
注解:json
@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"); // 容许的受权范围 }
访问受权页面:
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,若是要在生产环境中使用,还须要作更多的工做。
在上面的例子中,全部的token信息都是保存在内存中的,这显然没法在生产环境中使用(进程结束后全部token丢失, 用户须要从新受权),所以咱们须要将这些信息进行持久化操做。 把受权服务器中的数据存储到数据库中并不难,由于 Spring Cloud Security OAuth 已经为咱们设计好了一套Schema和对应的DAO对象。但在使用以前,咱们须要先对相关的类有必定的了解。
Spring Cloud Security OAuth2经过DefaultTokenServices
类来完成token生成、过时等 OAuth2 标准规定的业务逻辑,而DefaultTokenServices
又是经过TokenStore
接口完成对生成数据的持久化。在上面的demo中,TokenStore
的默认实现为InMemoryTokenStore
,即内存存储。 对于Client信息,ClientDetailsService
接口负责从存储仓库中读取数据,在上面的demo中默认使用的也是InMemoryClientDetialsService
实现类。说到这里就能看出,要想使用数据库存储,只须要提供这些接口的实现类便可。庆幸的是,框架已经为咱们写好JDBC实现了,即JdbcTokenStore
和JdbcClientDetailsService
。
要想使用这些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
类型,也须要修改一下。
数据库建好后,下一步就是配置框架使用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:
这里不得不说 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
至此一个能在生产环境下使用的受权服务就搭建好了。其实咱们在实际使用时应该适当定制JdbcTokenStore
或ClientDetailsService
来实适应业务须要,甚至能够直接从0开始实现接口,彻底不用框架提供的实现。另外,Spring 直接将DefaultOAuth2AccessToken
序列化成字节保存到数据库中的设计,我认为是很是不合理的。或许设计者的初衷是保密access_token
,可是经过加密的方法也能够实现,彻底不该该直接扔字节。不过经过定制TokenStore
接口,咱们可使用本身的表结构而不拘泥于默认实现。
Spring的OAuth2实现有些过于复杂了,oauth2自己只是个很是简单的协议,彻底能够本身在SpringMVC的基础上自由实现,没有难度,也不复杂。我想不少人去用框架应该是担忧oauth2协议复杂实现起来健壮性不足,实际上是多虑了。若是是开发我我的的项目,我确定会不使用任何框架。
github地址: https://github.com/wanghongfei/spring-security-oauth2-example