《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Spring-Security/OAuth2-learning-sso/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labslab-68-spring-security-oauth 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

在前面的文章中,我们学习了 Spring Security OAuth 的简单使用。

今天我们来搞波“大”的,通过 Spring Security OAuth 实现一个单点登录的功能。

可能会有粉丝不太了解单点登录是什么?单点登录,英文是 Single Sign On,简称为 SSO,指的是当有多个系统需要登录时,用户只需要登录一个统一的登录系统,而无需在多个系统重复登录。

举个最常见的例子,我们在浏览器中使用阿里“全家桶”:

求助信:麻烦有认识阿里的胖友,让他们给打下钱。。。

我们只需要在统一登录系统https://login.taobao.com)进行登录即可,而后就可以“愉快”的自由剁手,并且无需分别在淘宝、天猫、飞猪等等系统重新登录。

登录系统

友情提示:更多单点登录的介绍,可见《维基百科 —— 单点登录》

下面,我们正式搭建 Spring Security OAuth 实现 SSO 的示例项目,如下图所示:

项目结构

2. 搭建统一登录系统

示例代码对应仓库:

创建 lab-68-demo21-authorization-server-on-sso 项目,作为统一登录系统

友情提示:整个实现代码,和我们前文看到的授权服务器是基本一致的。

2.1 初始化数据库

resources/db 目录下,有四个 SQL 脚本,分别用于初始化 User 和 OAuth 相关的表。

SQL 脚本

2.1.1 初始化 OAuth 表

① 执行 oauth_schema.sql 脚本,创建数据库表结构

drop table if exists oauth_client_details;
create table oauth_client_details (
client_id VARCHAR(255) PRIMARY KEY,
resource_ids VARCHAR(255),
client_secret VARCHAR(255),
scope VARCHAR(255),
authorized_grant_types VARCHAR(255),
web_server_redirect_uri VARCHAR(255),
authorities VARCHAR(255),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(255)
);

create table if not exists oauth_client_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255),
client_id VARCHAR(255)
);

create table if not exists oauth_access_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255),
client_id VARCHAR(255),
authentication LONG VARBINARY,
refresh_token VARCHAR(255)
);

create table if not exists oauth_refresh_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication LONG VARBINARY
);

create table if not exists oauth_code (
code VARCHAR(255), authentication LONG VARBINARY
);

create table if not exists oauth_approvals (
userId VARCHAR(255),
clientId VARCHAR(255),
scope VARCHAR(255),
status VARCHAR(10),
expiresAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
lastModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

结果如下图所示:

表结构

作用
oauth_access_token OAuth 2.0 访问令牌
oauth_refresh_token OAuth 2.0 刷新令牌
oauth_code OAuth 2.0 授权码
oauth_client_details OAuth 2.0 客户端
oauth_client_token
oauth_approvals

旁白君:这里的表结构设计,我们可以借鉴参考,实现自己的 OAuth 2.0 的功能。

② 执行 oauth_data.sql 脚本,插入一个客户端记录。

INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
web_server_redirect_uri, authorities, access_token_validity,
refresh_token_validity, additional_information, autoapprove)
VALUES
('clientapp', '112233', 'read_userinfo,read_contacts',
'password,authorization_code,refresh_token', 'http://127.0.0.1:9090/login', null, 3600, 864000, null, true);

注意!这条记录的 web_server_redirect_uri 字段,我们设置为 http://127.0.0.1:9090/login,这是稍后我们搭建的 XXX 系统的回调地址。

  • 统一登录系统采用 OAuth 2.0 的授权码模式进行授权。
  • 授权成功后,浏览器会跳转 http://127.0.0.1:9090/login 回调地址,然后 XXX 系统会通过授权码向统一登录系统获取访问令牌

通过这样的方式,完成一次单点登录的过程。

结果如下图所示:

 表记录

2.1.2 初始化 User 表

① 执行 user_schema.sql 脚本,创建数据库表结构

DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
`username` varchar(50) NOT NULL,
`authority` varchar(50) NOT NULL,
UNIQUE KEY `ix_auth_username` (`username`,`authority`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`password` varchar(500) NOT NULL,
`enabled` tinyint(1) NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

结果如下图所示:

表结构

作用
users 用户
authorities 授权表,例如用户拥有的角色

② 执行 user_data.sql 脚本,插入一个用户记录和一个授权记录。

INSERT INTO `authorities` VALUES ('yunai', 'ROLE_USER');

INSERT INTO `users` VALUES ('yunai', '112233', '1');

结果如下图所示:

 和  表记录

2.2 引入依赖

创建 pom.xml 文件,引入 Spring Security OAuth 依赖。

<?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">
<parent>
<artifactId>lab-68</artifactId>
<groupId>cn.iocoder.springboot.labs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>lab-68-demo21-authorization-server-on-sso</artifactId>

<properties>
<!-- 依赖相关配置 -->
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<!-- 插件相关配置 -->
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- 实现对 Spring MVC 的自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 实现对 Spring Security OAuth2 的自动配置 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>

<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency> <!-- 本示例,我们使用 MySQL -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>

</dependencies>

</project>

2.3 配置文件

创建 application.yaml 配置文件,添加数据库连接池的配置:

spring:
# datasource 数据源配置内容,对应 DataSourceProperties 配置属性类
datasource:
url: jdbc:mysql://127.0.0.1:43063/demo-68-authorization-server-sso?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root # 数据库账号
password: 123456 # 数据库密码

2.4 SecurityConfig

创建 SecurityConfig 配置类,通过 Spring Security 提供用户认证的功能。代码如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

/**
* 数据源 DataSource
*/
@Autowired
private DataSource dataSource;

@Override
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource);
}

}

友情提示:如果胖友想要自定义用户的读取,可以参考《芋道 Spring Boot 安全框架 Spring Security 入门》文章。

2.5 OAuth2AuthorizationServerConfig

创建 OAuth2AuthorizationServerConfig 配置类,通过 Spring Security OAuth 提供授权服务器的功能。代码如下:

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

/**
* 用户认证 Manager
*/
@Autowired
private AuthenticationManager authenticationManager;

/**
* 数据源 DataSource
*/
@Autowired
private DataSource dataSource;

@Bean
public TokenStore jdbcTokenStore() {
return new JdbcTokenStore(dataSource);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.tokenStore(jdbcTokenStore());
}

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.checkTokenAccess("isAuthenticated()");
}

@Bean
public ClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}

}

友情提示:如果胖友看不懂这个配置类,回到《芋道 Spring Security OAuth2 存储器》文章复习下。

2.6 AuthorizationServerApplication

创建 AuthorizationServerApplication 类,统一登录系统的启动类。代码如下:

@SpringBootApplication
public class AuthorizationServerApplication {

public static void main(String[] args) {
SpringApplication.run(AuthorizationServerApplication.class, args);
}

}

2.7 简单测试

执行 AuthorizationServerApplication 启动统一登录系统。下面,我们使用 Postman 模拟一个 Client,测试我们是否搭建成功

POST 请求 http://localhost:8080/oauth/token 地址,使用密码模式进行授权。如下图所示:

密码模式

成功获取到访问令牌,成功!

3. 搭建 XXX 系统

示例代码对应仓库:

创建 lab-68-demo21-resource-server-on-sso 项目,搭建 XXX 系统,接入统一登录系统实现 SSO 功能。

友情提示:整个实现代码,和我们前文看到的资源服务器是基本一致的。

3.1 引入依赖

创建 pom.xml 文件,引入 Spring Security OAuth 依赖。

<?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">
<parent>
<artifactId>lab-68</artifactId>
<groupId>cn.iocoder.springboot.labs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>lab-68-demo21-resource-server</artifactId>

<properties>
<!-- 依赖相关配置 -->
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<!-- 插件相关配置 -->
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- 实现对 Spring MVC 的自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 实现对 Spring Security OAuth2 的自动配置 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>

</project>

3.2 配置文件

创建 application.yaml 配置文件,添加 SSO 相关配置:

