🍉Spring Authorization Server (7) 第三方平台账号存储

866 阅读6分钟

架构版本
Spring Boot 3.1
Spring Authorization Server 1.1.1
spring-cloud 2022.0.3
spring-cloud-alibaba 2022.0.0.0
完整代码👉watermelon-cloud

有这样一个常见的应用场景

当选择第三方登录的时候,第三方认证、授权成功后会回调到我方服务端,那么这个时候我们一定会将第三方平台账号&我方账号做一个关联,现在市面上都是这样玩的。例如你选择微信登录完了以后是不是还是得绑定手机号,这个绑定手机号的操作也就是做了第三方平台账号&我方系统账号做的关联操作。

当然除了扩展点,以下还会讲到架构设计😉

寻找扩展点

以gitee 为例 ,gitee授权成功后的回调 http://192.168.56.1:9000/login/oauth2/code/gitee ,对应的就是 OAuth2LoginAuthenticationFilter处理。 OAuth2LoginAuthenticationFilter

public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

   public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*";

   @Override
   public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
           throws AuthenticationException {
       //...省略

       OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);
       //...省略
   }
}

其中 OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this .getAuthenticationManager().authenticate(authenticationRequest);
跟源码继续往下,对应的就是 OAuth2LoginAuthenticationProvider

public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {

   private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;

   private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;

   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
       //...省略
       OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
               loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
       //...省略
   }
}

this.userService.loadUser() 继续跟到实现就是 DefaultOAuth2UserService

public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, >OAuth2User> {
   @Override
   public OAuth2User loadUser(OAuth2UserRequest userRequest) throws >OAuth2AuthenticationException {
       //...省略
      
       //这个就是向gitee发起的请求了
       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));
       }
       //最后将gitee获取的用户信息 封装成一个 DefaultOAuth2User返回
       return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
   }
}

DefaultOAuth2UserService实际上是处理第三方平台返回的用户信息。
那如何去扩展 🤔?

要将信息存到我们自己系统,在返回之后去做处理就行了😁
直接继承DefaultOAuth2UserService去扩展 实现扩展 😀

第三方平台账号存储扩展

PostgrepSql 存储表

sys_third_user 第三方用户信息表

DROP TABLE IF EXISTS sys_third_user;
CREATE TABLE sys_third_user (
 create_time timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone),
 modified_time timestamp(6) DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone),
 id int8 NOT NULL DEFAULT nextval('sys_third_user_id_seq'::regclass),
 unique_id varchar(128)  NOT NULL,
 name varchar(64)  NOT NULL,
 platform varchar(64)  NOT NULL,
 avatar varchar(255)  DEFAULT NULL,
 user_id int8 NOT NULL
)
;
COMMENT ON COLUMN sys_third_user.create_time IS '创建时间';
COMMENT ON COLUMN sys_third_user.modified_time IS '修改时间';
COMMENT ON COLUMN sys_third_user.id IS 'id';
COMMENT ON COLUMN sys_third_user.unique_id IS '第三方平台唯一id';
COMMENT ON COLUMN sys_third_user.name IS '用户名称';
COMMENT ON COLUMN sys_third_user.platform IS '平台类型(WX:微信;QQ:QQ)';
COMMENT ON COLUMN sys_third_user.avatar IS '头像';
COMMENT ON COLUMN sys_third_user.user_id IS '用户id';
COMMENT ON TABLE sys_third_user IS '第三方用户表';

-- ----------------------------
-- Primary Key structure for table sys_third_user
-- ----------------------------
ALTER TABLE sys_third_user ADD CONSTRAINT sys_third_user_pkey PRIMARY KEY (id);

AccountPlatform 枚举

public enum AccountPlatform {
   WX,
   QQ,
   GITEE,
   GITHUB;
}

OAuth2ThirdUserDto

定义第三方平台的用户信息实体,用构造方法的原因是因为想把此次扩展的定义为一个stater,不引入lombok,不是low 😂

public class OAuth2ThirdUserDto implements Serializable {
   /**
    * 第三方平台唯一id
    */
   private String uniqueId;
   /**
    * 用户名称
    */
   private String name;
   /**
    * 平台类型(WX:微信;QQ:QQ)
    */
   private AccountPlatform platform;
   /**
    * 头像
    */
   private String avatar;


   public OAuth2ThirdUserDto(String uniqueId, String name, AccountPlatform platform, String avatar) {
       this.uniqueId = uniqueId;
       this.name = name;
       this.platform = platform;
       this.avatar = avatar;
   }


   public String getUniqueId() {
       return uniqueId;
   }

   public void setUniqueId(String uniqueId) {
       this.uniqueId = uniqueId;
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public AccountPlatform getPlatform() {
       return platform;
   }

   public void setPlatform(AccountPlatform platform) {
       this.platform = platform;
   }

   public String getAvatar() {
       return avatar;
   }

   public void setAvatar(String avatar) {
       this.avatar = avatar;
   }

   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       OAuth2ThirdUserDto that = (OAuth2ThirdUserDto) o;
       return Objects.equals(uniqueId, that.uniqueId) && Objects.equals(name, that.name) && platform == that.platform && Objects.equals(avatar, that.avatar);
   }

   @Override
   public int hashCode() {
       return Objects.hash(uniqueId, name, platform, avatar);
   }
}

OAuth2UserConvert

定义转换的接口,多个平台的情况下,采用一个策略模式

//第三方用户转换接口定义
public interface OAuth2UserConvert {
   
   default AccountPlatform platform() {
       return null;
   }
   /**
    * 第三方用户信息统一转换为 OAuth2ThirdUserDto
    * @param oAuth2User
    * @param userNameAttributeName 额外的属性
    * @return
    */
   Pair<OAuth2ThirdUserDto, LinkedHashMap<String,Object>> convert(OAuth2User oAuth2User,String userNameAttributeName);
}

GiteeOAuth2UserConvert
gitee的OAuth2UserConvert实现

public class GiteeOAuth2UserConvert implements OAuth2UserConvert {
  
   private final static String AVATAR_URL = "avatar_url";

   private final static String UNIQUE_ID = "id";

   private final static String NAME = "name";

   private final static String EMAIL = "email";

   private final static String PLATFORM = "platform";
   
   @Override
   public AccountPlatform platform() {
       return AccountPlatform.GITEE;
   }
   @Override
   public Pair<OAuth2ThirdUserDto, LinkedHashMap<String, Object>> convert(OAuth2User oAuth2User, String userNameAttributeName) {
       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 name = Optional.ofNullable(oAuth2User.getAttribute(NAME)).map(Object::toString).orElse(null);
       String email = Optional.ofNullable(oAuth2User.getAttribute(EMAIL)).map(Object::toString).orElse(null);
       Object nameAttributeValue = Optional.ofNullable(userNameAttributeName).map(oAuth2User::getAttribute).orElse(null);

       LinkedHashMap<String, Object> userAttributesLinkedHashMap = new LinkedHashMap<>();
       //根据需要取所需要字段
       userAttributesLinkedHashMap.put(UNIQUE_ID, uniqueId);
       userAttributesLinkedHashMap.put(NAME, name);
       userAttributesLinkedHashMap.put(EMAIL, email);
       userAttributesLinkedHashMap.put(AVATAR_URL, avatarUrl);
       userAttributesLinkedHashMap.put(userNameAttributeName, nameAttributeValue);
       userAttributesLinkedHashMap.put(PLATFORM, this.platform().name());
       OAuth2ThirdUserDto oAuth2ThirdUserDto = new OAuth2ThirdUserDto(uniqueId, name, AccountPlatform.GITEE, avatarUrl);
       return new Pair<>(oAuth2ThirdUserDto, userAttributesLinkedHashMap);
   }
}

OAuth2UserConvertContext

OAuth2UserConvert 的context 管理

public class OAuth2UserConvertContext {

   private Map<AccountPlatform, OAuth2UserConvert> oAuth2UserConvertMap;
   /**
    * 加载 OAuth2UserConvert
    * @param oAuth2UserConvertList
    */
   public OAuth2UserConvertContext(List<OAuth2UserConvert> oAuth2UserConvertList) {
       this.oAuth2UserConvertMap = oAuth2UserConvertList.stream().collect(Collectors.toMap(OAuth2UserConvert::platform, Function.identity()));

   }
   /**
    * 获取实例
    * @param platform
    * @return
    */
   public OAuth2UserConvert getInstance(AccountPlatform platform) {
       if (platform == null) {
           throw new SystemException("平台类型不能为空");
       }
       OAuth2UserConvert oAuth2UserConvert = oAuth2UserConvertMap.get(platform);
       if (oAuth2UserConvert == null) {
           throw new SystemException("暂不支持[" + platform + "]平台类型");
       }
       return oAuth2UserConvert;
   }
}

Oauth2UserStorage

//第三方平台保存接口定义
public interface Oauth2UserStorage {

   /**
    * 保存
    * @param auth2ThirdUserDto
    */
   void save(OAuth2ThirdUserDto auth2ThirdUserDto);

}

DefaultOauth2UserStorage
定义这个接口的好处在于,其他人也可以引入这个 stater ,然后就不用再去实现spring官方的接口去做了,实现DefaultOauth2UserStorage 定义的接口就可以了。

