Spring Security源碼分析三:Spring Social實現QQ社交登陸

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

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步。html

使用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());
    }
複製代碼

效果以下: java

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…git

相關文章
相關標籤/搜索