社交登陸又稱做社會化登陸(Social Login),是指網站的用戶可使用騰訊QQ、人人網、開心網、新浪微博、搜狐微博、騰訊微博、淘寶、豆瓣、MSN、Google等社會化媒體帳號登陸該網站。html
在上一章Spring-Security源碼分析三-Spring-Social社交登陸過程中,咱們已經實現了使用Spring Social
+Security
的QQ社交登陸。本章咱們將實現微信的社交登陸。(微信和QQ登陸的大致流程相同,但存在一些細節上的差別,下面咱們來簡單實現一下)java
appid
和appsecret
爲了方便你們測試,博主在某寶租用了一個月的appid和appSecretgit
appid | wxfd6965ab1fc6adb2 |
---|---|
appsecret | 66bb4566de776ac699ec1dbed0cc3dd1 |
參考github
api
定義api綁定的公共接口config
微信的一些配置信息connect
與服務提供商創建鏈接所需的一些類。public interface Weixin {
WeixinUserInfo getUserInfo(String openId);
}
複製代碼
這裏咱們看到相對於QQ的getUserInfo
微信多了一個參數openId
。這是由於微信文檔中在OAuth2.0的認證流程示意圖第五步時,微信的openid
同access_token
一塊兒返回。而Spring Social
獲取access_token
的類AccessGrant.java
中沒有openid
。所以咱們本身須要擴展一下Spring Social
獲取令牌的類(AccessGrant.java
);web
@Data
public class WeixinAccessGrant extends AccessGrant{
private String openId;
public WeixinAccessGrant() {
super("");
}
public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
super(accessToken, scope, refreshToken, expiresIn);
}
}
複製代碼
public class WeiXinImpl extends AbstractOAuth2ApiBinding implements Weixin {
/** * 獲取用戶信息的url */
private static final String WEIXIN_URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";
private ObjectMapper objectMapper = new ObjectMapper();
public WeiXinImpl(String accessToken) {
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
}
/** * 獲取用戶信息 * * @param openId * @return */
@Override
public WeixinUserInfo getUserInfo(String openId) {
String url = WEIXIN_URL_GET_USER_INFO + openId;
String result = getRestTemplate().getForObject(url, String.class);
if(StringUtils.contains(result, "errcode")) {
return null;
}
WeixinUserInfo userInfo = null;
try{
userInfo = objectMapper.readValue(result,WeixinUserInfo.class);
}catch (Exception e){
e.printStackTrace();
}
return userInfo;
}
/** * 使用utf-8 替換默認的ISO-8859-1編碼 * @return */
@Override
protected List<HttpMessageConverter<?>> getMessageConverters() {
List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
messageConverters.remove(0);
messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return messageConverters;
}
}
複製代碼
與QQ
獲取用戶信息相比,微信
的實現類中少了一步經過access_token
獲取openid
的請求。openid
由本身定義的擴展類WeixinAccessGrant
中獲取;spring
@Slf4j
public class WeixinOAuth2Template extends OAuth2Template {
private String clientId;
private String clientSecret;
private String accessTokenUrl;
private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";
public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessTokenUrl = accessTokenUrl;
}
/* (non-Javadoc) * @see org.springframework.social.oauth2.OAuth2Template#exchangeForAccess(java.lang.String, java.lang.String, org.springframework.util.MultiValueMap) */
@Override
public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap<String, String> parameters) {
StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl);
accessTokenRequestUrl.append("?appid="+clientId);
accessTokenRequestUrl.append("&secret="+clientSecret);
accessTokenRequestUrl.append("&code="+authorizationCode);
accessTokenRequestUrl.append("&grant_type=authorization_code");
accessTokenRequestUrl.append("&redirect_uri="+redirectUri);
return getAccessToken(accessTokenRequestUrl);
}
public AccessGrant refreshAccess(String refreshToken, MultiValueMap<String, String> additionalParameters) {
StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL);
refreshTokenUrl.append("?appid="+clientId);
refreshTokenUrl.append("&grant_type=refresh_token");
refreshTokenUrl.append("&refresh_token="+refreshToken);
return getAccessToken(refreshTokenUrl);
}
@SuppressWarnings("unchecked")
private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {
log.info("獲取access_token, 請求URL: "+accessTokenRequestUrl.toString());
String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);
log.info("獲取access_token, 響應內容: "+response);
Map<String, Object> result = null;
try {
result = new ObjectMapper().readValue(response, Map.class);
} catch (Exception e) {
e.printStackTrace();
}
//返回錯誤碼時直接返回空
if(StringUtils.isNotBlank(MapUtils.getString(result, "errcode"))){
String errcode = MapUtils.getString(result, "errcode");
String errmsg = MapUtils.getString(result, "errmsg");
throw new RuntimeException("獲取access token失敗, errcode:"+errcode+", errmsg:"+errmsg);
}
WeixinAccessGrant accessToken = new WeixinAccessGrant(
MapUtils.getString(result, "access_token"),
MapUtils.getString(result, "scope"),
MapUtils.getString(result, "refresh_token"),
MapUtils.getLong(result, "expires_in"));
accessToken.setOpenId(MapUtils.getString(result, "openid"));
return accessToken;
}
/** * 構建獲取受權碼的請求。也就是引導用戶跳轉到微信的地址。 */
public String buildAuthenticateUrl(OAuth2Parameters parameters) {
String url = super.buildAuthenticateUrl(parameters);
url = url + "&appid="+clientId+"&scope=snsapi_login";
return url;
}
public String buildAuthorizeUrl(OAuth2Parameters parameters) {
return buildAuthenticateUrl(parameters);
}
/** * 微信返回的contentType是html/text,添加相應的HttpMessageConverter來處理。 */
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
}
複製代碼
與QQ
處理令牌類相比多了三個全局變量而且複寫了exchangeForAccess
方法。這是由於微信
在經過code
獲取access_token
是傳遞的參數是appid
和secret
而不是標準的client_id
和client_secret
。api
public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {
/** * 微信獲取受權碼的url */
private static final String WEIXIN_URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
/** * 微信獲取accessToken的url(微信在獲取accessToken時也已經返回openId) */
private static final String WEIXIN_URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";
public WeixinServiceProvider(String appId, String appSecret) {
super(new WeixinOAuth2Template(appId, appSecret, WEIXIN_URL_AUTHORIZE, WEIXIN_URL_ACCESS_TOKEN));
}
@Override
public Weixin getApi(String accessToken) {
return new WeiXinImpl(accessToken);
}
}
複製代碼
public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> {
/** * @param appId * @param appSecret */
public WeixinConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter());
}
/** * 因爲微信的openId是和accessToken一塊兒返回的,因此在這裏直接根據accessToken設置providerUserId便可,不用像QQ那樣經過QQAdapter來獲取 */
@Override
protected String extractProviderUserId(AccessGrant accessGrant) {
if(accessGrant instanceof WeixinAccessGrant) {
return ((WeixinAccessGrant)accessGrant).getOpenId();
}
return null;
}
/* (non-Javadoc) * @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant) */
public Connection<Weixin> createConnection(AccessGrant accessGrant) {
return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));
}
/* (non-Javadoc) * @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.connect.ConnectionData) */
public Connection<Weixin> createConnection(ConnectionData data) {
return new OAuth2Connection<Weixin>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
}
private ApiAdapter<Weixin> getApiAdapter(String providerUserId) {
return new WeixinAdapter(providerUserId);
}
private OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() {
return (OAuth2ServiceProvider<Weixin>) getServiceProvider();
}
}
複製代碼
public class WeixinAdapter implements ApiAdapter<Weixin> {
private String openId;
public WeixinAdapter() {
}
public WeixinAdapter(String openId) {
this.openId = openId;
}
@Override
public boolean test(Weixin api) {
return true;
}
@Override
public void setConnectionValues(Weixin api, ConnectionValues values) {
WeixinUserInfo userInfo = api.getUserInfo(openId);
values.setProviderUserId(userInfo.getOpenid());
values.setDisplayName(userInfo.getNickname());
values.setImageUrl(userInfo.getHeadimgurl());
}
@Override
public UserProfile fetchUserProfile(Weixin api) {
return null;
}
@Override
public void updateStatus(Weixin api, String message) {
}
}
複製代碼
@Configuration
public class WeixinAuthConfig extends SocialAutoConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private ConnectionSignUp myConnectionSignUp;
@Override
protected ConnectionFactory<?> createConnectionFactory() {
return new WeixinConnectionFactory(DEFAULT_SOCIAL_WEIXIN_PROVIDER_ID, SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_ID,
SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_SECRET);
}
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
connectionFactoryLocator, Encryptors.noOpText());
if (myConnectionSignUp != null) {
repository.setConnectionSignUp(myConnectionSignUp);
}
return repository;
}
/** * /connect/weixin POST請求,綁定微信返回connect/weixinConnected視圖 * /connect/weixin DELETE請求,解綁返回connect/weixinConnect視圖 * @return */
@Bean({"connect/weixinConnect", "connect/weixinConnected"})
@ConditionalOnMissingBean(name = "weixinConnectedView")
public View weixinConnectedView() {
return new SocialConnectView();
}
}
複製代碼
因爲社交登陸都是經過SocialAuthenticationFilter
過濾器攔截的,若是 上一章 已經配置過,則本章不須要配置。微信
效果以下:app
從個人 github 中下載,github.com/longfeizhen…ide