public class DefaultOauth2UserStorage implements Oauth2UserStorage {
   @Override
   public void save(OAuth2ThirdUserDto auth2ThirdUserDto) {

   }
}

ExtDefaultOAuth2UserService

用户信息保存默认实现 ,来看看吧,直接继承DefaultOAuth2UserService

public class ExtDefaultOAuth2UserService extends DefaultOAuth2UserService {

   public final OAuth2UserConvertContext oAuth2UserConvertContext;

   public final Oauth2UserStorage oauth2UserStorage;

   public ExtDefaultOAuth2UserService(OAuth2UserConvertContext oAuth2UserConvertContext, Oauth2UserStorage oauth2UserStorage) {
       this.oAuth2UserConvertContext = oAuth2UserConvertContext;
       this.oauth2UserStorage = oauth2UserStorage;
   }
   
   @Override
   public OAuth2User loadUser(OAuth2UserRequest userRequest) throws >OAuth2AuthenticationException {
       OAuth2User oAuth2User = super.loadUser(userRequest);
       String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
               .getUserNameAttributeName();
       AccountPlatform platform = >this.loginPlatformConvert(userRequest.getClientRegistration().getRegistrationId());
       //将 OAuth2User 根据不同的平台 转成统一的 第三方用户了
       Pair<OAuth2ThirdUserDto, LinkedHashMap<String, Object>> oAuth2ThirdUserConvertPair = oAuth2UserConvertContext.getInstance(platform)
               .convert(oAuth2User, userNameAttributeName);
       LinkedHashMap<String, Object> userAttributes = oAuth2ThirdUserConvertPair.getValue();
       //这个地方保存逻辑了
       oauth2UserStorage.save(oAuth2ThirdUserConvertPair.getKey());
       return new DefaultOAuth2User(oAuth2User.getAuthorities(), userAttributes, userNameAttributeName);
   }

   /**
    * registrationId 转换平台枚举
    * @param registrationId
    * @return
   */
   synchronized private AccountPlatform loginPlatformConvert(String registrationId) {
       return switch (registrationId) {
           case "gitee" -> AccountPlatform.GITEE;
           case "wechat" -> AccountPlatform.WECHAT;//todo Convert
           case "qq" -> AccountPlatform.QQ;//todo Convert
           default -> throw new OAuth2UserConvertException("暂不支持该客户端[" + registrationId + "]对应的第三方平台用户信息Convert");
       };
   }
}

扩展的class就全部搞定了

watermelon-authorization 模块下重写定义了watermelon-authorization-oauth2-client 模块,这样的好处是可以单独作为一个 stater 给其他任何项目使用。

watermelon-authorization-oauth2-client完整版本就是这样了,那这样的stater就完成了 img_7day_1.png

那就在watermelon-authorization-server 中引入我们自己定义好的 watermelon-authorization-server 依赖。

img_7daya_2.png

watermelon-authorization-server 中使用,就需要实现Oauth2UserStorage接口就能搞定了

基于PostgrepSql 去存储 用的mybatis-plus 那就这样实现一个吧

@Primary
@Service("mybatisOauth2UserStorage")
@RequiredArgsConstructor
public class MybatisOauth2UserStorage implements Oauth2UserStorage {

   private final SysThirdUserService sysThirdUserService;

   private final SysUserService sysUserService;

   @Override
   public void save(OAuth2ThirdUserDto auth2ThirdUserDto) {
       SysUseAddDto sysUseAddDto = new SysUseAddDto();
       sysUseAddDto.setName(auth2ThirdUserDto.getName());
       sysUseAddDto.setAvatar(auth2ThirdUserDto.getAvatar());
       sysUseAddDto.setStatus(1);
       Long sysUserId = sysUserService.save(sysUseAddDto);
       SysThirdUserAddDto sysThirdUserAddDto = new SysThirdUserAddDto();
       sysThirdUserAddDto.setUniqueId(auth2ThirdUserDto.getUniqueId());
       sysThirdUserAddDto.setAvatar(auth2ThirdUserDto.getAvatar());
       sysThirdUserAddDto.setPlatform(auth2ThirdUserDto.getPlatform());
       sysThirdUserAddDto.setName(auth2ThirdUserDto.getName());
       sysThirdUserAddDto.setUserId(sysUserId);
       sysThirdUserService.save(sysThirdUserAddDto);
   }
}

其他任何工程只要实现Oauth2UserStorag 接口实现就可以了,这也是定义stater 的目的所在-为了通用。
如果有其他平台的用户转换,可以实现OAuth2UserConvert 去自行实现即可

最后看看测试的最终结果

img_7day_3.png

**完整的代码在:https://github.com/WatermelonPlanet/watermelon-cloud**中。