为MongoDB定制Spring Social Connect框架

阎坚成
2023-12-01

在上一篇文章中 ,我谈到了我面临的第一个挑战是更改数据模型并添加连接框架。 在这里,我想提供有关我如何做的更多细节。 Spring Social项目已经提供了基于jdbc的连接存储库实现,以将用户连接数据持久保存到关系数据库中。 但是,我使用的是MongoDB,因此我需要自定义代码,并且发现这样做相对容易。 用户连接数据将保存为UserSocialConnection的对象,它是一个MongoDB文档:

@SuppressWarnings('serial')
@Document(collection = 'UserSocialConnection')
public class UserSocialConnection extends BaseEntity {
    private String userId;
    private String providerId;
    private String providerUserId;
    private String displayName;
    private String profileUrl;
    private String imageUrl;
    private String accessToken;
    private String secret;
    private String refreshToken;
    private Long expireTime;

    //Getter/Setter omitted.

    public UserSocialConnection() {
        super();
    }

    public UserSocialConnection(String userId, String providerId, String providerUserId, int rank,
            String displayName, String profileUrl, String imageUrl, String accessToken, String secret,
            String refreshToken, Long expireTime) {
        super();
        this.userId = userId;
        this.providerId = providerId;
        this.providerUserId = providerUserId;
        this.displayName = displayName;
        this.profileUrl = profileUrl;
        this.imageUrl = imageUrl;
        this.accessToken = accessToken;
        this.secret = secret;
        this.refreshToken = refreshToken;
        this.expireTime = expireTime;
    }
}

BaseEntity仅具有“ id”。 在Spring Data项目的帮助下,我不需要为UserSocialConnection编写任何CRUD操作代码,只需扩展MongoRepository

public interface UserSocialConnectionRepository extends MongoRepository<UserSocialConnection, String>{
    List<UserSocialConnection> findByUserId(String userId);

    List<UserSocialConnection> findByUserIdAndProviderId(String userId, String providerId);

    List<UserSocialConnection> findByProviderIdAndProviderUserId(String providerId, String providerUserId);

    UserSocialConnection findByUserIdAndProviderIdAndProviderUserId(String userId, String providerId, String providerUserId);

    List<UserSocialConnection> findByProviderIdAndProviderUserIdIn(String providerId, Collection<String> providerUserIds);
}

在拥有数据库UserSocialConnectionRepository ,我们将实现Spring Social所需的ConnectionRepositoryUsersConnectionRepository 。 我只是从JdbcConnectionRepositoryJdbcUsersConnectionRepository复制了代码,并创建了自己的MongoConnectionRepositoryMongoUsersConnectionRepository

public class MongoUsersConnectionRepository implements UsersConnectionRepository{

    private final UserSocialConnectionRepository userSocialConnectionRepository;

    private final SocialAuthenticationServiceLocator socialAuthenticationServiceLocator;

    private final TextEncryptor textEncryptor;

    private ConnectionSignUp connectionSignUp;

    public MongoUsersConnectionRepository(UserSocialConnectionRepository userSocialConnectionRepository, 
            SocialAuthenticationServiceLocator socialAuthenticationServiceLocator, TextEncryptor textEncryptor){
        this.userSocialConnectionRepository = userSocialConnectionRepository;
        this.socialAuthenticationServiceLocator = socialAuthenticationServiceLocator;
        this.textEncryptor = textEncryptor;
    }

    /**
     * The command to execute to create a new local user profile in the event no user id could be mapped to a connection.
     * Allows for implicitly creating a user profile from connection data during a provider sign-in attempt.
     * Defaults to null, indicating explicit sign-up will be required to complete the provider sign-in attempt.
     * @see #findUserIdsWithConnection(Connection)
     */
    public void setConnectionSignUp(ConnectionSignUp connectionSignUp) {
        this.connectionSignUp = connectionSignUp;
    }

    public List<String> findUserIdsWithConnection(Connection<?> connection) {
        ConnectionKey key = connection.getKey();
        List<UserSocialConnection> userSocialConnectionList = 
                this.userSocialConnectionRepository.findByProviderIdAndProviderUserId(key.getProviderId(), key.getProviderUserId());
        List<String> localUserIds = new ArrayList<String>();
        for (UserSocialConnection userSocialConnection : userSocialConnectionList){
            localUserIds.add(userSocialConnection.getUserId());
        }

        if (localUserIds.size() == 0 && connectionSignUp != null) {
            String newUserId = connectionSignUp.execute(connection);
            if (newUserId != null)
            {
                createConnectionRepository(newUserId).addConnection(connection);
                return Arrays.asList(newUserId);
            }
        }
        return localUserIds;
    }

