首页 慕课教程 Spring Security Spring Security OAuth2 集成构造认证授权服务器

OAuth2 集成 - 构造认证授权服务器

1. 前言

上一节中,我们使用了 Spring Security 提供的社交化组件,实现了利用第三方认证平台完成用户身份识别的过程。

虽然使用第三方平台作为认证中心十分的方便,但是如果我们的系统是在内部环境下使用,或者我们的用户没有注册过 Github、微信这类平台,又或者我们希望自己的平台为其他应用提供认证服务时,就需要考虑创建自己的认证中心了。

本节将重点讨论如何创建自己的 OAuth2 认证中心。

本小节实例开发环境

本小节所使用的实例代码是基于 Spring 官网中提供的最小化 HelloWorld 模板创建,请点此下载完整的 HelloWorld 模板压缩包

  • 编译环境:JDK 1.8,点此下载
  • 构建工具:Maven 3.5.3,点此下载
  • 开发工具:VS Code,点此下载、控制台;
  • 其他依赖性:
    • spring-security-oauth2-autoconfigure

2. OAuth2 授权原理介绍

OAuth 的全称为 Open Authorization 即「开放授权」。它被设计成为一个通用安全协议,用于实现桌面应用(包括手机应用)及 B / S 应用的统一 API 鉴权服务。它通过颁发令牌的方式,允许第三方网站在特定时间、操作范围内访问资源而避免了重新输入密码。

OAuth 授权有三个主要特点:

  1. 简单:易于理解和实现;
  2. 安全:过程中不暴露敏感信息(如:用户名、密码等);
  3. 开放:谁都可以用。

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
  1. 使其继承 org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter 类,并其增加 @EnableAuthorizationServer 标签,以声明此类作为 OAuth2 认证服务器的配置依据;
  2. configure(AuthorizationServerEndpointsConfigurer endpoints) 方法中配置其 TokenStore,为了便于演示,此例中 TokenStore 采用内存形式,账户信息写死在代码中;
  3. 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 认证需要提供四个参数:

  1. 客户端标识:clientID;
  2. 客户端认证密码:clientSecret;
  3. 授权类型:grant_type,该值固定为「password」;
  4. 认证用户的用户名:username;
  5. 认证用户的密码: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 认证中心,主要知识点有:

图片描述

OAuth 2.0 认证服务器
  • Spring Security 对 OAuth2.0 认证标准已经有成熟的支持,仅需几个注解就可以构造出 OAuth2.0 认证服务器;
  • Spring Security 对 OAuth2.0 认证同样提供了多种认证支持,比如内存认证、数据库认证及 Redis 认证等,可以根据需要进行配置或调整。

下节我们继续 OAuth2.0 话题,讨论如何在 OAuth2.0 协议下,如何保护私密资源被合理访问。