用過的20個高顏值登錄頁,個個都創意十足!
在項目中集成第三方登錄后,我們需要將第三方平臺的賬號與我們自己的賬號體系關聯。例如,當用戶選擇使用微信登錄時,還需綁定一個手機號。這個手機號的綁定操作實際上是將微信賬號與我們系統中的賬號進行關聯。本文將詳細介紹如何在選擇使用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 類來處理用戶登錄和數據綁定。我們首先分析了三方登錄的認證流程,并創建了必要的數據庫結構以存儲第三方用戶信息。
接著,我們定義了用戶轉換接口和具體的實現類,以便將第三方用戶信息轉換為我們自定義的用戶數據結構。為了實現數據的有效存儲,我們設計了一個服務類,用于檢查用戶是否已存在于系統中,并進行相應的保存或更新操作。