    public Set<String> findUserIdsConnectedTo(String providerId, Set<String> providerUserIds) {
        final Set<String> localUserIds = new HashSet<String>();

        List<UserSocialConnection> userSocialConnectionList = 
                this.userSocialConnectionRepository.findByProviderIdAndProviderUserIdIn(providerId, providerUserIds);
        for (UserSocialConnection userSocialConnection : userSocialConnectionList){
            localUserIds.add(userSocialConnection.getUserId());
        }
        return localUserIds;
    }

    public ConnectionRepository createConnectionRepository(String userId) {
        if (userId == null) {
            throw new IllegalArgumentException('userId cannot be null');
        }
        return new MongoConnectionRepository(userId, userSocialConnectionRepository, socialAuthenticationServiceLocator, textEncryptor);
    }

}

MongoUsersConnectionRepository非常类似于JdbcUsersConnectionRepository 。 但是对于MongoConnectionRepository ,我需要进行一些更改:

public class MongoConnectionRepository implements ConnectionRepository {

    private final String userId;

    private final UserSocialConnectionRepository userSocialConnectionRepository;

    private final SocialAuthenticationServiceLocator socialAuthenticationServiceLocator;

    private final TextEncryptor textEncryptor;

    public MongoConnectionRepository(String userId, UserSocialConnectionRepository userSocialConnectionRepository,
            SocialAuthenticationServiceLocator socialAuthenticationServiceLocator, TextEncryptor textEncryptor) {
        this.userId = userId;
        this.userSocialConnectionRepository = userSocialConnectionRepository;
        this.socialAuthenticationServiceLocator = socialAuthenticationServiceLocator;
        this.textEncryptor = textEncryptor;
    }

    public MultiValueMap<String, Connection<?>> findAllConnections() {
        List<UserSocialConnection> userSocialConnectionList = this.userSocialConnectionRepository
                .findByUserId(userId);

        MultiValueMap<String, Connection<?>> connections = new LinkedMultiValueMap<String, Connection<?>>();
        Set<String> registeredProviderIds = socialAuthenticationServiceLocator.registeredProviderIds();
        for (String registeredProviderId : registeredProviderIds) {
            connections.put(registeredProviderId, Collections.<Connection<?>> emptyList());
        }
        for (UserSocialConnection userSocialConnection : userSocialConnectionList) {
            String providerId = userSocialConnection.getProviderId();
            if (connections.get(providerId).size() == 0) {
                connections.put(providerId, new LinkedList<Connection<?>>());
            }
            connections.add(providerId, buildConnection(userSocialConnection));
        }
        return connections;
    }

    public List<Connection<?>> findConnections(String providerId) {
        List<Connection<?>> resultList = new LinkedList<Connection<?>>();
        List<UserSocialConnection> userSocialConnectionList = this.userSocialConnectionRepository
                .findByUserIdAndProviderId(userId, providerId);
        for (UserSocialConnection userSocialConnection : userSocialConnectionList) {
            resultList.add(buildConnection(userSocialConnection));
        }
        return resultList;
    }

    @SuppressWarnings('unchecked')
    public <A> List<Connection<A>> findConnections(Class<A> apiType) {
        List<?> connections = findConnections(getProviderId(apiType));
        return (List<Connection<A>>) connections;
    }

    public MultiValueMap<String, Connection<?>> findConnectionsToUsers(MultiValueMap<String, String> providerUsers) {
        if (providerUsers == null || providerUsers.isEmpty()) {
            throw new IllegalArgumentException('Unable to execute find: no providerUsers provided');
        }

        MultiValueMap<String, Connection<?>> connectionsForUsers = new LinkedMultiValueMap<String, Connection<?>>();

        for (Iterator<Entry<String, List<String>>> it = providerUsers.entrySet().iterator(); it.hasNext();) {
            Entry<String, List<String>> entry = it.next();
            String providerId = entry.getKey();
            List<String> providerUserIds = entry.getValue();
            List<UserSocialConnection> userSocialConnections = 
                    this.userSocialConnectionRepository.findByProviderIdAndProviderUserIdIn(providerId, providerUserIds);
            List<Connection<?>> connections = new ArrayList<Connection<?>>(providerUserIds.size());
            for (int i = 0; i < providerUserIds.size(); i++) {
                connections.add(null);
            }
            connectionsForUsers.put(providerId, connections);

            for (UserSocialConnection userSocialConnection : userSocialConnections) {
                String providerUserId = userSocialConnection.getProviderUserId();
                int connectionIndex = providerUserIds.indexOf(providerUserId);
                connections.set(connectionIndex, buildConnection(userSocialConnection));
            }

        }
        return connectionsForUsers;
    }

