OAuth2 集成 - 构造认证授权服务器
1. 前言
上一节中,我们使用了 Spring Security 提供的社交化组件,实现了利用第三方认证平台完成用户身份识别的过程。
虽然使用第三方平台作为认证中心十分的方便,但是如果我们的系统是在内部环境下使用,或者我们的用户没有注册过 Github、微信这类平台,又或者我们希望自己的平台为其他应用提供认证服务时,就需要考虑创建自己的认证中心了。
本节将重点讨论如何创建自己的 OAuth2 认证中心。
本小节实例开发环境:
本小节所使用的实例代码是基于 Spring 官网中提供的最小化 HelloWorld 模板创建,请点此下载完整的 HelloWorld 模板压缩包。
2. OAuth2 授权原理介绍
OAuth 的全称为 Open Authorization 即「开放授权」。它被设计成为一个通用安全协议,用于实现桌面应用(包括手机应用)及 B / S 应用的统一 API 鉴权服务。它通过颁发令牌的方式,允许第三方网站在特定时间、操作范围内访问资源而避免了重新输入密码。
OAuth 授权有三个主要特点:
- 简单:易于理解和实现;
- 安全:过程中不暴露敏感信息(如:用户名、密码等);
- 开放:谁都可以用。
OAuth 标准一共出现了两代,1.0 在 2007 年底提出,只适用于浏览器 B / S 应用,后来在 2011 年,OAuth 发布了协议 2.0,并开始支持终端应用的认证。现在 OAuth 2.0 协议基本完全替代了 OAuth 1.0 协议。
OAuth 2.0 有四种授权方式,也就是四种获得令牌的方式,分别是:授权码式、隐蔽式、密码式、客户端凭证式。
2.1 授权码式
这种方式下,APP(或网站)首先申请授权码并保存在客户端(或浏览器)中,再用授权码去换取令牌,并将令牌保存在服务器上。这样就实现了即认证客户端,又认证了服务器。
2.2 隐蔽式
有时会遇到应用只有前端,没有后端,上述方式就无法实现了,此时我们需要将令牌保存在前端,于是出现了第二种方式:隐蔽式。
这种方式下应用客户端直接向认证服务器请求令牌。
注意:这种方式下,令牌被存储在客户端,容易被攻击者拦截,所以用完后应及时销毁。
2.3 密码式
加入客户端应用实可信的,既用户允许客户端知道自己的用户名密码,此时就可以使用密码式换取令牌。
这种方式下,由客户端认证用户,并携带用户的认证信息一并发送到认证服务器换取令牌。
2.4 客户端凭证式
有的应用并没有明确的前端应用,比如控制台程序或者是服务接口,这种情况下就需要用到客户端凭证式获得凭证了。
这种方式下,没有「人」的参与,只有认证服务对后台服务的认证。
3. 过程实现
在前面章节,我们讨论了如何快速建立一个 Spring Security 的认证服务器,此处我们将在前述实例上扩展 OAuth2.0 认证支持。
3.1 创建 Spring Boot web 服务端应用
工程目录结构如下:
▾ OAuth2AuthorizationServer/
▾ src/
▾ main/
▾ java/imooc/springsecurity/oauth2/server/
▾ config/
OAuth2ServerConfiguration.java # OAuth2 相关配置类
UserConfiguration.java # 基础认证配置类,用于配置用户信息
OAuth2AuthorizationServerApplication.java # 程序入口
▾ resources/
application.properties # 配置文件,本例中无特殊配置
▾ test/java/
pom.xml
在 pom.xml 文件中增加依赖项,相比「用户名密码认证实例」,此处注意添加了 OAuth2 自动配置的相关依赖。spring-security-oauth2-autoconfigure
。完整 pom.xml 文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>imooc.springsecurity</groupId>
<artifactId>OAuth2AuthorizationServerSample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
创建 SpringSecurity OAuth2 配置类: OAuth2ServerConfiguration.java。
src/
main/
java/
imooc/
springsecurity/
oauth2/
server/
OAuth2ServerConfiguration.java
- 使其继承
org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
类,并其增加@EnableAuthorizationServer
标签,以声明此类作为 OAuth2 认证服务器的配置依据; - 在
configure(AuthorizationServerEndpointsConfigurer endpoints)
方法中配置其 TokenStore,为了便于演示,此例中 TokenStore 采用内存形式,账户信息写死在代码中; - 在
configure(ClientDetailsServiceConfigurer clients)
方法中为 OAuth2 认证服务器设置可用于认证的客户端信息。
完整代码如下:
package imooc.springsecurity.oauth2.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
@EnableAuthorizationServer
@Configuration
public class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager authenticationManager;
public OAuth2ServerConfiguration(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
// 配置授信客户端信息
clients.inMemory() // 内存模式
.withClient("reader") // 第一个客户端用户,其名称为「reader」
.authorizedGrantTypes("password") // 授权模式为「password」
.secret("{noop}secret") // 认证密码为「secret」,加密方式为「NoOp」
.scopes("message:read") // 权限的使用范围
.accessTokenValiditySeconds(600_000_000) // 票据有效期
.and() // 增加第二个授权客户端,设置方法一致,但拥有不同的范围权限
.withClient("writer")
.authorizedGrantTypes("password")
.secret("{noop}secret")
.scopes("message:write")
.accessTokenValiditySeconds(600_000_000);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(this.authenticationManager)
.tokenStore(tokenStore()); // 使用虚机内存存储票据信息,也可替换成 Mysql、Redis 等。
}
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
除了设置授权客户端之外,还要增加客户端中被授权的用户。
创建类 UserConfiguration.java
src/
main/
java/
imooc/
springsecurity/
oauth2/
server/
UserConfiguration.java
并在其中配置用户信息。
package imooc.springsecurity.oauth2.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class UserConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated() // 任何地址都受到保护,需要首先认证
.and()
.httpBasic() // 支持基本认证,因为 OAuth2 认证往往用于不同种类客户端,所以基本认证支持是必要的。
.and()
.csrf().disable();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("admin").password("$2a$10$sR.KWdKOWYseh0KVHHnzMOveh/S7wvOkd.JrTyP2AzHhEcCSZfAmK").roles("USER").build()); // 用户名: admin; 密码: 123456
return inMemoryUserDetailsManager;
}
}
3.2 运行及测试
我们用 curl 工具测试 OAuth2.0 认证服务器。
在 OAuth2.0 框架中,实现 Password 认证需要提供四个参数:
- 客户端标识:clientID;
- 客户端认证密码:clientSecret;
- 授权类型:grant_type,该值固定为「password」;
- 认证用户的用户名:username;
- 认证用户的密码:password。
完整的请求表达式为:
curl [clientID]:[clientSecret]@ip:port/oauth/token -d grant_type=password -d username=[username] -d password=[password]
在本实例中,测试指令可定义为:
curl reader:secret@localhost:8080/oauth/token -d grant_type=password -d username=admin -d password=123456
如果认证成功,服务端将返回以下内容:
{
"access_token": "OOwNfgjvJKHItYnk4buWC8BMGtU=",
"token_type": "bearer",
"expires_in": 599995027,
"scope": "message:read"
}
其中,access_token 值在 OAuth2 体系中作为统一票据,用于各个资源服务的认证。
至此,OAuth2 认证服务器的 Password 模式授权模式就已完成。Spring Security 对 OAuth2.0 的其他几种授权模式已有成熟支持,在使用时需要配置对应的客户端授权模式权限。
3.3 使用数据库作为认证源
如果使用数据库(例如:mysql)作为数据源时,需要创建 JdbcClientDetailsService
对象,并配置到 ClientDetailsServiceConfigurer
之中。具体代码为
@Bean
public ClientDetailsService clientDetailsService() {
// 新增部分,用于从数据库获取客户端信息
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
// 此处去掉内存配置项,改为 jdbc 数据源
clients.withClientDetails(clientDetailsService);
}
除此之外,还需要在书库中插入相关数据表,表结构定义如下,也可以从 spring 项目主页 中获取。
-- used in tests that use HSQL
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
create table oauth_client_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
create table oauth_access_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication LONGVARBINARY,
refresh_token VARCHAR(256)
);
create table oauth_refresh_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication LONGVARBINARY
);
create table oauth_code (
code VARCHAR(256), authentication LONGVARBINARY
);
create table oauth_approvals (
userId VARCHAR(256),
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);
-- customized oauth_client_details table
create table ClientDetails (
appId VARCHAR(256) PRIMARY KEY,
resourceIds VARCHAR(256),
appSecret VARCHAR(256),
scope VARCHAR(256),
grantTypes VARCHAR(256),
redirectUrl VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(256)
);
4. 小结
本节我们讨论了如何构建自己的 OAuth2.0 认证中心,主要知识点有:
- Spring Security 对 OAuth2.0 认证标准已经有成熟的支持,仅需几个注解就可以构造出 OAuth2.0 认证服务器;
- Spring Security 对 OAuth2.0 认证同样提供了多种认证支持,比如内存认证、数据库认证及 Redis 认证等,可以根据需要进行配置或调整。
下节我们继续 OAuth2.0 话题,讨论如何在 OAuth2.0 协议下,如何保护私密资源被合理访问。