Spring Security原始碼分析三:Spring Social實現QQ社交登入

鄭龍飛發表於2018-01-09

社交登入又稱作社會化登入(Social Login),是指網站的使用者可以使用騰訊QQ、人人網、開心網、新浪微博、搜狐微博、騰訊微博、淘寶、豆瓣、MSN、Google等社會化媒體賬號登入該網站。

OAuth2.0的認證流程示意圖

https://user-gold-cdn.xitu.io/2018/1/9/160da83977cbd95d?w=1390&h=1324&f=png&s=49273
https://user-gold-cdn.xitu.io/2018/1/9/160da83977cbd95d?w=1390&h=1324&f=png&s=49273

  1. 請求第三方應用
  2. 第三方應用將使用者請求導向服務提供商
  3. 使用者同意授權
  4. 服務提供商返回code
  5. client根據code去服務提供商換取令牌
  6. 返回令牌
  7. 獲取使用者資訊

在標準的OAuth2協議中,1-6步都是固定,只有最後一步,不通的服務提供商返回的使用者資訊是不同的。Spring Social已經為我們封裝好了1-6步。

使用Spring Social

準備工作

  1. qq互聯申請個人開發者,獲得appId和appKey或者使用 SpringForAll貢獻出來的
  2. 配置本地host 新增 127.0.0.1 www.ictgu.cn
  3. 資料庫執行以下sql
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);
複製程式碼
  1. 專案埠設定為80

引入Spring Social 模組

模組 描述
spring-social-core 提供社交連線框架和OAuth 客戶端支援
spring-social-config 提供Java 配置
spring-social-security 社交安全的一些支援
spring-social-web 管理web應用程式的連線
!--spring-social 相關-->
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-config</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-web</artifactId>
		</dependency>
複製程式碼

目錄結構

https://user-gold-cdn.xitu.io/2018/1/9/160da839741309a3?w=544&h=402&f=png&s=72620
https://user-gold-cdn.xitu.io/2018/1/9/160da839741309a3?w=544&h=402&f=png&s=72620

  1. 'api' 定義api繫結的公共介面
  2. 'config' qq的一些配置資訊
  3. 'connect'與服務提供商建立連線所需的一些類。

定義返回使用者資訊介面

public interface QQ {
    /**
     * 獲取使用者資訊
     * @return
     */
    QQUserInfo getUserInfo();
}
複製程式碼

實現返回使用者資訊介面

@Slf4j
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {

    //http://wiki.connect.qq.com/openapi%E8%B0%83%E7%94%A8%E8%AF%B4%E6%98%8E_oauth2-0
    private static final String QQ_URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
    //http://wiki.connect.qq.com/get_user_info(access_token由父類提供)
    private static final String QQ_URL_GET_USER_INFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
    /**
     * appId 配置檔案讀取
     */
    private String appId;
    /**
     * openId 請求QQ_URL_GET_OPENID返回
     */
    private String openId;
    /**
     * 工具類
     */
    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 構造方法獲取openId
     */
    public QQImpl(String accessToken, String appId) {
        //access_token作為查詢引數來攜帶。
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);

        this.appId = appId;

        String url = String.format(QQ_URL_GET_OPENID, accessToken);
        String result = getRestTemplate().getForObject(url, String.class);

        log.info("【QQImpl】 QQ_URL_GET_OPENID={} result={}", QQ_URL_GET_OPENID, result);

        this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
    }

    @Override
    public QQUserInfo getUserInfo() {
        String url = String.format(QQ_URL_GET_USER_INFO, appId, openId);
        String result = getRestTemplate().getForObject(url, String.class);

        log.info("【QQImpl】 QQ_URL_GET_USER_INFO={} result={}", QQ_URL_GET_USER_INFO, result);

        QQUserInfo userInfo = null;
        try {
            userInfo = objectMapper.readValue(result, QQUserInfo.class);
            userInfo.setOpenId(openId);
            return userInfo;
        } catch (Exception e) {
            throw new RuntimeException("獲取使用者資訊失敗", e);
        }
    }
}
複製程式碼

QQOAuth2Template處理qq返回的令牌資訊

@Slf4j
public class QQOAuth2Template extends OAuth2Template {
    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true);
    }

    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
        String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);

        log.info("【QQOAuth2Template】獲取accessToke的響應:responseStr={}" + responseStr);

        String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
        //http://wiki.connect.qq.com/使用authorization_code獲取access_token
        //access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
        String accessToken = StringUtils.substringAfterLast(items[0], "=");
        Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
        String refreshToken = StringUtils.substringAfterLast(items[2], "=");

        return new AccessGrant(accessToken, null, refreshToken, expiresIn);
    }


    /**
     * 坑,日誌debug模式才列印出來 處理qq返回的text/html 型別資料
     *
     * @return
     */
    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }
}
複製程式碼

QQServiceProvider連線服務提供商

public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {

    /**
     * 獲取code
     */
    private static final String QQ_URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
    /**
     * 獲取access_token 也就是令牌
     */
    private static final String QQ_URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
    private String appId;

    public QQServiceProvider(String appId, String appSecret) {
        super(new QQOAuth2Template(appId, appSecret, QQ_URL_AUTHORIZE, QQ_URL_ACCESS_TOKEN));
        this.appId = appId;
    }

    @Override
    public QQ getApi(String accessToken) {

        return new QQImpl(accessToken, appId);
    }
}
複製程式碼

QQConnectionFactory連線服務提供商的工廠類

public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {

    public QQConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
    }
}
複製程式碼

QQAdapter 適配spring Social預設的返回資訊

public class QQAdapter implements ApiAdapter<QQ> {
    @Override
    public boolean test(QQ api) {
        return true;
    }

    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        QQUserInfo userInfo = api.getUserInfo();

        values.setProviderUserId(userInfo.getOpenId());//openId 唯一標識
        values.setDisplayName(userInfo.getNickname());
        values.setImageUrl(userInfo.getFigureurl_qq_1());
        values.setProfileUrl(null);
    }

    @Override
    public UserProfile fetchUserProfile(QQ api) {
        return null;
    }

    @Override
    public void updateStatus(QQ api, String message) {

    }
}
複製程式碼

SocialConfig 社交配置主類

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    /**
     * 社交登入配類
     *
     * @return
     */
    @Bean
    public SpringSocialConfigurer merryyouSocialSecurityConfig() {
        String filterProcessesUrl = SecurityConstants.DEFAULT_SOCIAL_QQ_PROCESS_URL;
        MerryyouSpringSocialConfigurer configurer = new MerryyouSpringSocialConfigurer(filterProcessesUrl);
        return configurer;
    }

    /**
     * 處理註冊流程的工具類
     * @param factoryLocator
     * @return
     */
    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator factoryLocator) {
        return new ProviderSignInUtils(factoryLocator, getUsersConnectionRepository(factoryLocator));
    }

}
複製程式碼
QQAuthConfig 針對qq返回結果的一些操作
@Configuration
public class QQAuthConfig extends SocialAutoConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private ConnectionSignUp myConnectionSignUp;

    @Override
    protected ConnectionFactory<?> createConnectionFactory() {
        return new QQConnectionFactory(SecurityConstants.DEFAULT_SOCIAL_QQ_PROVIDER_ID, SecurityConstants.DEFAULT_SOCIAL_QQ_APP_ID, SecurityConstants.DEFAULT_SOCIAL_QQ_APP_SECRET);
    }

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
                connectionFactoryLocator, Encryptors.noOpText());
        if (myConnectionSignUp != null) {
            repository.setConnectionSignUp(myConnectionSignUp);
        }
        return repository;
    }
}
複製程式碼

MerryyouSpringSocialConfigurer自定義登入和註冊連線

public class MerryyouSpringSocialConfigurer extends SpringSocialConfigurer {

    private String filterProcessesUrl;

    public MerryyouSpringSocialConfigurer(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }

    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(filterProcessesUrl);
        filter.setSignupUrl("/register");
        return (T) filter;
    }
}
複製程式碼

開啟SocialAuthenticationFilter過濾器

@Autowired
    private SpringSocialConfigurer merryyouSpringSocialConfigurer;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()//使用表單登入,不再使用預設httpBasic方式
                .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//如果請求的URL需要認證則跳轉的URL
                .loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//處理表單中自定義的登入URL
                .and()
                .apply(merryyouSpringSocialConfigurer)
                .and()
                .authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM,
                SecurityConstants.DEFAULT_REGISTER_URL,
                "/register",
                "/social/info",
                "/**/*.js",
                "/**/*.css",
                "/**/*.jpg",
                "/**/*.png",
                "/**/*.woff2",
                "/code/image")
                .permitAll()//以上的請求都不需要認證
                //.antMatchers("/").access("hasRole('USER')")
                .and()
                .csrf().disable()//關閉csrd攔截
        ;
        //安全模組單獨配置
        authorizeConfigProvider.config(http.authorizeRequests());
    }
複製程式碼

效果如下:

https://user-gold-cdn.xitu.io/2018/1/9/160da83974d9557b?w=1323&h=789&f=gif&s=1257155
https://user-gold-cdn.xitu.io/2018/1/9/160da83974d9557b?w=1323&h=789&f=gif&s=1257155

程式碼下載

從我的 github 中下載,github.com/longfeizhen…

相關文章