Spring Security 開發(fā)2

接著上文,談到了表單登錄和手機驗證碼登錄,有時候我們需要使用第三方登錄,那么這個和 SS 如何結(jié)合呢?

OAth

OAuth簡介
我們?yōu)榱耸褂?QQ、微信、微博用戶名和密碼來獲取用戶,簡單的說可以使用用戶的用戶名和密碼進入第三方系統(tǒng),然后抓取信息。泄漏自私的賬號密碼這樣的事用戶肯定不干,那么如何讓用戶既不泄漏自己的密碼又能把自己的第三方賬號的某些信息提供到我們系統(tǒng)呢?這就得使用 OAuth,他使用的是一個 Token 來進行訪問第三方系統(tǒng),第三方提供這個 Token 只能提供部分服務(wù),而且這個 Token 是有時效性的,在使用 Token 之前也是需要用戶確認(rèn)的。

OAuth 的角色和術(shù)語
1、Provider:服務(wù)提供商,象 QQ、微信等,他們存儲了用戶的信息,能夠提供 Token;
2、Resource Owner:資源所有者,用戶的自有數(shù)據(jù),數(shù)據(jù)雖然存放在 QQ 等平臺,但數(shù)據(jù)擁有者是用戶;
3、第三方 Client:也就是想要獲取QQ等提供商存儲的用戶信息的這些應(yīng)用;
4、Authorization Server:認(rèn)證服務(wù)器,認(rèn)證用戶身份并且產(chǎn)生令牌;
5、Resource Server:資源服務(wù)器,保留用戶數(shù)據(jù)的地方,驗證令牌;

OAuth 流程


image

其中第二步驟同意授權(quán)有四種實現(xiàn):
授權(quán)碼模式(目前使用最多)、密碼模式、客戶端模式、簡化模式

授權(quán)碼模式的流程(主流模式)


image

其中第二步是導(dǎo)向服務(wù)提供商,防止偽造同意。第四步是服務(wù)器端后臺進行的。

Spring Social開發(fā)第三方登錄

流程


image

簡單說:通過供應(yīng)商的賬號密碼信息之后獲取用戶基本信息,組裝成我們的認(rèn)證信息,完成登錄。
而Spring Social是一個過濾器,完成上面的操作。

Spring Social對應(yīng)的內(nèi)部實現(xiàn)分析
ServiceProvider:相當(dāng)于服務(wù)提供商的封裝類;
OAuth2Operations:相當(dāng)于1-5步驟的通用操作;
API:第6步個性化操作,有的提供商提供3個字段,有點提供5個字段不等;
第7步與提供商沒啥關(guān)系,只與我們自己的有關(guān),他有如下的接口和類
Connection:封裝前六步獲取的鏈接信息,具有固定信息;
ConnectionFactory:生成上面鏈接的工廠,而創(chuàng)建這些鏈接,就需要ServiceProvider;由于第六步的每家不同的數(shù)據(jù),就需要把不同的數(shù)據(jù)封裝成一個標(biāo)準(zhǔn)的數(shù)據(jù),才能構(gòu)造 Connection,所以還需要ApiAdapter;
另外,平臺用戶 A 是如何和自己系統(tǒng)用戶張三對應(yīng)上的呢,是在數(shù)據(jù)庫層面有一張userConnection 表進行映射,而操作這個的表的是userConnectionRepository 這個類


image

QQ 登錄

根據(jù)上面的依賴分析可知,需要首先構(gòu)造OAuth2Operations和API相關(guān)的類。

首先來操作 QQAPI 部分
新建 QQUserInfo 類來封裝 QQ 用戶信息;
新建 QQ 接口,提供一個方法,返回 QQUserInfo;
新建 QQ 接口的實現(xiàn) QQImpl,該類繼承 AbstractOAuth2ApiBinding 和實現(xiàn) QQ 接口;

查看AbstractOAuth2ApiBinding源碼可以發(fā)現(xiàn),需要注意是多實例對象。


image

接下來我們查閱下如何訪問 QQ 用戶信息的 API 文檔


QQ 互聯(lián)

請求說明
參數(shù)說明
返回參數(shù)

根據(jù)返回參數(shù)構(gòu)建QQUserInfo屬性字段,省略

然后開始編寫 QQImpl 的具體實現(xiàn)

public class QQImpl extends AbstractOAuth2ApiBinding implements QQ{

    //獲取 openID 的 URL
    private static final String URL_GET_OPEN_ID="https://graph.qq.com/oauth2.0/me?access_token=%s";
    
