1. 程式人生 > >【Spring Security OAuth2筆記系列】- Spring Social第三方登入

【Spring Security OAuth2筆記系列】- Spring Social第三方登入

QQ登入

上一章節完成了 ServiceProvider的功能,這一節完成應用內部的需要做的一些功能

注意看這個官網文件: https://docs.spring.io/spring-social/docs/1.1.x/
由於在spring-boot-autoconfigure-2.0.4.RELEASE.jar沒有對 social的自動配置了
所以我搞這節課的連通流程花費了5個小時,最後認證檢視官網文件的說明才跑起來

實現 ConnectionFactory

package cn.mrcode.imooc.springsecurity.securitycore.qq.connet;

import
cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQ; import org.springframework.social.connect.Connection; import org.springframework.social.connect.support.OAuth2ConnectionFactory; import org.springframework.social.oauth2.GenericOAuth2ConnectionFactory; /** * qq * @author : zhuqiang * @version
: V1.0 * @date : 2018/8/6 9:02 * @see GenericOAuth2ConnectionFactory 模仿這個來寫 */
public class QQOAuth2ConnectionFactory extends OAuth2ConnectionFactory<QQ> { /** * 唯一的建構函式,需要 * Create a {@link OAuth2ConnectionFactory}. * @param providerId 服務商id;自定義字串;也是後面新增social的過濾,過濾器幫我們攔截的url其中的某一段地址 * on} interface. */
public QQOAuth2ConnectionFactory(String providerId, String appid, String secret) { // 傳遞進來是因為使用該服務的地方才知道 這些引數是什麼 /** * serviceProvider 用於執行授權流和獲取本機服務API例項的ServiceProvider模型 * apiAdapter 介面卡,用於將不同服務提供商的個性化使用者資訊對映到 {@link Connection} */ super(providerId, new QQServiceProvider(appid, secret), new QQApiAdapter()); } }

這裡需要提供一個 ApiAdapter

QQApiAdapter

package cn.mrcode.imooc.springsecurity.securitycore.qq.connet;

import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQ;
import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;

/**
 * 介面卡,用於將不同服務提供商的個性化使用者資訊對映到 {@link Connection}
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 9:10
 */
public class QQApiAdapter implements ApiAdapter<QQ> {
    @Override
    public boolean test(QQ api) {
        // 測試服務是否可用
        return true;
    }

    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        QQUserInfo userInfo = api.getUserInfo();
        values.setDisplayName(userInfo.getNickname());
        values.setImageUrl(userInfo.getFigureurl_qq_1());
        values.setProfileUrl(null); // 主頁地址,像微博一般有主頁地址
        // 服務提供商返回的該user的openid
        // 一般來說這個openid是和你的開發賬戶也就是appid繫結的
        values.setProviderUserId(userInfo.getOpenId());
    }

    @Override
    public UserProfile fetchUserProfile(QQ api) {
        // 暫時不知道有什麼用處
        return UserProfile.EMPTY;
    }

    @Override
    public void updateStatus(QQ api, String message) {
        // 應該是退出的狀態操作。
    }
}

開啟並配置串聯之前寫的功能元件

/**
 *
 */
package cn.mrcode.imooc.springsecurity.securitycore.qq;

import cn.mrcode.imooc.springsecurity.securitycore.qq.config.QQAutoConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.UserIdSource;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ConnectController;
import org.springframework.social.security.AuthenticationNameUserIdSource;
import org.springframework.social.security.SpringSocialConfigurer;

import javax.sql.DataSource;

/**
 * @author zhailiang
 */
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        // 指定表字首,字尾是固定的,在JdbcUsersConnectionRepository所在位置
        repository.setTablePrefix("imooc_");
        return repository;
    }

    @Override
    public UserIdSource getUserIdSource() {
        return new AuthenticationNameUserIdSource();
    }

    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        // 預設配置類,進行元件的組裝
        // 包括了過濾器SocialAuthenticationFilter 新增到security過濾鏈中
        SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
        return springSocialConfigurer;
    }

    //https://docs.spring.io/spring-social/docs/1.1.x-SNAPSHOT/reference/htmlsingle/#creating-connections-with-connectcontroller
    // 這個在目前階段不是必須的,
    // 之前不知道為什麼就是沒有響應
    // 可以暫時忽略該配置
    @Bean
    public ConnectController connectController(
            ConnectionFactoryLocator connectionFactoryLocator,
            ConnectionRepository connectionRepository) {
        return new ConnectController(connectionFactoryLocator, connectionRepository);
    }
}

表建立的sql在 JdbcUsersConnectionRepository類所在位置

-- This SQL contains a "create table" that can be used to create a table that JdbcUsersConnectionRepository can persist
-- connection in. It is, however, not to be assumed to be production-ready, all-purpose SQL. It is merely representative
-- of the kind of table that JdbcUsersConnectionRepository works with. The table and column names, as well as the general
-- column types, are what is important. Specific column types and sizes that work may vary across database vendors and
-- the required sizes may vary across API providers.

