PigX 环境说明

环境说明

环境组件 版本 备注
PigX 5.9
JDK 17

概述

本文详细介绍如何在 Spring Authorization Server 中实现自定义认证模式,以邮件验证码登录为例,展示完整的自定义认证流程实现。

模块架构图

背景与目标

在传统的 OAuth2 授权流程中,通常使用用户名密码进行身份验证。但在某些业务场景下,我们可能需要使用其他方式进行身份验证,比如:

  • 邮件验证码登录
  • 短信验证码登录
  • 第三方平台登录
  • 生物识别登录

本文将演示如何基于 Spring Authorization Server 实现邮件验证码登录的自定义认证模式,为其他自定义认证方式提供参考模板。

实现架构

自定义认证模式主要涉及以下核心组件:

  1. 认证令牌 (Authentication Token) - 定义自定义的认证令牌类型
  2. 认证转换器 (Authentication Converter) - 处理请求参数并转换为认证令牌
  3. 认证提供者 (Authentication Provider) - 执行实际的认证逻辑
  4. 用户详情服务 (User Details Service) - 根据认证信息加载用户详情
  5. 授权服务器配置 - 注册自定义认证模式

核心实现

1. 认证令牌 (Authentication Token)

首先定义自定义的认证令牌类,用于封装邮件登录的认证信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 邮件验证码认证令牌
* 继承自 OAuth2ResourceOwnerBaseAuthenticationToken,用于邮件登录流程
*/
public class OAuth2ResourceOwnerEmailAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {

/**
* 构造函数 - 固定写法,无需修改
* @param authorizationGrantType 授权类型,这里为 "email"
* @param clientPrincipal 客户端认证信息
* @param scopes 请求的权限范围
* @param additionalParameters 额外参数,包含邮件地址等信息
*/
public OAuth2ResourceOwnerEmailAuthenticationToken(AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal, Set<String> scopes, Map<String, Object> additionalParameters) {
super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
}
}

关键点说明:

  • 继承 OAuth2ResourceOwnerBaseAuthenticationToken 基类
  • 构造函数参数固定,无需自定义修改
  • 主要用于标识这是一个邮件认证类型的令牌

2. 认证转换器 (Authentication Converter)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class OAuth2ResourceOwnerEmailAuthenticationConverter
extends OAuth2ResourceOwnerBaseAuthenticationConverter<OAuth2ResourceOwnerEmailAuthenticationToken> {

// 重要 支持email模式
@Override
public boolean support(String grantType) {
return "email".equals(grantType);
}

// 校验参数 email不能为空
@Override
public void checkParams(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// email (REQUIRED)
String email = parameters.getFirst("email");
if (!StringUtils.hasText(email) || parameters.get("email").size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "email",
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
}

// ========== 下面是固定写法 =================
@Override
public OAuth2ResourceOwnerEmailAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes,
Map additionalParameters) {
return new OAuth2ResourceOwnerEmailAuthenticationToken(new AuthorizationGrantType("email"), clientPrincipal,
requestedScopes, additionalParameters);
}

}

3. 认证提供者 (Authentication Provider)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class OAuth2ResourceOwnerEmailAuthenticationProvider
extends OAuth2ResourceOwnerBaseAuthenticationProvider<OAuth2ResourceOwnerEmailAuthenticationToken> {

// 获取请求中的 email 参数, 用于构建 UsernamePasswordAuthenticationToken
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String email = (String) reqParameters.get("email");
return new UsernamePasswordAuthenticationToken(email, null);
}

// 判断是否支持此 Authentication Token
public boolean supports(Class<?> authentication) {
return OAuth2ResourceOwnerEmailAuthenticationToken.class.isAssignableFrom(authentication);
}

// ========== 下面是固定写法 =================
public OAuth2ResourceOwnerEmailAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
super(authenticationManager, authorizationService, tokenGenerator);
}

public void checkClient(RegisteredClient registeredClient) {
}

}