    //獲取用戶信息的 URL
    //https://graph.qq.com/user/get_user_info?access_token=YOUR_ACCESS_TOKEN&oauth_consumer_key=YOUR_APP_ID&openid=YOUR_OPENID
    //AccessToken 參數(shù)交給父類處理,這里不用掛載
    private static final String URL_GET_USER_INFO="https://graph.qq.com/user/get_user_info?&oauth_consumer_key=%s&openid=%s";
    
    private String appId;
    
    private String openId;
    
    public QQImpl(String accessToken,String appId) {
        //如果是一個參數(shù)的話,會把accessToken塞到請求頭,我們需要的是掛載的 URL 后面,所以這里是2個參數(shù)的父構(gòu)造
        super(accessToken,TokenStrategy.ACCESS_TOKEN_PARAMETER);
        this.appId=appId;
        //openid 需要請求后才能獲取
        String url=String.format(URL_GET_OPEN_ID, accessToken);
        String result=super.getRestTemplate().getForObject(url, String.class);
        //從返回字符串里面進行截取
        this.openId=StringUtils.substringBetween(result, "\"openId\":", "}");
    }
    
    @Override
    public QQUserInfo getQQUserInfo() {
        String url=String.format(URL_GET_USER_INFO, appId,openId);
        String result=super.getRestTemplate().getForObject(url, String.class);
        System.out.println(result);
        ObjectMapper om=new ObjectMapper();
        QQUserInfo qqUserInfo=null;
        try {
            qqUserInfo=om.readValue(result, QQUserInfo.class);
            qqUserInfo.setOpenId(openId);
            return qqUserInfo;
        } catch (Exception e) {
            throw new RuntimeException("獲取用戶失敗");
        } 
    }

}

到這里,完成了提供商的 API 操作。

接下來 OAuth2Operations 我們采用默認(rèn)實現(xiàn) OAuth2Template 來進行,緊跟著就開始構(gòu)造 ServiceProvider 對象,新建 QQServiceProvider

public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ>{

    private String appId;
    
    private static final String authorizeUrl="https://graph.qq.com/oauth2.0/authorize";
    
    private static final String accessTokenUrl="https://graph.qq.com/oauth2.0/token";
    
    
    public QQServiceProvider(String appId,String appSecret) {
        //authorizeUrl 第一步的 URL
        //accessTokenUrl 第四步的 URL
        super(new OAuth2Template(appId, appSecret, authorizeUrl, accessTokenUrl));
        
    }

    @Override
    public QQ getApi(String accessToken) {
        return new QQImpl(accessToken,appId);
    }

}

到這里,右邊提供商方面的代碼完成,接下來完成左邊部分的代碼。
首先看到左邊包含最里面的類是一個適配器類,把 QQ 用戶數(shù)據(jù)包裝成 Connection 通用數(shù)據(jù),先從這開始,構(gòu)建 QQAdapter

public class QQAdapter implements ApiAdapter<QQ>{

    @Override
    public boolean test(QQ api) {
        // 測試 QQ 服務(wù)是否可用,這里假設(shè)可用
        return true;
    }

    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        //把提供商提供的個性化數(shù)據(jù)包裝成 Connect 通用的數(shù)據(jù)格式
        QQUserInfo userInfo = api.getQQUserInfo();
        
        values.setDisplayName(userInfo.getNickname());//設(shè)置顯示名稱
        values.setImageUrl(userInfo.getFigureurl_qq_1());//設(shè)置圖像
        values.setProfileUrl(null);//個人主頁
        values.setProviderUserId(userInfo.getOpenId());//提供商給用戶的唯一標(biāo)識
    }

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

    @Override
    public void updateStatus(QQ api, String message) {
        // 發(fā)條消息更新狀態(tài)
    }

}

有了 ServiceProvider 和 QQAdapter 就可以構(gòu)造 Connectionfactory了。

public class QQConectionFactory extends OAuth2ConnectionFactory<QQ>{

    public QQConectionFactory(String providerId, String appId,String appSecret) {
        super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
    }

}

有了ConectionFactory就有了 Connection,然后需要UsersConnectionRepository,這個可以使用默認(rèn)的 JdbcUsersConnectionRepository,只需要配置即可

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter{

    @Autowired private DataSource dataSource;
    /**
     * 
     * 需要預(yù)先建立表
     * 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);
     * 
     */
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        //參數(shù)1是數(shù)據(jù)源
        //參數(shù)2是鏈接工廠,可能有多個(QQ、微信),會找到自己需要的
        //參數(shù)3是加密形式,這些數(shù)據(jù)敏感
        return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
    }
}

以此同時,我們的UserDetailService 也需要增加社交登錄實現(xiàn)

@Component("userDetailsService")
public class CustomerUserDetailService implements UserDetailsService,SocialUserDetailsService{

    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根據(jù)用戶名去數(shù)據(jù)庫查詢用戶信息
        //可以注入 jdbc,mybatis 等 DAO 
        //這里方便演示,直接在代碼里面做了
        System.out.println("=======表單登錄========="+username);
        //User 對象已經(jīng)實現(xiàn)了UserDetails
        //AuthorityUtils.commaSeparatedStringToAuthorityList 方法是以逗號分割產(chǎn)生一個授權(quán)集合
        User user=new User(username, 
                passwordEncoder.encode("123456"), //其實是 DB 存的加密密碼
                true,//賬號可用
                true,//賬號不過期
                true,//密碼不過期
                true,//賬號沒有鎖定
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        return user;
    }

    @Override
    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        System.out.println("======社交用戶登錄=========="+userId);
        SocialUser user=new SocialUser(userId, 
                passwordEncoder.encode("123456"), //其實是 DB 存的加密密碼
                true,//賬號可用
                true,//賬號不過期
                true,//密碼不過期
                true,//賬號沒有鎖定
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        return user;
    }

}

我們還需要配置一些 QQ 登錄的基本信息,新建 QQProperties

public class QQProperties extends SocialProperties{

    private String providerId="qq";

    public String getProviderId() {
        return providerId;
    }

    public void setProviderId(String providerId) {
        this.providerId = providerId;
    }
}

并且把QQProperties設(shè)置到總的配置類中,以便調(diào)用。

有了這個配置,我們就可以構(gòu)造 QQConnectionFactory 了,為了得到這個工廠,我們需要新建一個配置類

@Configuration
@ConditionalOnProperty("cn.ts.qq")//希望有配置才起作用
public class QQConfig extends SocialAutoConfigurerAdapter{

    @Autowired private SecurityProperties securityProperties;
    
    @Override
    protected ConnectionFactory<?> createConnectionFactory() {
        
        return new QQConectionFactory(
                securityProperties.getQq().getProviderId(), 
                securityProperties.getQq().getAppId(),
                securityProperties.getQq().getAppSecret()
                );
    }

}

為了能夠把社交登錄布置到過濾器鏈上,需要配置一個SpringSocialConfigurer到 http
直接在SocialConfig里面增加一個方法,返回SpringSocialConfigurer

@Bean
    public SpringSocialConfigurer qqSocialConfigurer(){
        return new SpringSocialConfigurer();
    }

在WebSecurityConfig里面注入,并且配置到HttpSecurity http這個對象上


image

頁面增加 QQ 登錄

<h3>社交登錄</h3>
<a href="/auth/qq">QQ 登錄</a>

其中“/auth/”開頭的請求都會被SocialAuthenticationFilter 攔截;后面的qq 是 providerId

問題

由于我們的表里面需要錢三個字段組合成復(fù)合組件,我們現(xiàn)在只有后2兩個 ID,沒有我們自己系統(tǒng)的 ID,這就需要引導(dǎo)用戶到注冊界面。童年故事我們查看SocialAuthenticationProvider源碼也發(fā)現(xiàn)

image

首先驗證是不是SocialAuthenticationToken,然后通過他拿到 Connection,再通過 Connection 拿到userId,然后根據(jù) userId 獲取UserDetails,最后重新構(gòu)造一個新的SocialAuthenticationToken返回。
我們注意到,一旦userId 為空,就會報出BadCredentialsException異常,而處理這個異常的過濾器是SocialAuthenticationFilter,可以查看這個源碼。
image

這個異常被捕獲之后的處理邏輯如上代碼,可以發(fā)現(xiàn),需要配置一個注冊入口。

新建一個注冊頁面,配置 properties 屬性,配置權(quán)限入口


image

image

image

為了更好的體驗,需要在注冊頁面顯示 QQ 用戶信息,或者用戶注冊后,需要綁定相應(yīng)的信息,需要一個工具類ProviderSignInUtils,該工具類構(gòu)造方法需要兩個參數(shù)

public ProviderSignInUtils( ConnectionFactoryLocator connectionFactoryLocator,UsersConnectionRepository connectionRepository) {
        this(new HttpSessionSessionStrategy(),connectionFactoryLocator,connectionRepository);
    }

第一個參數(shù),SpringBoot 已經(jīng)給我們注冊好了,直接注入使用,第二可以在 SocialConfig 里面看到


image

所以直接在這個類里面構(gòu)造一個 Bean,作為其他的地方的注入即可

整個流程如下圖


image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容