create table UserConnection (userId varchar(255) not null,
    providerId varchar(255) not null,
    providerUserId varchar(255),
    rank int not null,
    displayName varchar(255),
    profileUrl varchar(512),
    imageUrl varchar(512),
    accessToken varchar(512) not null,
    secret varchar(512),
    refreshToken varchar(512),
    expireTime bigint,
    primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);

在 SpringSocialConfigurer 中需要注入 SocialUserDetailsService,之前我們有寫好的,改造一下

package cn.mrcode.imooc.springsecurity.securitybrowser;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.social.security.SocialUser;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/3 9:16
 * @date 2018/8/3 9:16
 * @since 1.0
 */
// 自定義資料來源來獲取資料
// 這裡只要是存在一個自定義的 UserDetailsService ,那麼security將會使用該例項進行配置
@Component
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {
    Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;

    // 可以從任何地方獲取資料
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根據使用者名稱查詢使用者資訊
        logger.info("登入使用者名稱:{}", username);
        // 寫死一個密碼,賦予一個admin許可權
//        User admin = new User(username, "{noop}123456",
//                              AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        return getUserDetails(username);
    }


    @Override
    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        logger.info("登入使用者名稱:{}", userId);
        return getUserDetails(userId);
    }

    private SocialUser getUserDetails(String username) {
        String password = passwordEncoder.encode("123456");
        logger.info("資料庫密碼{}", password);
        SocialUser admin = new SocialUser(username,
//                              "{noop}123456",
                password,
                true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        return admin;
    }
}

提供qq服務的配置

這個單獨拿出來來。方便自動配置和切換

/**
 *
 */
package cn.mrcode.imooc.springsecurity.securitycore.properties;

/**
 * 沒有預設值;由使用方注入
 * @author zhailiang
 */
public class QQProperties {
    /**
     * Application id.
     */
    private String appId;

    /**
     * Application secret.
     */
    private String appSecret;
    private String providerId = "qq";
/**
 *
 */
package cn.mrcode.imooc.springsecurity.securitycore.properties;

/**
 * @author zhailiang
 *
 */
public class SocialProperties {

    private QQProperties qq = new QQProperties();
package cn.mrcode.imooc.springsecurity.securitycore.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/3 15:28
 * @date 2018/8/3 15:28
 * @since 1.0
 */
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
    /** imooc.security.browser 路徑下的配置會被對映到該配置類中 */
    private BrowserProperties browser = new BrowserProperties();
    private ValidateCodeProperties code = new ValidateCodeProperties();
    private SocialProperties social = new SocialProperties();

上面的程式碼是為了提供配置功能,和之前這些配置一樣的思路

下面的配置是為qq登入提供服務商

package cn.mrcode.imooc.springsecurity.securitycore.qq.config;

import cn.mrcode.imooc.springsecurity.securitycore.properties.QQProperties;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import cn.mrcode.imooc.springsecurity.securitycore.qq.connet.QQOAuth2ConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactory;

/**
 * autoconfigure2.04中已經不存在social的自動配置類了
 * org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 9:20
 */
@Configuration
// 當配置了app-id的時候才啟用
@ConditionalOnProperty(prefix = "imooc.security.social.qq", name = "app-id")
public class QQAutoConfig extends SocialConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer configurer,
                                       Environment environment) {
        configurer.addConnectionFactory(createConnectionFactory());
    }

    public ConnectionFactory<?> createConnectionFactory() {
        QQProperties qq = securityProperties.getSocial().getQq();
        return new QQOAuth2ConnectionFactory(qq.getProviderId(), qq.getAppId(), qq.getAppSecret());
    }

    // 後補:做到處理註冊邏輯的時候發現的一個bug:登入完成後,資料庫沒有資料,但是再次登入卻不用註冊了
    // 就懷疑是否是在記憶體中儲存了。結果果然發現這裡父類的記憶體ConnectionRepository覆蓋了SocialConfig中配置的jdbcConnectionRepository
    // 這裡需要返回null,否則會返回記憶體的 ConnectionRepository
  @Override
  public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
      return null;
  }
}

瀏覽器專案中的配置,需要使用apply把開啟social的配置檔案加入

cn.mrcode.imooc.springsecurity.securitybrowser.BrowserSecurityConfig

// 這裡目前注入的其實就是 之前寫的開啟social的配置類SocialConfig
@Autowired
  private SpringSocialConfigurer imoocSocialSecurityConfig;

  .apply(imoocSocialSecurityConfig)

最後注意把 “/auth/*” 路徑放行;

頁面提供qq登入地址

<h3>社交登入</h3>
<!--不支援get請求-->
<form action="/auth/qq" method="post">
    <button type="submit">QQ登入</button>
</form>