OAuth2 单点登录
摘要: 原创出处 http://www.iocoder.cn/Spring-Security/OAuth2-learning-sso/ 「芋道源码」欢迎转载,保留摘要,谢谢!
本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labs 的 lab-68-spring-security-oauth 目录。
1. 概述
在前面的文章中,我们学习了 Spring Security OAuth 的简单使用。
今天我们来搞波“大”的,通过 Spring Security OAuth 实现一个单点登录的功能。
可能会有女粉丝不太了解单点登录是什么?单点登录,英文是 Single Sign On,简称为 SSO,指的是当有多个系统需要登录时,用户只需要登录一个统一的登录系统,而无需在多个系统重复登录。
举个最常见的例子,我们在浏览器中使用阿里“全家桶”:
求助信:麻烦有认识阿里的胖友,让他们给打下钱。。。
我们只需要在统一登录系统(https://login.taobao.com)进行登录即可,而后就可以“愉快”的自由剁手,并且无需分别在淘宝、天猫、飞猪等等系统重新登录。
友情提示:更多单点登录的介绍,可见《维基百科 —— 单点登录》。
下面,我们正式搭建 Spring Security OAuth 实现 SSO 的示例项目,如下图所示:
创建 lab-68-demo21-authorization-server-on-sso
项目,作为统一登录系统。
旁白君:机智的胖友,是不是发现这个项目和授权服务器非常相似!!!
创建 lab-68-demo21-resource-server-on-sso
项目,模拟需要登录的 XXX 系统。
旁白君:机智的胖友,是不是发现这个项目和资源服务器非常相似!!!
2. 搭建统一登录系统
示例代码对应仓库:
创建 lab-68-demo21-authorization-server-on-sso
项目,作为统一登录系统。
友情提示:整个实现代码,和我们前文看到的授权服务器是基本一致的。
2.1 初始化数据库
在 resources/db
目录下,有四个 SQL 脚本,分别用于初始化 User 和 OAuth 相关的表。
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-id
和 client-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 的整体流程图,胖友可以继续深入理解下:
后续,想要深入的胖友,可以看看 Spring Security OAuth 提供的如下两个过滤器: