圖一
基於SpringSocial實現qq登錄,要走一個OAuth流程,拿到服務提供商qq返回的用戶信息。
由上篇介紹的可知,用戶信息被封裝在了Connection里,所以最終要拿到Connection
1,Connection <---- ConnectionFactory:拿到一個Connection,就需要一個ConnectionFactory工廠
2,ConnectionFactory:需要ServiceProvider 服務提供商、ApiAdapter api適配器,在服務提供商和業務系統user之間轉換
3,ServiceProvider :需要 OAuthe2Operations、Api 讀取用戶信息
4,在數據庫中建數據庫表
上圖中括號里都是Spring Social對對應接口的默認實現,代碼中使用它們。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
開始開發:
1,構建ServiceProvider
1.1 構建ServiceProvider需要的Api(api 用來獲取用戶信息,就是圖一的第6步,自定義QQ接口,繼承 Spring Social的默認實現 AbstractOAuth2ApiBinding )
代碼:
接口
public interface QQ { /** * 獲取qq用戶信息*/ QQUserInfo getUserInfo() throws Exception; }
實現類:
package com.imooc.security.core.social.qq.api;/** * 流程中的Api。 * ClassName: QQImpl * @Description: * ***********注意************* * 這個是多例的,每個用戶不一樣進來他們的accessToken、openid是不一樣的 * 所以不能@Component聲明為spring組件!!! * ***********注意************* * @author lihaoyang * @date 2018年3月6日 */ public class QQImpl extends AbstractOAuth2ApiBinding implements QQ{ /** * 調用qq的get_user_info * 1:3個參數,OAuth2.0協議的通用三個參數: * access_token: 父類已提供 * appid://申請QQ登錄成功后,分配給應用的appid * openid://用戶的ID,與QQ號碼一一對應。 * * 2:2個路徑 * 獲取openid的路徑: * https://graph.qq.com/oauth2.0/me?access_token=YOUR_ACCESS_TOKEN * 獲取用戶信息的路徑: * https://graph.qq.com/user/get_user_info?access_token=*************&oauth_consumer_key=12345&openid= */ private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s"; private static final String URL_GET_USRE_INFO = "https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=12345&openid=%s"; private String appId; private String openId; private ObjectMapper objectMapper = new ObjectMapper(); /** * 實例化時獲取openid * <p>Description: </p> * @param accessToken * @param appId */ public QQImpl(String accessToken , String appId){ //父類默認構造會把accessToken放在請求頭里,這是不符合qq要求的放在url參數里的,所以掉一下作為參數的構造 super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER); this.appId = appId; String url = String.format(URL_GET_USRE_INFO, accessToken); String result = getRestTemplate().getForObject(url, String.class);//調用父類的restTemplate發請求,獲取openid System.err.println(result); //{"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} //截取openid this.openId = StringUtils.substringBetween(result, "\"openid\"", "}"); } @Override public QQUserInfo getUserInfo() throws Exception { //accessToken已在父類掛在了參數 String url = String.format(URL_GET_USRE_INFO, appId,openId); String result = getRestTemplate().getForObject(url, String.class); System.err.println(result); QQUserInfo userInfo = objectMapper.readValue(result, QQUserInfo.class); return userInfo; } }
AbstractOAuth2ApiBinding:
********* 登錄QQ互聯官網get_user_info接口http://wiki.connect.qq.com/get_user_info *************
需要的3個參數:
QQUserInfo照着qq要求的構造:
1.2,有了Api,就可以構建ServiceProvider,因為ServiceProvider需要的第三個參數OAuthOperations可以使用Spring Social提供的OAuth2Template
代碼:
package com.imooc.security.core.social.qq.connect; import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider; import org.springframework.social.oauth2.OAuth2Template; import com.imooc.security.core.social.qq.api.QQ; import com.imooc.security.core.social.qq.api.QQImpl; /** * QQ服務提供商 * ClassName: QQServiceProvider * @Description: * 需要繼承spring social的默認實現AbstractOAuth2ServiceProvider * 泛型是指獲取用戶信息的Api,類型就是QQ * @author lihaoyang * @date 2018年3月6日 */ public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ>{ private String appId; //將用戶引導到獲取授權碼的地址 private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize"; //拿着授權碼申請令牌token的地址 private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token"; /** * 返回ServiceProvider需要的OAuthOperations * <p>Description: </p> * @param appId 不同的應用是不一樣的 * @param appSecret 不同的應用是不一樣的 */ public QQServiceProvider(String appId , String appSecret) { super(new OAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN)); } /** * 返回ServiceProvider需要的Api */ @Override public QQ getApi(String accessToken) { return new QQImpl(accessToken, appId); } }
到此為止,Service已構建好
2,構建ConnectionFactory工廠,產生Connection
新建QQAdapter,實現ApiAdapter接口:
package com.imooc.security.core.social.qq.connect; import org.springframework.social.connect.ApiAdapter; import org.springframework.social.connect.ConnectionValues; import org.springframework.social.connect.UserProfile; import com.imooc.security.core.social.qq.api.QQ; import com.imooc.security.core.social.qq.api.QQUserInfo; /** * 在服務提供商qq和第三方應用之間做用戶信息的轉換 * ClassName: QQAdapter * @Description: * 在服務提供商qq和第三方應用之間做用戶信息的轉換 * 實現 ApiAdapter接口,泛型是API接口,對應我們的QQ接口 * @author lihaoyang * @date 2018年3月6日 */ public class QQAdapter implements ApiAdapter<QQ>{ /** * 測試當前api是否可用,測試qq是否可用 */ @Override public boolean test(QQ api) { //就不掉了,直接true return true; } /** * Connection和api之間的適配 * ConnectionValues: * 創建Connection需要的數據項 * 從api中獲取數據,給ConnectionValues設置值 */ @Override public void setConnectionValues(QQ api, ConnectionValues values) { //獲取用戶信息 QQUserInfo userInfo = api.getUserInfo(); values.setDisplayName(userInfo.getNickname());//展示名,qq用戶名 values.setImageUrl(userInfo.getFigureurl_1()); //qq頭像 values.setProfileUrl(null); //個人主頁,qq沒有,微博有 values.setProviderUserId(userInfo.getOpenId()); } /** * */ @Override public UserProfile fetchUserProfile(QQ api) { // TODO Auto-generated method stub return null; } /** * 某些社交如微博才有 */ @Override public void updateStatus(QQ api, String message) { // do nothing } }
構建ConnectionFactory:QQConnectionFactory類實現ConnectionFactory接口,把構造器需要的 ServiceProvider (QQServiceProvider) ApiAdapter (QQAdapter) 給他
package com.imooc.security.core.social.qq.connect; import org.springframework.social.connect.support.OAuth2ConnectionFactory; import com.imooc.security.core.social.qq.api.QQ; /** * 創建Connection工廠 * ClassName: QQConnectionFactory * @Description: * 創建Connection工廠 * 繼承默認實現 OAuth2ConnectionFactory * 泛型:Api是什么,就是我們的QQ * @author lihaoyang * @date 2018年3月7日 */ public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> { /** * * 需要兩個對象: * 1,ServiceProvider --> QQServieProvider * 2,ApiAdapter --> QQApiAdapter * <p>Description: </p> * @param providerId * @param appId * @param appSecret */ public QQConnectionFactory(String providerId, String appId , String appSecret) { super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter()); } }
有了ConnectionFactory,就可以自己產生Connection了,我們不用管產生Connection的過程。
第四步:配置JdbcUsersConnectionRepository
有了Connection,還需要把Connection中的數據保存到數據庫中,所以還需要JdbcUsersConnectionRepository。Spring已經寫好了,只需要配置一下即可。
新建SocialConfig配置類,繼承SocialConfigurerAdapter,注入數據源
package com.imooc.security.core.social; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.encrypt.Encryptors; 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.UsersConnectionRepository; import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository; /** * SpringSocial相關配置 * ClassName: SocialConfig * @Description: 社交相關配置 * @author lihaoyang * @date 2018年3月7日 */ @Configuration @EnableSocial //把Spring Social相關的特性啟動 public class SocialConfig extends SocialConfigurerAdapter{ @Autowired private DataSource dataSource; /** * 配置JdbcUsersConnectionRepository */ @Override public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) { /** * 參數: * 1, dataSource:數據源 注進來 * 2,connectionFactoryLocator: * 根據條件去查找需要的 ConnectionFactory,因為系統里可能有多個ConnectionFactory,如qq,微信等,使用默認穿的 * 3,textEncryptor:把插入到數據庫的數據加密解密 */ return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());//先不加密 } }
新建存Connection數據的表,該sql在JdbcUsersConnectionRepository所在的包里:org.springframework.social.connect.jdbc
sql:
-- 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);
字段說明:
userId :業務系統用戶id
providerId :服務提供商id,是qq、微信、微博...
providerUserId :openId
rank :等級
displayName :昵稱
profileUrl :主頁
imageUrl :頭像
accessToken :
secret :
refreshToken :
expireTime :
在數據庫執行一下,這個表名不能變,但是可以根據公司需要加前綴,如加上imooc_
若加上了前綴,,則配置 JdbcUsersConnectionRepository 時需要指定一下前綴:
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
repository.setTablePrefix("imooc_");
用戶表單登錄時,我們建了MyUserDetailsService 用 實現了SpringSecurity的UserDetailsService接口通過用戶名來獲取用戶信息;社交登錄,spring提供了SocialUserDetailsService接口,通過userId獲取UserDetail用戶信息,在MyUserDetailsService 也實現SocialUserDetailsService來處理社交登錄,SocialUserDetails 是UserDetails的實現
package com.imooc.security.browser;/** * UserDetailsService是SpringSecurity的一個接口, * 只有一個方法:根據用戶名獲取用戶詳情 * ClassName: MyUserDetailService * @Description: * SocialUserDetailsService:SpringSocial查詢用戶信息的接口 * @author lihaoyang * @date 2018年2月28日 */ @Component public class MyUserDetailsService_ implements UserDetailsService,SocialUserDetailsService{ private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private PasswordEncoder passwordEncoder; /** * UserDetails接口,實際可以自己實現這個接口,返回自己的實現類 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("表單登錄用戶名:"+username); //根據用戶名查詢用戶信息 //User:springsecurity 對 UserDetails的一個實現 //為了演示在這里用passwordEncoder加密一下密碼,實際中在注冊時就加密,此處直接拿出密碼 // String password = passwordEncoder.encode("123456"); // System.err.println("加密后密碼: "+password); // //參數:用戶名|密碼|是否啟用|賬戶是否過期|密碼是否過期|賬戶是否鎖定|權限集合 // return new User(username,password,true,true,true,true,AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); return buildUser(username); } /** * 第三方登錄使用 */ @Override public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException { logger.info("設交用戶id:"+userId); return buildUser(userId); } private SocialUserDetails buildUser(String userId) { String password = passwordEncoder.encode("123456"); System.err.println("加密后密碼: "+password); //參數:用戶名|密碼|是否啟用|賬戶是否過期|密碼是否過期|賬戶是否鎖定|權限集合 return new SocialUser(userId,password,true,true,true,true,AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
第五步:配置appId、
QQProperties:
package com.imooc.security.core.properties; import org.springframework.boot.autoconfigure.social.SocialProperties; /** * QQ登錄相關配置 * ClassName: QQProperties * @Description: TODO * @author lihaoyang * @date 2018年3月7日 */ public class QQProperties extends SocialProperties { private String providerId = "qq"; public String getProviderId() { return providerId; } public void setProviderId(String providerId) { this.providerId = providerId; } }
SocialProperties 是spring提供的:
加一層SocialProperties:
package com.imooc.security.core.properties; /** * 第三方登錄相關配置 * ClassName: SocialProperties * @Description: TODO * @author lihaoyang * @date 2018年3月7日 */ public class SocialProperties { private QQProperties qq = new QQProperties(); public QQProperties getQq() { return qq; } public void setQq(QQProperties qq) { this.qq = qq; } }
QQAutoConfig:
package com.imooc.security.core.social.qq.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter; import org.springframework.context.annotation.Configuration; import org.springframework.social.connect.ConnectionFactory; import com.imooc.security.core.properties.QQProperties; import com.imooc.security.core.properties.SecurityProperties; import com.imooc.security.core.social.qq.connect.QQConnectionFactory; /** * * ClassName: QQAutoConfig * @Description: * @ConditionalOnProperty: 配置里有imooc.security.social.qq.app-id 這個類才會生效 * @author lihaoyang * @date 2018年3月7日 */ @Configuration @ConditionalOnProperty(prefix = "imooc.security.social.qq" , name = "app-id") public class QQAutoConfig extends SocialAutoConfigurerAdapter { @Autowired private SecurityProperties securityProperties; @Override protected ConnectionFactory<?> createConnectionFactory() { QQProperties qqConfig = securityProperties.getSocial().getQq(); return new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret()); } }
在demo項目的application.properties 里配置:
imooc.security.social.qq.app-id =
imooc.security.social.qq.app-secret =