    public Connection<?> getConnection(ConnectionKey connectionKey) {
        UserSocialConnection userSocialConnection = this.userSocialConnectionRepository
                .findByUserIdAndProviderIdAndProviderUserId(userId, connectionKey.getProviderId(),
                        connectionKey.getProviderUserId());
        if (userSocialConnection != null) {
            return buildConnection(userSocialConnection);
        }
        throw new NoSuchConnectionException(connectionKey);
    }

    @SuppressWarnings('unchecked')
    public <A> Connection<A> getConnection(Class<A> apiType, String providerUserId) {
        String providerId = getProviderId(apiType);
        return (Connection<A>) getConnection(new ConnectionKey(providerId, providerUserId));
    }

    @SuppressWarnings('unchecked')
    public <A> Connection<A> getPrimaryConnection(Class<A> apiType) {
        String providerId = getProviderId(apiType);
        Connection<A> connection = (Connection<A>) findPrimaryConnection(providerId);
        if (connection == null) {
            throw new NotConnectedException(providerId);
        }
        return connection;
    }

    @SuppressWarnings('unchecked')
    public <A> Connection<A> findPrimaryConnection(Class<A> apiType) {
        String providerId = getProviderId(apiType);
        return (Connection<A>) findPrimaryConnection(providerId);
    }

    public void addConnection(Connection<?> connection) {
        //check cardinality
        SocialAuthenticationService<?> socialAuthenticationService = 
                this.socialAuthenticationServiceLocator.getAuthenticationService(connection.getKey().getProviderId());
        if (socialAuthenticationService.getConnectionCardinality() == ConnectionCardinality.ONE_TO_ONE ||
                socialAuthenticationService.getConnectionCardinality() == ConnectionCardinality.ONE_TO_MANY){
            List<UserSocialConnection> storedConnections = 
                    this.userSocialConnectionRepository.findByProviderIdAndProviderUserId(
                            connection.getKey().getProviderId(), connection.getKey().getProviderUserId());
            if (storedConnections.size() > 0){
                //not allow one providerId connect to multiple userId
                throw new DuplicateConnectionException(connection.getKey());
            }
        }

        UserSocialConnection userSocialConnection = this.userSocialConnectionRepository
                .findByUserIdAndProviderIdAndProviderUserId(userId, connection.getKey().getProviderId(), 
                        connection.getKey().getProviderUserId());
        if (userSocialConnection == null) {
            ConnectionData data = connection.createData();
            userSocialConnection = new UserSocialConnection(userId, data.getProviderId(), data.getProviderUserId(), 0,
                    data.getDisplayName(), data.getProfileUrl(), data.getImageUrl(), encrypt(data.getAccessToken()),
                    encrypt(data.getSecret()), encrypt(data.getRefreshToken()), data.getExpireTime());
            this.userSocialConnectionRepository.save(userSocialConnection);
        } else {
            throw new DuplicateConnectionException(connection.getKey());
        }
    }

    public void updateConnection(Connection<?> connection) {
        ConnectionData data = connection.createData();
        UserSocialConnection userSocialConnection = this.userSocialConnectionRepository
                .findByUserIdAndProviderIdAndProviderUserId(userId, connection.getKey().getProviderId(), connection
                        .getKey().getProviderUserId());
        if (userSocialConnection != null) {
            userSocialConnection.setDisplayName(data.getDisplayName());
            userSocialConnection.setProfileUrl(data.getProfileUrl());
            userSocialConnection.setImageUrl(data.getImageUrl());
            userSocialConnection.setAccessToken(encrypt(data.getAccessToken()));
            userSocialConnection.setSecret(encrypt(data.getSecret()));
            userSocialConnection.setRefreshToken(encrypt(data.getRefreshToken()));
            userSocialConnection.setExpireTime(data.getExpireTime());
            this.userSocialConnectionRepository.save(userSocialConnection);
        }
    }