server:
port: 9090
servlet:
session:
cookie:
name: SSO-SESSIONID # 自定义 Session 的 Cookie 名字,防止冲突。冲突后,会导致 SSO 登录失败。

security:
oauth2:
# OAuth2 Client 配置,对应 OAuth2ClientProperties 类
client:
client-id: clientapp
client-secret: 112233
user-authorization-uri: http://127.0.0.1:8080/oauth/authorize # 获取用户的授权码地址
access-token-uri: http://127.0.0.1:8080/oauth/token # 获取访问令牌的地址
# OAuth2 Resource 配置,对应 ResourceServerProperties 类
resource:
token-info-uri: http://127.0.0.1:8080/oauth/check_token # 校验访问令牌是否有效的地址

server.servlet.session.cookie.name 配置项,自定义 Session 的 Cookie 名字,防止冲突。冲突后,会导致 SSO 登录失败。

友情提示:具体的值,胖友可以根据自己的喜欢设置。

security.oauth2.client 配置项,OAuth2 Client 配置,对应 OAuth2ClientProperties 类。在这个配置项中,我们添加了客户端的 client-idclient-secret

security.oauth2.client.user-authorization-uri 配置项,获取用户的授权码地址。

在访问 XXX 系统需要登录的地址时,Spring Security OAuth 会自动跳转到统一登录系统,进行统一登录获取授权

而这里配置的 security.oauth2.client.user-authorization-uri 地址,就是之前授权服务器的 oauth/authorize 接口,可以进行授权码模式的授权。

友情提示:如果胖友忘记授权服务器的 oauth/authorize 接口,建议回看下《芋道 Spring Security OAuth2 入门》「3. 授权码模式」小节。

security.oauth2.client.access-token-uri 配置项,获取访问令牌的地址。

统一登录系统完成统一登录并授权后,浏览器会跳转回 XXX 系统的回调地址。在该地址上,会调用统一登录系统security.oauth2.client.user-authorization-uri 地址,通过授权码获取到访问令牌

而这里配置的 security.oauth2.client.user-authorization-uri 地址,就是之前授权服务器的 oauth/token 接口。

security.oauth2.resource.client.token-info-uri 配置项,校验访问令牌是否有效的地址。

在获取到访问令牌之后,每次请求 XXX 系统时,都会调用 统一登录系统security.oauth2.resource.client.token-info-uri 地址,校验访问令牌的有效性,同时返回用户的基本信息

而这里配置的 security.oauth2.resource.client.token-info-uri 地址,就是之前授权服务器的 oauth/check_token 接口。


至此,我们可以发现,Spring Security OAuth 实现的 SSO 单点登录功能,是基于其授权码模式实现的。这一点,非常重要,稍后我们演示下会更加容易理解到。

3.3 OAuthSsoConfig

创建 OAuthSsoConfig 类,配置接入 SSO 功能。代码如下:

@Configuration
@EnableOAuth2Sso // 开启 Sso 功能
public class OAuthSsoConfig {

}

在类上添加 @EnableOAuth2Sso 注解,声明基于 Spring Security OAuth 的方式接入 SSO 功能。

友情提示:想要深入的胖友,可以看看 SsoSecurityConfigurer 类。

3.4 UserController

创建 UserController 类,提供获取当前用户的 /user/info 接口。代码如下:

@RestController
@RequestMapping("/user")
public class UserController {

@RequestMapping("/info")
public Authentication info(Authentication authentication) {
return authentication;
}

}

3.5 ResourceServerApplication

创建 ResourceServerApplication 类,XXX 系统的启动类。代码如下:

@SpringBootApplication
public class ResourceServerApplication {

public static void main(String[] args) {
SpringApplication.run(ResourceServerApplication.class, args);
}

}

3.6 简单测试(第一弹)

执行 ResourceServerApplication 启动 XXX 系统。下面,我们来演示下 SSO 单点登录的过程。

① 使用浏览器,访问 XXX 系统http://127.0.0.1:9090/user/info 地址。因为暂未登录,所以被重定向到统一登录系统http://127.0.0.1:8080/oauth/authorize 授权地址。

