0. 导言
我们在浏览器上可以访问成百上千个网站,使用每个网站的服务一般都要先注册账号,那么我们为了更好地记忆,一般都会在多个网站使用相同的账号和密码进行注册。那么问题就来了,如果在你注册的网站中有某些个网站的系统设计不够严谨和安全,数据库的用户信息使用明文存储,那么一旦这个网站遭到攻击或者是数据泄露,那么一些不怀好意的人就可能通过使用这些泄露的数据“撞库”,刚好就登录了你使用相同账号密码注册的其他网站。那么你的权益就有可能会受到损害。
由此行内大佬们便提出了 OAuth。
1. OAuth2 简介
OAuth 是一个开放的非常重要的认证标准/协议,允许用户授权第三方应用访问其存储在其他网站上的资源,而无需将用户名密码提供给第三方网站的开放标准/协议。OAuth2 是 OAuth 的最新版本,同时也是被广泛应用的一个版本。我们在网站上常见的QQ登录,微信扫码登录,GitHub 授权登录就是基于 OAuth2.0 实现的。
官方文档:RFC 6749 - The OAuth 2.0 Authorization Framework
2. OAuth2 认证授权总体流程
整体认证授权流程如下:
从这张图中我们可以看出,整个 OAuth2 的认证授权流程中有4个不同角色:
Client:客户端,也就是第三方网站(相对于认证网站来说)。
Resource Owner:资源拥有者,也就是“我”。
Authorization Server:认证服务器或者说授权服务器。
Resource Server:资源服务器。
3. OAuth2 标准接口
/oauth/authorize:授权端点
/oauth/token:获取令牌端点
/oauth/confirm_access:用户确认授权提交端点
/oauth/error:授权服务错误信息端点
/oauth/check_token:用于资源服务访问的令牌解析端点
/oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话
4. OAuth2 四种授权模式
4.1 授权码模式
授权码模式(Authorization Code):这是最常用的授权模式。在模式下,用户通过将重定向到授权服务器来进行身份验证,并获一个授权码。然后,应程序使用授权码与授权服务器进行交互,以获取访问牌和刷新令牌。访问令牌用于问受保护的资源,而刷新令牌用于获取新的访问令牌。微信扫码登录就是典型的授权码模式。
授权码模式的授权流程如下图所示:
具体流程如下:
(A)用户访问第三方应用,第三方应用通过浏览器导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
核心参数(假设第三方网站的地址为:wx.com):
https://wx.com/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=http://www.baidu.com&scope=read
1
字段 描述
client_id 授权服务器注册应用后的唯一标识
response_type 必须 固定值 在授权码中必须为 token
redirect_uri 必须 通过客户端注册的重定向URL
scope 必须 令牌可以访问资源权限
state 可选 存在原样返回客户端 用来防止 CSRF跨站攻击
申请令牌的请求示例(地址栏中的client表示client_id,secret表示密钥值):
刷新令牌请求示例:
4.2 简化模式
简化模式(Implicit Grant):简化模式不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。其具体的授权流程如图所示
具体步骤如下:
(A)第三方应用将用户导向认证服务器。
(B)用户决定是否给于客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。#token
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端。
核心参数:
https://wx.com/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=http://www.baidu.com&scope=read
1
字段 描述
client_id 授权服务器注册应用后的唯一标识
response_type 必须 固定值 在授权码中必须为 token
redirect_uri 必须 通过客户端注册的重定向URL
scope 必须 令牌可以访问资源权限
state 可选 存在原样返回客户端 用来防止 CSRF跨站攻击
4.3 密码模式
**密码模式(Resource Owner Password Credentials Grant)**中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个相同公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
授权流程图如下所示:
具体步骤如下:
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
核心参数:
https://wx.com/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID
1
申请令牌的请求示例(地址栏中的client表示client_id,secret表示密钥值):
刷新令牌的请求示例:
4.4 客户端模式
**客户端模式(Client Credentials Grant)**指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。
具体步骤如下:
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。
https://wx.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
1
申请令牌的请求示例(地址栏中的client表示client_id,secret表示密钥值):
5. GitHub授权登录
GitHub 授权登录流程:
(1)打开 GitHub 网站,在头部导航栏点击头像,再点击 Settings。
(2)找到 Developer settings
(3)先点击左侧 OAuth Apps
,再点击 New OAuth App
(4)输入信息,点击 Register application
(5)注册成功,点击生成密钥
(6)保存好密钥,之后不会完整显示了
(7)创建一个springboot项目
依赖如下:
<?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 https://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.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hmblogs</groupId>
<artifactId>oauth-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>oauth-test</name>
<description>hmblogs</description>
<properties>
<java.version>8</java.version>
<druid.version>1.2.8</druid.version>
<log4jdbc.version>1.16</log4jdbc.version>
<es.version>7.9.2</es.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
<version>${log4jdbc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
yml配置文件如下所示:
server:
port: 8080
servlet.context-path: /
spring:
security:
oauth2:
client:
registration:
github:
client-id: Iv1.3a0f77a8ae06e98c
client-secret: deb634113d4bd06a735437ad3f2b95f1ba83f9e4
redirect-uri: http://localhost:8080/login/oauth2/code/github
datasource:
druid:
db-type: com.alibaba.druid.pool.DruidDataSource
driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
url: jdbc:log4jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:hmblogs}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
username: ${DB_USER:root}
password: ${DB_PWD:123456}
创建一个配置类(这是新版本SpringSecurity的配置写法,废弃了WebSecurityConfigurerAdapter):
package com.hmblogs.backend.util;
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.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login();//使用 oauth2 认证
// ...
return http.build();
}
}
再创建一个控制器类:
package com.hmblogs.backend.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public DefaultOAuth2User hello(){
System.out.println("hello ");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (DefaultOAuth2User) authentication.getPrincipal();
}
}
启动项目,在浏览器中访问“http://localhost:8080”,跳转到github授权端点。