    public void removeConnections(String providerId) {
        List<UserSocialConnection> userSocialConnectionList = this.userSocialConnectionRepository
                .findByUserIdAndProviderId(userId, providerId);
        for (UserSocialConnection userSocialConnection : userSocialConnectionList) {
            this.userSocialConnectionRepository.delete(userSocialConnection);
        }
    }

    public void removeConnection(ConnectionKey connectionKey) {
        UserSocialConnection userSocialConnection = this.userSocialConnectionRepository
                .findByUserIdAndProviderIdAndProviderUserId(userId, connectionKey.getProviderId(), connectionKey.getProviderUserId());
        this.userSocialConnectionRepository.delete(userSocialConnection);
    }

    // internal helpers

    private Connection<?> buildConnection(UserSocialConnection userSocialConnection) {
        ConnectionData connectionData = new ConnectionData(userSocialConnection.getProviderId(),
                userSocialConnection.getProviderUserId(), userSocialConnection.getDisplayName(),
                userSocialConnection.getProfileUrl(), userSocialConnection.getImageUrl(),
                decrypt(userSocialConnection.getAccessToken()), decrypt(userSocialConnection.getSecret()),
                decrypt(userSocialConnection.getRefreshToken()), userSocialConnection.getExpireTime());
        ConnectionFactory<?> connectionFactory = this.socialAuthenticationServiceLocator.getConnectionFactory(connectionData
                .getProviderId());
        return connectionFactory.createConnection(connectionData);
    }

    private Connection<?> findPrimaryConnection(String providerId) {
        List<UserSocialConnection> userSocialConnectionList = this.userSocialConnectionRepository
                .findByUserIdAndProviderId(userId, providerId);

        return buildConnection(userSocialConnectionList.get(0));
    }

    private <A> String getProviderId(Class<A> apiType) {
        return socialAuthenticationServiceLocator.getConnectionFactory(apiType).getProviderId();
    }

    private String encrypt(String text) {
        return text != null ? textEncryptor.encrypt(text) : text;
    }

    private String decrypt(String encryptedText) {
        return encryptedText != null ? textEncryptor.decrypt(encryptedText) : encryptedText;
    }

}

首先,我将JdbcTemplate替换为UserSocialConnectionRepository以从数据库中检索UserSocialConnection对象。 然后用spring-social-security模块中的SocialAuthenticationServiceLocator替换ConnectionFactoryLocator 。 最大的变化是addConnection方法(上面已突出显示),它首先检查连接基数。 如果connectionCardinalitysocialAuthenticationServiceONE_TO_ONE (这意味着一个用户id与一个且仅一个对providerId / providerUserId的),或ONE_TO_MANY (这意味着一个用户id可以连接到一个或多个providerId / providerUserId,但一对providerId / providerUserId的只能连接到一个userId)。

完成所有这些自定义之后,最后一步是在spring config中将它们粘合在一起:

@Configuration
public class SocialAndSecurityConfig {
    @Inject
    private Environment environment;

    @Inject
    AccountService accountService;

    @Inject
    private AuthenticationManager authenticationManager;

    @Inject
    private UserSocialConnectionRepository userSocialConnectionRepository;

    @Bean
    public SocialAuthenticationServiceLocator socialAuthenticationServiceLocator() {
        SocialAuthenticationServiceRegistry registry = new SocialAuthenticationServiceRegistry();

        //add google
        OAuth2ConnectionFactory<Google> googleConnectionFactory = new GoogleConnectionFactory(environment.getProperty('google.clientId'),
                environment.getProperty('google.clientSecret'));
        OAuth2AuthenticationService<Google> googleAuthenticationService = new OAuth2AuthenticationService<Google>(googleConnectionFactory);
        googleAuthenticationService.setScope('https://www.googleapis.com/auth/userinfo.profile');
        registry.addAuthenticationService(googleAuthenticationService);

        //add twitter
        OAuth1ConnectionFactory<Twitter> twitterConnectionFactory = new TwitterConnectionFactory(environment.getProperty('twitter.consumerKey'),
                environment.getProperty('twitter.consumerSecret'));
        OAuth1AuthenticationService<Twitter> twitterAuthenticationService = new OAuth1AuthenticationService<Twitter>(twitterConnectionFactory);
        registry.addAuthenticationService(twitterAuthenticationService);

        //add facebook
        OAuth2ConnectionFactory<Facebook> facebookConnectionFactory = new FacebookConnectionFactory(environment.getProperty('facebook.clientId'),
                environment.getProperty('facebook.clientSecret'));
        OAuth2AuthenticationService<Facebook> facebookAuthenticationService = new OAuth2AuthenticationService<Facebook>(facebookConnectionFactory);
        facebookAuthenticationService.setScope('');
        registry.addAuthenticationService(facebookAuthenticationService);

        return registry;
    }