又因为在统一登录系统暂未登录,所以被重定向到统一登录系统http://127.0.0.1:8080/login 登录地址。如下图所示:

登录界面

② 输入用户的账号密码「yunai/1024」,进行统一登录系统的登录。登录完成后,进入统一登录系统http://127.0.0.1:8080/oauth/authorize 授权地址。如下图所示:

授权界面

③ 点击「Authorize」按钮,完成用户的授权。授权完成后,浏览器重定向到 XXX 系统http://127.0.0.1:9090/login 回调地址。

XX 系统的回调地址,拿到授权的授权码后,会自动请求统一登录系统,通过授权码获取到访问令牌。如此,我们便完成了 XXX 系统 的登录。

获取授权码完成后,自动跳转到登录前的 http://127.0.0.1:9090/user/info 地址,打印出当前登录的用户信息。如下图所示:

用户信息


如此,我们从统一登录系统也拿到了用户信息。下面,我们来进一步将 Spring Security 的权限控制功能来演示下。

3.7 SecurityConfig

创建 SecurityConfig 配置类,添加 Spring Security 的功能。代码如下:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启对 Spring Security 注解的方法,进行权限验证。
@Order(101) // OAuth2SsoDefaultConfiguration 使用了 Order(100),所以这里设置为 Order(101),防止相同顺序导致报错
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

在类上,增加 @EnableGlobalMethodSecurity 注解,开启对 Spring Security 注解的方法,进行权限验证。

3.8 DemoController

创建 DemoController 类,提供测试权限的功能的接口。代码如下:

@RestController
@RequestMapping("/demo")
public class DemoController {

@GetMapping("/admin-list")
@PreAuthorize("hasRole('ADMIN')") // 要求管理员 ROLE_ADMIN 角色
public String adminList() {
return "管理员列表";
}

@GetMapping("/user-list")
@PreAuthorize("hasRole('USER')") // 要求普通用户 ROLE_USER 角色
public String userList() {
return "用户列表";
}

}

因为当前登录的用户只有 ROLE_USE 角色,所以可以访问 /demo/user-list 接口,无法访问 /demo/admin-list 接口。

3.9 简单测试(第二弹)

执行 ResourceServerApplication 重启 XXX 系统。下面,我们来演示下 Spring Security 的权限控制功能。

① 使用浏览器,访问 http://127.0.0.1:9090/demo/user-list 地址,成功。如下图所示:

成功访问

② 使用浏览器,访问 http://127.0.0.1:9090/demo/admin-list 地址,失败。如下图所示:

失败访问

666. 彩蛋

至此,我们成功使用 Spring Security OAuth 实现了一个 SSO 单点登录的示例。下图,是 SSO 的整体流程图,胖友可以继续深入理解下:

SSO 流程图

后续,想要深入的胖友,可以看看 Spring Security OAuth 提供的如下两个过滤器:

文章目录
  1. 1. 1. 概述
  2. 2. 2. 搭建统一登录系统
    1. 2.1. 2.1 初始化数据库
      1. 2.1.1. 2.1.1 初始化 OAuth 表
      2. 2.1.2. 2.1.2 初始化 User 表
    2. 2.2. 2.2 引入依赖
    3. 2.3. 2.3 配置文件
    4. 2.4. 2.4 SecurityConfig
    5. 2.5. 2.5 OAuth2AuthorizationServerConfig
    6. 2.6. 2.6 AuthorizationServerApplication
    7. 2.7. 2.7 简单测试
  3. 3. 3. 搭建 XXX 系统
    1. 3.1. 3.1 引入依赖
    2. 3.2. 3.2 配置文件
    3. 3.3. 3.3 OAuthSsoConfig
    4. 3.4. 3.4 UserController
    5. 3.5. 3.5 ResourceServerApplication
    6. 3.6. 3.6 简单测试(第一弹)
    7. 3.7. 3.7 SecurityConfig
    8. 3.8. 3.8 DemoController
    9. 3.9. 3.9 简单测试(第二弹)
  4. 4. 666. 彩蛋