用過的20個(gè)高顏值登錄頁,個(gè)個(gè)都創(chuàng)意十足!
在項(xiàng)目中集成第三方登錄后,我們需要將第三方平臺(tái)的賬號(hào)與我們自己的賬號(hào)體系關(guān)聯(lián)。例如,當(dāng)用戶選擇使用微信登錄時(shí),還需綁定一個(gè)手機(jī)號(hào)。這個(gè)手機(jī)號(hào)的綁定操作實(shí)際上是將微信賬號(hào)與我們系統(tǒng)中的賬號(hào)進(jìn)行關(guān)聯(lián)。本文將詳細(xì)介紹如何在選擇使用Gitee進(jìn)行登錄時(shí),將其與系統(tǒng)用戶表 sys_user 進(jìn)行綁定。
1. SAS三方平臺(tái)認(rèn)證邏輯
如前所述,在SAS中,當(dāng)?shù)谌秸J(rèn)證成功后,會(huì)回調(diào)配置的接口 /login/oauth2/code/*。該接口會(huì)被過濾器 OAuth2LoginAuthenticationFilter 攔截并處理。在執(zhí)行核心邏輯 authenticate() 方法時(shí),會(huì)交由 OAuth2LoginAuthenticationProvider 進(jìn)行處理。
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 負(fù)責(zé)實(shí)現(xiàn),通過 RestTemplate 調(diào)用 Gitee 平臺(tái)獲取用戶信息。
圖片
public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
...
//構(gòu)建請(qǐng)求
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);
}
//獲取用戶響應(yīng)
private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRequest, RequestEntity<?> request) {
try {
return this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
}
...
}
...
}
2. 實(shí)現(xiàn)自定義用戶關(guān)聯(lián)邏輯
通過對(duì)三方登錄流程的分析,我們可以通過繼承 DefaultOAuth2UserService 類來實(shí)現(xiàn)自定義的用戶關(guān)聯(lián)邏輯。
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
// 在這里實(shí)現(xiàn)用戶綁定邏輯,例如與 sys_user 表進(jìn)行關(guān)聯(lián)
...
return oauth2User;
}
}
接下來思考一下,將第三方的賬戶轉(zhuǎn)換成我們的自定義用戶需要做哪些事?
1、首先,通過 super.loadUser 方法獲取到第三方用戶對(duì)象 OAuth2User。
2、由于數(shù)據(jù)結(jié)構(gòu)存在差異,我們還需將 OAuth2User 轉(zhuǎn)換為我們自己的用戶數(shù)據(jù)結(jié)構(gòu)。
3、數(shù)據(jù)轉(zhuǎn)換后,需要驗(yàn)證第三方賬號(hào)是否在系統(tǒng)中存在。如果不存在,則進(jìn)行保存操作并關(guān)聯(lián)賬號(hào);如果存在,則執(zhí)行更新操作。
以下為實(shí)現(xiàn)過程。
2.1 存儲(chǔ)第三方用戶
首先,創(chuàng)建一張表用于存儲(chǔ)第三方用戶,其建表語句如下:
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 '第三方用戶賬號(hào)',
`platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '平臺(tái)類型',
`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 '綁定時(shí)間',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新時(shí)間',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
在這個(gè)表中,通過字段 user_id 與我們自己的用戶表 sys_user 進(jìn)行關(guān)聯(lián),同時(shí)通過第三方登錄平臺(tái) platform 與第三方用戶 ID unique_id 來確定唯一用戶。
2.2 創(chuàng)建接口用于將第三方用戶轉(zhuǎn)化成我們自己的用戶
public interface OAuth2UserConvert {
/**
* 轉(zhuǎn)換成自定義用戶
* @param oAuth2User Oauth2用戶
* @return Oauth2UnionUser
*/
Oauth2UnionUser convert(OAuth2User oAuth2User );
}
由于本文集成的是 Gitee 平臺(tái),因此需要編寫一個(gè)具體的實(shí)現(xiàn)類用于用戶轉(zhuǎn)換:
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);
// 轉(zhuǎn)換至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;
}
}
當(dāng)然,如果需要集成多個(gè)平臺(tái),還需要?jiǎng)?chuàng)建一個(gè)上下文類,用于選擇具體的接口實(shí)現(xiàn)進(jìn)行用戶轉(zhuǎn)換。
@Component
@RequiredArgsConstructor
public class Oauth2UserConverterContext {
/**
* 用戶轉(zhuǎn)換器
*/
public Oauth2UnionUser convert(OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// 獲取三方登錄配置的registrationId,這里將他當(dāng)做登錄方式
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// 轉(zhuǎn)換用戶信息
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過期時(shí)間
oauth2UnionUser.setCredentialsExpiresAt(tokenExpiresAt);
}
return oauth2UnionUser;
}
/**
* 獲取轉(zhuǎn)換器
* @param registrationId 登錄類型
* @return 轉(zhuǎn)換器
*/
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);
};
}
}
在這段代碼中,通過第三方登錄平臺(tái)的 registrationId 來選擇具體的接口實(shí)現(xiàn)類。
2.3 創(chuàng)建Oauth2ThirdService用于實(shí)現(xiàn)用戶的存儲(chǔ)邏輯
@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) {
//查詢用戶是否存在,通過平臺(tái)和第三方的ID兩個(gè)字段確定唯一用戶
LambdaQueryWrapper<Oauth2ThirdUserDO> queryWrapper = Wrappers.lambdaQuery(Oauth2ThirdUserDO.class)
.eq(Oauth2ThirdUserDO::getPlatform, oauth2UnionUser.getPlatform())
.eq(Oauth2ThirdUserDO::getUniqueId, oauth2UnionUser.getUniqueId());
Oauth2ThirdUserDO oauth2ThirdUserDO = oauth2ThirdUserMapper.selectOne(queryWrapper);
//數(shù)據(jù)庫如果為空,則先保存到系統(tǒng)用戶表,然后再初始化到第三方用戶表
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,用于業(yè)務(wù)流程編排
@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、獲取到遠(yuǎn)程用戶信息
OAuth2User oAuth2User = super.loadUser(userRequest);
//2、轉(zhuǎn)換用戶信息
Oauth2UnionUser oauth2UnionUser = oauth2UserConverterContext.convert(userRequest, oAuth2User);
//3、檢查是否存在并保存
oauth2ThirdService.save(oauth2UnionUser);
// 將yml配置的RegistrationId當(dāng)做登錄類型設(shè)置至attributes中
LinkedHashMap<String, Object> attributes = new LinkedHashMap<>(oAuth2User.getAttributes());
attributes.put("platform", oauth2UnionUser.getPlatform());
return new DefaultOAuth2User(oAuth2User.getAuthorities(), attributes, oauth2UnionUser.getUserNameAttributeName());
}
}
通過上面四步處理,當(dāng)我們初次使用Gitee平臺(tái)登錄時(shí),會(huì)在sys_user中先插入一條數(shù)據(jù),然后再在oauth2_third_user表中插入第三方用戶數(shù)據(jù),這樣就實(shí)現(xiàn)了用戶數(shù)據(jù)的綁定。
圖片
3. 小結(jié)
本文詳細(xì)介紹了如何在項(xiàng)目中實(shí)現(xiàn)自定義的第三方用戶關(guān)聯(lián)邏輯,通過繼承 DefaultOAuth2UserService 類來處理用戶登錄和數(shù)據(jù)綁定。我們首先分析了三方登錄的認(rèn)證流程,并創(chuàng)建了必要的數(shù)據(jù)庫結(jié)構(gòu)以存儲(chǔ)第三方用戶信息。
接著,我們定義了用戶轉(zhuǎn)換接口和具體的實(shí)現(xiàn)類,以便將第三方用戶信息轉(zhuǎn)換為我們自定義的用戶數(shù)據(jù)結(jié)構(gòu)。為了實(shí)現(xiàn)數(shù)據(jù)的有效存儲(chǔ),我們?cè)O(shè)計(jì)了一個(gè)服務(wù)類,用于檢查用戶是否已存在于系統(tǒng)中,并進(jìn)行相應(yīng)的保存或更新操作。