社交登入又稱作社會化登入(Social Login),是指網站的使用者可以使用騰訊QQ、人人網、開心網、新浪微博、搜狐微博、騰訊微博、淘寶、豆瓣、MSN、Google等社會化媒體賬號登入該網站。
OAuth2.0的認證流程示意圖
- 請求第三方應用
- 第三方應用將使用者請求導向服務提供商
- 使用者同意授權
- 服務提供商返回code
- client根據code去服務提供商換取令牌
- 返回令牌
- 獲取使用者資訊
在標準的OAuth2協議中,1-6
步都是固定,只有最後一步,不通的服務提供商返回的使用者資訊是不同的。Spring Social
已經為我們封裝好了1-6
步。
使用Spring Social
準備工作
- 在qq互聯申請個人開發者,獲得appId和appKey或者使用 SpringForAll貢獻出來的
- 配置本地host 新增
127.0.0.1 www.ictgu.cn
- 資料庫執行以下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);
複製程式碼
- 專案埠設定為
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>
複製程式碼
目錄結構
- 'api' 定義api繫結的公共介面
- 'config' qq的一些配置資訊
- '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());
}
複製程式碼
程式碼下載
從我的 github 中下載,github.com/longfeizhen…