在项目中集成第三方登录后,我们需要将第三方平台的账号与我们自己的账号体系关联。例如,当用户选择使用微信登录时,还需绑定一个手机号。这个手机号的绑定操作实际上是将微信账号与我们系统中的账号进行关联。本文将详细介绍如何在选择使用Gitee进行登录时,将其与系统用户表 sys_user 进行绑定。
1. SAS三方平台认证逻辑
如前所述,在SAS中,当第三方认证成功后,会回调配置的接口 /login/oauth2/code/*。该接口会被过滤器 OAuth2LoginAuthenticationFilter 拦截并处理。在执行核心逻辑 authenticate() 方法时,会交由 OAuth2LoginAuthenticationProvider 进行处理。
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
.getAuthenticationManager()
.authenticate(authenticationRequest);
在 OAuth2LoginAuthenticationProvider#authenticate 方法中,通过 OAuth2UserService 加载用户信息:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
...
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
...
return authenticationResult;
}
loadUser 方法由 DefaultOAuth2UserService 负责实现,通过 RestTemplate 调用 Gitee 平台获取用户信息。
图片
public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
...
//构建请求
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
Map<String, Object> userAttributes = response.getBody();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
//获取用户响应
private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRequest, RequestEntity<?> request) {
try {
return this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
}
...
}
...
}
2. 实现自定义用户关联逻辑
通过对三方登录流程的分析,我们可以通过继承 DefaultOAuth2UserService 类来实现自定义的用户关联逻辑。
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
// 在这里实现用户绑定逻辑,例如与 sys_user 表进行关联
...
return oauth2User;
}
}
接下来思考一下,将第三方的账户转换成我们的自定义用户需要做哪些事?
1、首先,通过 super.loadUser 方法获取到第三方用户对象 OAuth2User。
2、由于数据结构存在差异,我们还需将 OAuth2User 转换为我们自己的用户数据结构。
3、数据转换后,需要验证第三方账号是否在系统中存在。如果不存在,则进行保存操作并关联账号;如果存在,则执行更新操作。
以下为实现过程。
2.1 存储第三方用户
首先,创建一张表用于存储第三方用户,其建表语句如下:
CREATE TABLE `oauth2_third_user` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NULL DEFAULT NULL COMMENT '用户ID',
`unique_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '第三方用户ID',
`unique_account` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`unique_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '第三方用户账号',
`platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '平台类型',
`credentials` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'token信息',
`credentials_expires_at` datetime NULL DEFAULT NULL,
`create_time` datetime NULL DEFAULT NULL COMMENT '绑定时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
在这个表中,通过字段 user_id 与我们自己的用户表 sys_user 进行关联,同时通过第三方登录平台 platform 与第三方用户 ID unique_id 来确定唯一用户。
2.2 创建接口用于将第三方用户转化成我们自己的用户
public interface OAuth2UserConvert {
/**
* 转换成自定义用户
* @param oAuth2User Oauth2用户
* @return Oauth2UnionUser
*/
Oauth2UnionUser convert(OAuth2User oAuth2User );
}
由于本文集成的是 Gitee 平台,因此需要编写一个具体的实现类用于用户转换:
public class GiteeUserConvert implements OAuth2UserConvert{
private final static String AVATAR_URL = "avatar_url";
private final static String UNIQUE_ID = "id";
private final static String ACCOUNT = "login";
private final static String NAME = "name";
private final static String EMAIL = "email";
@Override
public Oauth2UnionUser convert(OAuth2User oAuth2User) {
// 获取三方用户信息
String avatarUrl = Optional.ofNullable(oAuth2User.getAttribute(AVATAR_URL)).map(Object::toString).orElse(null);
String uniqueId = Optional.ofNullable(oAuth2User.getAttribute(UNIQUE_ID)).map(Object::toString).orElse(null);
String uniqueAccount = Optional.ofNullable(oAuth2User.getAttribute(ACCOUNT)).map(Object::toString).orElse(null);
String email = Optional.ofNullable(oAuth2User.getAttribute(EMAIL)).map(Object::toString).orElse(null);
String nickName = Optional.ofNullable(oAuth2User.getAttribute(NAME)).map(Object::toString).orElse(null);
// 转换至Oauth2ThirdAccount
Oauth2UnionUser unionUser = new Oauth2UnionUser();
unionUser.setUniqueId(uniqueId);
unionUser.setUniqueAccount(uniqueAccount);
unionUser.setAvatarUrl(avatarUrl);
unionUser.setNickName(nickName);
unionUser.setEmail(email);
unionUser.setPlatform(ThirdPlatFormEnum.GITEE.name());
return unionUser;
}
}
当然,如果需要集成多个平台,还需要创建一个上下文类,用于选择具体的接口实现进行用户转换。
@Component
@RequiredArgsConstructor
public class Oauth2UserConverterContext {
/**
* 用户转换器
*/
public Oauth2UnionUser convert(OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// 获取三方登录配置的registrationId,这里将他当做登录方式
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// 转换用户信息
Oauth2UnionUser oauth2UnionUser = this.getInstance(registrationId).convert(oAuth2User);
oauth2UnionUser.setUserNameAttributeName(userNameAttributeName);
// 获取AccessToken
OAuth2AccessToken accessToken = userRequest.getAccessToken();
oauth2UnionUser.setCredentials(accessToken.getTokenValue());
Instant expiresAt = accessToken.getExpiresAt();
if (expiresAt != null) {
LocalDateTime tokenExpiresAt = expiresAt.atZone(ZoneId.of("UTC")).toLocalDateTime();
// token过期时间
oauth2UnionUser.setCredentialsExpiresAt(tokenExpiresAt);
}
return oauth2UnionUser;
}
/**
* 获取转换器
* @param registrationId 登录类型
* @return 转换器
*/
private OAuth2UserConvert getInstance(String registrationId) {
if (Objects.isNull(registrationId)){
throw new UnsupportedOperationException("登录方式不能为空.");
}
return switch (registrationId) {
case "github" -> new GithubUserConvert();
case "gitee" -> new GiteeUserConvert();
default -> throw new IllegalStateException("Unexpected value: " + registrationId);
};
}
}
在这段代码中,通过第三方登录平台的 registrationId 来选择具体的接口实现类。
2.3 创建Oauth2ThirdService用于实现用户的存储逻辑
@Service
@RequiredArgsConstructor
public class Oauth2ThirdServiceImpl implements Oauth2ThirdService {
private final SysUserService sysUserService;
private final Oauth2ThirdUserMapper oauth2ThirdUserMapper;
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void save(Oauth2UnionUser oauth2UnionUser) {
//查询用户是否存在,通过平台和第三方的ID两个字段确定唯一用户
LambdaQueryWrapper<Oauth2ThirdUserDO> queryWrapper = Wrappers.lambdaQuery(Oauth2ThirdUserDO.class)
.eq(Oauth2ThirdUserDO::getPlatform, oauth2UnionUser.getPlatform())
.eq(Oauth2ThirdUserDO::getUniqueId, oauth2UnionUser.getUniqueId());
Oauth2ThirdUserDO oauth2ThirdUserDO = oauth2ThirdUserMapper.selectOne(queryWrapper);
//数据库如果为空,则先保存到系统用户表,然后再初始化到第三方用户表
if(oauth2ThirdUserDO == null){
Integer userId = sysUserService.saveByThirdUser(oauth2UnionUser);
Oauth2ThirdUserDO thirdUserDO = convertThirdUser(oauth2UnionUser);
thirdUserDO.setUserId(userId);
oauth2ThirdUserMapper.insert(thirdUserDO);
}else {
oauth2ThirdUserDO.setCredentialsExpiresAt(oauth2UnionUser.getCredentialsExpiresAt());
oauth2ThirdUserDO.setCredentials(oauth2UnionUser.getCredentials());
oauth2ThirdUserDO.setUpdateTime(LocalDateTime.now());
oauth2ThirdUserMapper.updateById(oauth2ThirdUserDO);
}
}
...
}
2.4 继承DefaultOAuth2UserService,用于业务流程编排
@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOauth2UnionService extends DefaultOAuth2UserService {
private final Oauth2UserConverterContext oauth2UserConverterContext;
private final Oauth2ThirdService oauth2ThirdService;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
//1、获取到远程用户信息
OAuth2User oAuth2User = super.loadUser(userRequest);
//2、转换用户信息
Oauth2UnionUser oauth2UnionUser = oauth2UserConverterContext.convert(userRequest, oAuth2User);
//3、检查是否存在并保存
oauth2ThirdService.save(oauth2UnionUser);
// 将yml配置的RegistrationId当做登录类型设置至attributes中
LinkedHashMap<String, Object> attributes = new LinkedHashMap<>(oAuth2User.getAttributes());
attributes.put("platform", oauth2UnionUser.getPlatform());
return new DefaultOAuth2User(oAuth2User.getAuthorities(), attributes, oauth2UnionUser.getUserNameAttributeName());
}
}
通过上面四步处理,当我们初次使用Gitee平台登录时,会在sys_user中先插入一条数据,然后再在oauth2_third_user表中插入第三方用户数据,这样就实现了用户数据的绑定。
图片
3. 小结
本文详细介绍了如何在项目中实现自定义的第三方用户关联逻辑,通过继承 DefaultOAuth2UserService 类来处理用户登录和数据绑定。我们首先分析了三方登录的认证流程,并创建了必要的数据库结构以存储第三方用户信息。
接着,我们定义了用户转换接口和具体的实现类,以便将第三方用户信息转换为我们自定义的用户数据结构。为了实现数据的有效存储,我们设计了一个服务类,用于检查用户是否已存在于系统中,并进行相应的保存或更新操作。