Spring Security 開發(fā)2

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

OAth

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

OAuth 的角色和術語
1、Provider:服務提供商化撕,象 QQ几晤、微信等,他們存儲了用戶的信息植阴,能夠提供 Token蟹瘾;
2、Resource Owner:資源所有者掠手,用戶的自有數(shù)據(jù)憾朴,數(shù)據(jù)雖然存放在 QQ 等平臺,但數(shù)據(jù)擁有者是用戶喷鸽;
3众雷、第三方 Client:也就是想要獲取QQ等提供商存儲的用戶信息的這些應用;
4做祝、Authorization Server:認證服務器砾省,認證用戶身份并且產(chǎn)生令牌;
5混槐、Resource Server:資源服務器编兄,保留用戶數(shù)據(jù)的地方,驗證令牌声登;

OAuth 流程


image

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

授權碼模式的流程(主流模式)


image

其中第二步是導向服務提供商碰煌,防止偽造同意舒岸。第四步是服務器端后臺進行的。

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

流程


image

簡單說:通過供應商的賬號密碼信息之后獲取用戶基本信息芦圾,組裝成我們的認證信息蛾派,完成登錄。
而Spring Social是一個過濾器个少,完成上面的操作洪乍。

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


image

QQ 登錄

根據(jù)上面的依賴分析可知,需要首先構造OAuth2Operations和API相關的類芯咧。

首先來操作 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ù)構建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ù)的父構造
        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 我們采用默認實現(xiàn) OAuth2Template 來進行慕嚷,緊跟著就開始構造 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ù)挠说,先從這開始澡谭,構建 QQAdapter

public class QQAdapter implements ApiAdapter<QQ>{

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

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

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

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

}

有了 ServiceProvider 和 QQAdapter 就可以構造 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,這個可以使用默認的 JdbcUsersConnectionRepository,只需要配置即可

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter{

    @Autowired private DataSource dataSource;
    /**
     * 
     * 需要預先建立表
     * 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)生一個授權集合
        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設置到總的配置類中灶体,以便調(diào)用阅签。

有了這個配置,我們就可以構造 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

問題

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

image

首先驗證是不是SocialAuthenticationToken鱼辙,然后通過他拿到 Connection廉嚼,再通過 Connection 拿到userId,然后根據(jù) userId 獲取UserDetails倒戏,最后重新構造一個新的SocialAuthenticationToken返回怠噪。
我們注意到,一旦userId 為空杜跷,就會報出BadCredentialsException異常傍念,而處理這個異常的過濾器是SocialAuthenticationFilter,可以查看這個源碼葱椭。
image

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

新建一個注冊頁面秦陋,配置 properties 屬性,配置權限入口


image

image

image

為了更好的體驗治笨,需要在注冊頁面顯示 QQ 用戶信息驳概,或者用戶注冊后,需要綁定相應的信息旷赖,需要一個工具類ProviderSignInUtils顺又,該工具類構造方法需要兩個參數(shù)

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

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


image

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

整個流程如下圖


image
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末俯萌,一起剝皮案震驚了整個濱河市果录,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咐熙,老刑警劉巖弱恒,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異棋恼,居然都是意外死亡返弹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門爪飘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來义起,“玉大人,你說我怎么就攤上這事师崎∧眨” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長穷蛹。 經(jīng)常有香客問我土陪,道長,這世上最難降的妖魔是什么肴熏? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任鬼雀,我火速辦了婚禮,結果婚禮上蛙吏,老公的妹妹穿的比我還像新娘源哩。我一直安慰自己,他們只是感情好鸦做,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布励烦。 她就那樣靜靜地躺著,像睡著了一般泼诱。 火紅的嫁衣襯著肌膚如雪坛掠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天治筒,我揣著相機與錄音屉栓,去河邊找鬼。 笑死耸袜,一個胖子當著我的面吹牛友多,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播堤框,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼域滥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜈抓?” 一聲冷哼從身側響起启绰,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎资昧,沒想到半個月后酬土,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荆忍,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡格带,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了刹枉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叽唱。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖微宝,靈堂內(nèi)的尸體忽然破棺而出棺亭,到底是詐尸還是另有隱情,我是刑警寧澤蟋软,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布镶摘,位于F島的核電站嗽桩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏凄敢。R本人自食惡果不足惜碌冶,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望涝缝。 院中可真熱鬧扑庞,春花似錦、人聲如沸拒逮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽滩援。三九已至栅隐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間玩徊,已是汗流浹背约啊。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留佣赖,地道東北人恰矩。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像憎蛤,于是被迫代替她去往敵國和親外傅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容