授权服务器配置

注册邮件登录自定义授权方式

AuthorizationServerConfiguration 中注册自定义的邮件认证模式:

授权服务器配置

配置要点:

  • 注册自定义的认证转换器
  • 注册自定义的认证提供者
  • 确保授权类型 “email” 被正确识别

资源服务配置

用户详情服务

PigxEmailUserDetailServiceImpl

Spring Security 回调业务的入口,负责根据邮件地址加载用户信息:

核心功能:

  • 验证邮件验证码
  • 根据邮件地址查询用户信息
  • 构造用户详情对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Slf4j
@RequiredArgsConstructor
public class PigxEmailUserDetailServiceImpl implements PigxUserDetailsService {

private final UserDetailsService pigxDefaultUserDetailsServiceImpl;

private final RemoteUserService remoteUserService;

@Override
public UserDetails loadUserByUsername(String email) {
// 获取用户传递的验证码
String code = WebUtils.getRequest().getParameter("code");
// 校验验证码 , 验证码和发送的保持不一致 throw new OAuth2AuthenticationException("验证码错误");

// 根据 email 查询 UPMS 用户信息 构造用户信息
R<UserInfo> result = remoteUserService.emailinfo(email);
return getUserDetails(RetOps.of(result).getData());
}

// 自定义的 email 授权方式都走这个
public boolean support(String clientId, String grantType) {
return "email".equals(grantType);
}

// === 固定写法 ===
public UserDetails loadUserByUser(PigxUser pigxUser) {
return pigxDefaultUserDetailsServiceImpl.loadUserByUsername(pigxUser.getUsername());
}

}

SPI 文件注册

通过 SPI 机制注册自定义的 UserDetailService:

SPI 注册配置

注册步骤:

  1. META-INF/services 目录下创建配置文件
  2. 指定自定义 UserDetailService 的实现类
  3. 确保 Spring 能够正确加载和实例化服务

认证提供者配置

配置 PigxDaoAuthenticationProvider 跳过密码校验:

认证提供者配置

配置说明:

  • 针对邮件认证模式,跳过传统的密码验证
  • 直接使用邮件地址进行用户身份验证
  • 确保认证流程的完整性

测试使用

1. 数据库配置

首先在数据库中插入支持邮件认证的客户端配置:

1
2
3
4
5
6
7
8
INSERT INTO `pigxx`.`sys_oauth_client_details` 
(`id`, `client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`,
`web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`,
`additional_information`, `autoapprove`, `del_flag`, `create_by`, `update_by`,
`create_time`, `update_time`, `tenant_id`)
VALUES (111, 'emailtest', NULL, 'emailtest', 'server', 'email', NULL, NULL,
43200, 2592001, '{\"captcha_flag\":\"0\",\"enc_flag\":\"0\",\"online_quantity\":\"1\"}',
'false', '0', NULL, NULL, NULL, NULL, 1);

配置说明:

  • client_id: 客户端标识符
  • authorized_grant_types: 授权类型设置为 “email”
  • scope: 权限范围
  • access_token_validity: 访问令牌有效期(秒)

2. API 测试

使用 curl 命令测试邮件认证接口:

1
2
3
4
5
curl --location --request POST 'http://127.0.0.1:9999/auth/oauth2/token?grant_type=email&code=1234' \
--header 'TENANT-ID: 1' \
--header 'Authorization: Basic ZW1haWx0ZXN0OmVtYWlsdGVzdA==' \
--data-urlencode 'email=sw@pigx.vip' \
--data-urlencode 'scope=server'

请求参数说明:

  • grant_type=email: 指定使用邮件认证模式
  • code=1234: 邮件验证码
  • email=sw@pigx.vip: 用户邮件地址
  • scope=server: 请求的权限范围
  • Authorization: 客户端认证信息(Base64 编码)

预期响应:

1
2
3
4
5
6
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 43200,
"scope": "server"
}