前言
上文我们介绍了oauth2.0的相关理论和流程,本文我们继续实现。
Oauth2协议中有个很重要的概念,叫做”端点“,
以下整理了一些常用的端点及其参考访问路径及使用场景的信息,供参考。
这些端点在oauth2.0协议的整个生命周期中扮演着非常重要的角色。
端点说明 | 参考值 | 使用场景 |
---|---|---|
访问授权端点 | /oauth2/authorize | 当客户端需要请求用户的授权时,会将用户重定向到这个端点。用户在这里进行登录验证,并决定是否授权给客户端。一旦用户同意授权,授权服务器会重定向用户回客户端预先注册的重定向URI,携带一个授权码或直接的访问令牌(取决于所使用的授权类型) |
获取令牌端点 | /oauth2/token | 客户端使用这个端点来交换授权码、用户密码、客户端证书等凭证,以获取访问令牌和刷新令牌。客户端在请求时通常需要进行身份验证,以证明自己有权请求令牌。 |
用户信息端点 | /oauth2/userInfo | 客户端使用访问令牌访问这个端点,以获取授权用户的基本信息,如用户名、电子邮件地址等。这通常用于获取用户个人资料,以便于个性化用户体验或进行进一步的业务逻辑处理。 |
撤销令牌端点 | /oauth2/revoke | 当访问令牌或刷新令牌不再需要或应当被撤销时,客户端或资源所有者可以使用这个端点来撤销令牌。这有助于保护用户数据的安全,防止令牌被滥用。 |
校验令牌端点 | /oauth2/introspect | 资源服务器使用这个端点来检查访问令牌的有效性和属性,以决定是否允许客户端访问受保护资源。这有助于确保只有有效的令牌才能访问资源。 |
公钥端点 | /oauth2/jwks | 如果使用JWT(JSON Web Tokens)作为令牌,客户端可以使用这个端点来获取用于验证JWT签名的公钥。这对于确保令牌未被篡改非常重要。 |
刷新令牌端点 | /oauth2/token | 实际上,刷新令牌的功能通常是在令牌端点中实现的。客户端使用刷新令牌来获取新的访问令牌,当原始的访问令牌过期时,这允许客户端无需重新进行用户授权即可继续访问资源。 |
实现一个oauth2.0服务器服务就需要提供以上这些端点的实现,本文我们实现一个基于Oauth2.0的认证服务器,并通过验证这些端点来验证其完整性和可用性。
准备
本文所依赖的环境如下:
环境/工具名称 | 版本 |
---|---|
Java | 17 |
Springboot | 3.2.2 |
MybatisPlus | 3.5.5 |
Authorization-Server | 1.3.1 |
Spring | 6.3.3 |
SpringSecurity | 6.3.3 |
Maven | 3.6.5 |
MySql | 8.0.33 |
开始
1. 新建springboot+maven项目
2. 修改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>
<groupId>com.zjtx.tech.security</groupId>
<artifactId>springboot3-sso</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>public</id>
<name>Aliyun Public Repository</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring</id>
<name>Aliyun Spring Repository</name>
<url>https://maven.aliyun.com/repository/spring</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>maven</id>
<name>Maven Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>sonatype</id>
<name>Sonatype Repository</name>
<url>https://s01.oss.sonatype.org/content/groups/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<finalName>sso-server</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.5</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3. 添加认证服务器配置文件
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.zjtx.tech.security.demo.provider.MyAuthenticationEntryPoint;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JdbcTemplate jdbcTemplate;
@Value("${spring.security.oauth2.authorization-server.issuer}")
private String issuer;
@Resource
private TokenAuthenticationFilter jwtTokenAuthenticationFilter;
@Resource
private MyAuthenticationEntryPoint authenticationEntryPoint;
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
// 配置OAuth2授权服务器的默认安全设置
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 配置授权端点和OIDC,默认设置
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.authorizationEndpoint(Customizer.withDefaults())
.oidc(Customizer.withDefaults());
// 配置异常处理,包括访问被拒绝和未认证的处理方式
http.exceptionHandling((exceptions) -> exceptions
.accessDeniedHandler((request, response, accessDeniedException) ->
accessDeniedException.printStackTrace())
.authenticationEntryPoint(authenticationEntryPoint))
// 配置OAuth2资源服务器,使用JWT令牌验证
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
// 构建并返回安全配置
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers("/css/**", "/js/**", "/images/**",
"/webjars/**", "/favicon.ico", "/login.html");
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
// 配置HTTP安全设置,包括授权规则、CORS、会话管理、过滤器、表单登录、CSRF保护和异常处理
http
// 设置对特定路径的请求不需要认证,如/auth/**和/oauth2/**的请求
.authorizeHttpRequests((authorize) ->
authorize.requestMatchers(
new AntPathRequestMatcher("/auth/**"),
new AntPathRequestMatcher("/oauth2/**"))
.permitAll()
// 设置任何其他请求都需要认证
.anyRequest().authenticated())
// 配置CORS,使用默认设置
.cors(Customizer.withDefaults())
// 设置总是创建会话,确保每次请求都有会话
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
// 在UsernamePasswordAuthenticationFilter之前添加自定义的JWT令牌认证过滤器
.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 配置表单登录,使用默认设置
.formLogin(Customizer.withDefaults())
// 禁用CSRF保护
.csrf(AbstractHttpConfigurer::disable)
// 配置异常处理,设置未认证的请求跳转到登录页面
.exceptionHandling((exceptions) -> {
exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login.html"));
});
// 构建并返回安全配置对象
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("oauth2-client")
.clientSecret(passwordEncoder.encode("123456"))
// 客户端认证基于请求头
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 配置授权的支持方式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("https://www.baidu.com")
.scope("user")
.scope("admin")
// 客户端设置,设置用户需要确认授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(false) .build())
.build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());
if (repositoryByClientId == null) {
registeredClientRepository.save(registeredClient);
}
return registeredClientRepository;
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
// 配置 JWK 源
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
// 生成 RSA 密钥对
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
/**
* 配置 JWT 解码器
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 配置授权服务器设置
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer(issuer).build();
}
/**
* 配置认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* 配置安全上下文存储库
*/
@Bean
public SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
/**
* 配置授权服务
*/
@Bean
public OAuth2AuthorizationService auth2AuthorizationService(RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 配置授权同意服务
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
}
针对上述配置,作如下说明:
- OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http):应用默认的安全配置。
- http.getConfigurer(OAuth2AuthorizationServerConfigurer.class):配置授权端点和OIDC的默认设置, 开启了OIDC后会开启一些节点,如/oauth2/jwks, /.well-known/openid-configuration
- http.exceptionHandling((exceptions) -> exceptions:配置异常处理,包括访问被拒绝和未认证的处理方式。
- http.oauth2ResourceServer((resourceServer) -> resourceServer:配置OAuth2资源服务器,使用JWT令牌验证。
- webSecurityCustomizer():定义了一个Web安全自定义器,用于忽略某些请求路径的安全检查。
- defaultSecurityFilterChain(http):配置HTTP安全设置,包括授权规则、CORS、会话管理、过滤器、表单登录、CSRF保护和异常处理。
- registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder):配置注册客户端存储库。
- jwkSource():配置JWK源,用于生成RSA密钥对。
- jwtDecoder(JWKSource jwkSource):配置JWT解码器。
- authorizationServerSettings():配置授权服务器设置。
- authenticationManager(AuthenticationConfiguration authenticationConfiguration):配置认证管理器。
- securityContextRepository():配置安全上下文存储库。
- auth2AuthorizationService(RegisteredClientRepository registeredClientRepository):配置授权服务。
- authorizationConsentService(RegisteredClientRepository registeredClientRepository):配置授权同意服务。
4. 添加配置文件
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://ip:port/dbname?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: username
password: password
security:
oauth2:
authorization-server:
issuer: http://localhost:8080
logging:
level:
org.springframework.security: info
数据库脚本文件:
CREATE TABLE oauth2_registered_client (
id varchar(100) NOT NULL,
client_id varchar(100) NOT NULL,
client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
client_secret varchar(200) DEFAULT NULL,
client_secret_expires_at timestamp DEFAULT NULL,
client_name varchar(200) NOT NULL,
client_authentication_methods varchar(1000) NOT NULL,
authorization_grant_types varchar(1000) NOT NULL,
redirect_uris varchar(1000) DEFAULT NULL,
post_logout_redirect_uris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
client_settings varchar(2000) NOT NULL,
token_settings varchar(2000) NOT NULL,
PRIMARY KEY (id)
);
/*
IMPORTANT:
If using PostgreSQL, update ALL columns defined with 'blob' to 'text',
as PostgreSQL does not support the 'blob' data type.
*/
CREATE TABLE oauth2_authorization (
id varchar(100) NOT NULL,
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorization_grant_type varchar(100) NOT NULL,
authorized_scopes varchar(1000) DEFAULT NULL,
attributes blob DEFAULT NULL,
state varchar(500) DEFAULT NULL,
authorization_code_value blob DEFAULT NULL,
authorization_code_issued_at timestamp DEFAULT NULL,
authorization_code_expires_at timestamp DEFAULT NULL,
authorization_code_metadata blob DEFAULT NULL,
access_token_value blob DEFAULT NULL,
access_token_issued_at timestamp DEFAULT NULL,
access_token_expires_at timestamp DEFAULT NULL,
access_token_metadata blob DEFAULT NULL,
access_token_type varchar(100) DEFAULT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
oidc_id_token_value blob DEFAULT NULL,
oidc_id_token_issued_at timestamp DEFAULT NULL,
oidc_id_token_expires_at timestamp DEFAULT NULL,
oidc_id_token_metadata blob DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
refresh_token_expires_at timestamp DEFAULT NULL,
refresh_token_metadata blob DEFAULT NULL,
user_code_value blob DEFAULT NULL,
user_code_issued_at timestamp DEFAULT NULL,
user_code_expires_at timestamp DEFAULT NULL,
user_code_metadata blob DEFAULT NULL,
device_code_value blob DEFAULT NULL,
device_code_issued_at timestamp DEFAULT NULL,
device_code_expires_at timestamp DEFAULT NULL,
device_code_metadata blob DEFAULT NULL,
PRIMARY KEY (id)
);
CREATE TABLE oauth2_authorization_consent (
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorities varchar(1000) NOT NULL,
PRIMARY KEY (registered_client_id, principal_name)
);
5. resources下的静态页面
本文目前的配置是前后端不分离场景的实现,也就是说它的登录页面其实是在resources文件夹下的。
如果需要做前后端分离的实现的话需要给前端返回401,由前端重定向到登录页面即可。
这里给出简单示例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>统一身份认证-登陆</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="format-detection" content="telephone=no">
<link rel="stylesheet" href="./js/layui/css/layui.css" />
<link rel="stylesheet" href="./css/layui-blue.css" />
<script type="text/javascript" src="./js/layui/layui.all.js" ></script>
<script type="text/javascript" src="./js/common.js" ></script>
<!--[if lt IE 9]>
<script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script>
<script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="main-body">
<div class="login-main">
<div class="login-top">
<span>统一身份认证平台</span>
<span class="bg1"></span>
<span class="bg2"></span>
</div>
<form class="layui-form login-bottom" id="loginForm">
<div class="center">
<div class="item">
<span class="icon icon-2"></span>
<input type="text" name="username" lay-verify="required" placeholder="请输入登录账号" maxlength="24"/>
</div>
<div class="item">
<span class="icon icon-3"></span>
<input type="password" name="password" lay-verify="required" placeholder="请输入密码" maxlength="20">
<span class="bind-password icon icon-4"></span>
</div>
</div>
<div class="layui-form-item" style="text-align:center; width:100%;height:100%;margin:0;">
<button class="login-btn" lay-submit lay-filter="login">立即登录</button>
</div>
</form>
</div>
</div>
<div class="footer">
这里是版权信息,请自行替换
</div>
<script>
console.log("lay-ui:", layui)
// 获取URL中的查询字符串
const urlParams = new URLSearchParams(window.location.search);
// 通过参数名获取参数值
const redirectUri = urlParams.get('redirect_uri');
console.log('redirectUri: ' + redirectUri);
layui.use(['form','jquery'], function () {
var $ = layui.$, form = layui.form, layer = layui.layer;
// 登录过期的时候,跳出ifram框架
if (top.location != self.location) top.location = self.location;
$('.bind-password').on('click', function () {
if ($(this).hasClass('icon-5')) {
$(this).removeClass('icon-5');
$("input[name='password']").attr('type', 'password');
} else {
$(this).addClass('icon-5');
$("input[name='password']").attr('type', 'text');
}
});
$('.icon-nocheck').on('click', function () {
if ($(this).hasClass('icon-check')) {
$(this).removeClass('icon-check');
} else {
$(this).addClass('icon-check');
}
});
layui.form.on('submit(login)', function(data){
$.ajax({
url: '/auth/usernamePwd',
type: 'GET',
// dataType: 'json',
data: {
username: data.field.username,
password: data.field.password
},
success: function(data) {
if(data.code === 200) {
layer.msg('登录成功',{time : 1000},function(){
console.log("登录成功", data)
// location.href='./index.html';
if(redirectUri) {
location.href = redirectUri;
} else {
location.href='./test/demo';
}
})
}
if(data.code === 6000) {
layer.msg(data.msg, {time : 1000})
}
}
})
return false;
});
});
</script>
</body>
</html>
登录页的效果如下:
6. 其他说明
- 本文中涉及的一些认证授权、异常处理或者公共类相关代码在之前文章有提到过,这里不再重复。
参考文章: 【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)_springboot3 security token 登录-CSDN博客
【工作记录】基于springboot3+springsecurity实现多种方式登录及鉴权(二)_spring security实现不通过用户名密码鉴权-CSDN博客
- 上述配置中客户端的配置是通过Jdbc直接存储到数据库然后再查询出来的,这里还有一些其他的自定义的实现方式,本文仅做示例,不做限制。
测试
期望实现的效果
- 通过页面访问时跳转到登录页面,登录成功后可以正常携带code跳转到目标页面
- 上文提到的应当有的端点都可以正常访问
开始测试
流程回顾
上篇文章我们提到过oauth2.0的认证的完整流程,认证授权方式有多种,本文我们以authorize_code
为例,我们这里再回顾一下:
-
授权请求:
第三方应用(客户端)希望访问资源所有者的资源,但资源存储在资源服务器上。客户端首先构建一个URL,指向授权服务器的/authorize端点,并添加必要的参数,如client_id(客户端标识符)、response_type=code(表示请求授权码)、redirect_uri(授权后重定向的URL)以及scope(请求的权限范围)。 -
用户认证与授权:
用户被重定向到授权服务器的登录页面进行身份验证。
验证成功后,用户会被呈现一个授权页面,询问是否允许客户端应用访问其资源。
如果用户同意,授权服务器将用户重定向回客户端提供的redirect_uri,并在URL中附加一个授权码(code)作为查询参数。 -
兑换令牌:
客户端使用接收到的授权码,连同其client_id和client_secret(如果已配置),向授权服务器的/token端点发送一个HTTP POST请求,请求访问令牌(access token)和刷新令牌(refresh token)。
授权服务器验证请求后,返回一个JSON格式的响应,其中包含access_token、token_type(通常是Bearer)、expires_in(过期时间)以及可选的refresh_token。 -
访问受保护资源:
客户端现在可以使用access_token来访问资源服务器上的受保护资源。在每个请求的头部中,客户端需要包含Authorization: Bearer <access_token>这样的字段。当然还有另外一种实现就是第三方通过access_token结合
/oauth2/userinfo
端点获取用户信息并跟自己的用户体系关联,生成自己跌token并返回给前端,后续使用自己生成的token进行资源访问。 -
刷新令牌:
当access_token即将过期时,客户端可以使用refresh_token来请求一个新的access_token,而无需再次进行完整的授权流程。
刷新令牌请求同样发送到/token端点,但使用不同的参数。 -
令牌撤销:
如果不再需要访问令牌,客户端可以请求撤销access_token或refresh_token,以防止未来的访问尝试。
授权链接组装
根据oauth2.0协议的规范,我们请求/oauth2/authorize
授权端点时需要提供的参数包含: client_id
、grant_type
、scope
、redirect_uri
,还有个可选的state
参数。
正常来讲,应该是有个应用注册的页面的,为了示例,上文中我们手动新建了一个客户端应用,配置如下:
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("oauth2-client")
.clientSecret(passwordEncoder.encode("123456"))
// 客户端认证基于请求头
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 配置授权的支持方式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("https://www.baidu.com")
.scope("user")
.scope("admin")
// 客户端设置,设置用户需要确认授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(false) .build())
.build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());
if (repositoryByClientId == null) {
registeredClientRepository.save(registeredClient);
}
return registeredClientRepository;
}
在项目启动的时候会执行这些代码,简单点说就是向数据库中写入了一条客户端应用记录,如果存在的话就不做处理。应用id和密钥对应的是oauth2-client
和123456
。
如上所述,我们可以组装的授权链接格式如下:
http://localhost:8080/oauth2/authorize?client_id=oauth2-client&grant_type=code&scope=user&redirect_uri=https://www.baidu.com
在浏览器访问上述链接,可以看到能正常跳转到登录页面。
输入用户名密码并登录成功后,会跳转到授权页面,
默认授权界面并不美观,后面我们会研究下如何自定义授权页面。
点击提交后会生成一条授权记录,后续就不再需要授权。
授权成功后会携带code跳转到配置的redirect_uri地址,示例中就是https://www.baidu.com?code=xxxxxxxxxxxxxxxxxxxx
至此我们的授权和跳转就完成了,接下来就是使用code换取token,再用token获取用户信息了,这部分我们使用接口测试。
获取token接口测试
请求及响应说明
- 请求地址: /oauth2/token 对应的过滤器是
OAuth2TokenEndpointFilter
- 请求头参数中Authorization值为
Bearer base64(username:password)
- 请求体中参数grant_type为authorization_code,表示授权类型是授权码方式,code即上一步跳转时携带的code参数,redirect_uri需要和应用注册时的uri保持一致。
- 请求响应返回了access_token,refresh_token和expires_in这些常用数据,不做解释。
- 需要注意code和token的有效期
到此获取token就完成了,接下来就是通过token获取用户信息了。
写不动了,留到下一篇吧。
小结
本文在之前文章的基础上实现了sso服务端,并完成了部分测试。
针对以上内容,有任何疑问或者建议欢迎留言。
创作不易,欢迎一键三连~~~