    /**
     * Singleton data access object providing access to connections across all users.
     */
    @Bean
    public UsersConnectionRepository usersConnectionRepository() {
        MongoUsersConnectionRepository repository = new MongoUsersConnectionRepository(userSocialConnectionRepository,
                socialAuthenticationServiceLocator(), Encryptors.noOpText());
        repository.setConnectionSignUp(autoConnectionSignUp());
        return repository;
    }

    /**
     * Request-scoped data access object providing access to the current user's connections.
     */
    @Bean
    @Scope(value = 'request', proxyMode = ScopedProxyMode.INTERFACES)
    public ConnectionRepository connectionRepository() {
        UserAccount user = AccountUtils.getLoginUserAccount();
        return usersConnectionRepository().createConnectionRepository(user.getUsername());
    }

    /**
     * A proxy to a request-scoped object representing the current user's primary Google account.
     * 
     * @throws NotConnectedException
     *             if the user is not connected to Google.
     */
    @Bean
    @Scope(value = 'request', proxyMode = ScopedProxyMode.INTERFACES)
    public Google google() {
        Connection<Google> google = connectionRepository().findPrimaryConnection(Google.class);
        return google != null ? google.getApi() : new GoogleTemplate();
    }

    @Bean
    @Scope(value='request', proxyMode=ScopedProxyMode.INTERFACES)   
    public Facebook facebook() {
        Connection<Facebook> facebook = connectionRepository().findPrimaryConnection(Facebook.class);
        return facebook != null ? facebook.getApi() : new FacebookTemplate();
    }

    @Bean
    @Scope(value='request', proxyMode=ScopedProxyMode.INTERFACES)   
    public Twitter twitter() {
        Connection<Twitter> twitter = connectionRepository().findPrimaryConnection(Twitter.class);
        return twitter != null ? twitter.getApi() : new TwitterTemplate();
    }

    @Bean
    public ConnectionSignUp autoConnectionSignUp() {
        return new AutoConnectionSignUp(accountService);
    }

    @Bean
    public SocialAuthenticationFilter socialAuthenticationFilter() {
        SocialAuthenticationFilter filter = new SocialAuthenticationFilter(authenticationManager, accountService,
                usersConnectionRepository(), socialAuthenticationServiceLocator());
        filter.setFilterProcessesUrl('/signin');
        filter.setSignupUrl(null); 
        filter.setConnectionAddedRedirectUrl('/myAccount');
        filter.setPostLoginUrl('/myAccount');
        return filter;
    }

    @Bean
    public SocialAuthenticationProvider socialAuthenticationProvider(){
        return new SocialAuthenticationProvider(usersConnectionRepository(), accountService);
    }

    @Bean
    public LoginUrlAuthenticationEntryPoint socialAuthenticationEntryPoint(){
        return new LoginUrlAuthenticationEntryPoint('/signin');
    }

}

accountService是我自己的用户帐户服务,用于提供与帐户相关的功能,它实现了SocialUserDetailsServiceUserDetailsServiceUserIdExtractor

还有很多地方需要改进,例如重构MongoConnectionRepositoryMongoUsersConnectionRepository以使用Spring Data Repository接口实现抽象的社交连接存储库实现。 而且我发现有人已经对此提出了一个问题: 利用Spring Data for UsersConnectionRepository

参考:来自我们的JCG合作伙伴 Yuan Ji在Jiwhiz博客上为MongoDB定制Spring Social Connect Framework

翻译自: https://www.javacodegeeks.com/2013/03/customize-spring-social-connect-framework-for-mongodb.html

 类似资料: