前後端分離項目 — SpringSocial 社交賬號登錄與註冊

1、前言

今天我們就來講解下最後一篇如何使用SpringSocial來處理類似微信、QQ社交賬號登錄自己的平臺,也就是大家說的第三方登錄,獲取社交賬戶所在平臺的用戶信息,與自己平臺信息做個綁定的操作,兩個系統之間是通過UserId交換信息的,這點一定要注意,平臺用戶表(Users)之間是社交用戶表(UserConnection)之間關係如下所示:

Users表:
id name age
1 David 18
2 Sam 22
3 Tom 30
UserConnection表:
userId providerId providerUserId accessToken secret
1 qq xxxxxxxxx
2 wechat xxxxxxxxx
3 sian xxxxxxxxx

這裏我要提一下,在你沒有登錄你自己的平臺系統之前你點擊微信登錄的時候要進行判斷是否已經綁定,判斷的依據是通過providerId和providerUserId從UserConnection表裏面查詢出社交賬戶信息,然後通過對應的userId再通過SocialUserDetailsService提供的loadUserByUserId方法查詢出系統平臺的User用戶信息。
1、如果已經綁定(說明上面查詢到了)就直接生成token並返回給前端;
2、如果沒有(說明上面沒有查詢到)就需要註冊並綁定了(往兩張表中插入數據並建立關係)。

2、QQ的實現

首先我們來看看QQ的實現,我把代碼都貼出來,遇到關鍵的地方我會講解一下,其它的自己看看就明白了

clipboard.png

上面是具體的代碼目錄結構,你沒必要和我一模一樣,只要能實現就好。

QQUserInfo
package com.awbeci.ssb.core.social.qq.api;

public class QQUserInfo {
    /**
     * 返回碼
     */
    private String ret;
    /**
     * 如果ret<0,會有相應的錯誤信息提示,返回數據全部用UTF-8編碼。
     */
    private String msg;
    /**
     *
     */
    private String openId;
    /**
     * 不知道什麼東西,文檔上沒寫,但是實際api返回裏有。
     */
    private String is_lost;
    /**
     * 省(直轄市)
     */
    private String province;
    /**
     * 市(直轄市區)
     */
    private String city;
    /**
     * 出生年月
     */
    private String year;
    /**
     * 用戶在QQ空間的暱稱。
     */
    private String nickname;

    private String constellation;
    /**
     * 大小爲30×30像素的QQ空間頭像URL。
     */
    private String figureurl;
    /**
     * 大小爲50×50像素的QQ空間頭像URL。
     */
    private String figureurl_1;
    /**
     * 大小爲100×100像素的QQ空間頭像URL。
     */
    private String figureurl_2;
    /**
     * 大小爲40×40像素的QQ頭像URL。
     */
    private String figureurl_qq_1;
    /**
     * 大小爲100×100像素的QQ頭像URL。需要注意,不是所有的用戶都擁有QQ的100×100的頭像,但40×40像素則是一定會有。
     */
    private String figureurl_qq_2;
    /**
     * 性別。 如果獲取不到則默認返回”男”
     */
    private String gender;
    /**
     * 標識用戶是否爲黃鑽用戶(0:不是;1:是)。
     */
    private String is_yellow_vip;
    /**
     * 標識用戶是否爲黃鑽用戶(0:不是;1:是)
     */
    private String vip;
    /**
     * 黃鑽等級
     */
    private String yellow_vip_level;
    /**
     * 黃鑽等級
     */
    private String level;
    /**
     * 標識是否爲年費黃鑽用戶(0:不是; 1:是)
     */
    private String is_yellow_year_vip;


    public String getRet() {
        return ret;
    }

    public void setRet(String ret) {
        this.ret = ret;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String getOpenId() {
        return openId;
    }

    public void setOpenId(String openId) {
        this.openId = openId;
    }

    public String getIs_lost() {
        return is_lost;
    }

    public void setIs_lost(String is_lost) {
        this.is_lost = is_lost;
    }

    public String getProvince() {
        return province;
    }

    public void setProvince(String province) {
        this.province = province;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getYear() {
        return year;
    }

    public void setYear(String year) {
        this.year = year;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getFigureurl() {
        return figureurl;
    }

    public void setFigureurl(String figureurl) {
        this.figureurl = figureurl;
    }

    public String getFigureurl_1() {
        return figureurl_1;
    }

    public void setFigureurl_1(String figureurl_1) {
        this.figureurl_1 = figureurl_1;
    }

    public String getFigureurl_2() {
        return figureurl_2;
    }

    public void setFigureurl_2(String figureurl_2) {
        this.figureurl_2 = figureurl_2;
    }

    public String getFigureurl_qq_1() {
        return figureurl_qq_1;
    }

    public void setFigureurl_qq_1(String figureurl_qq_1) {
        this.figureurl_qq_1 = figureurl_qq_1;
    }

    public String getFigureurl_qq_2() {
        return figureurl_qq_2;
    }

    public void setFigureurl_qq_2(String figureurl_qq_2) {
        this.figureurl_qq_2 = figureurl_qq_2;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getIs_yellow_vip() {
        return is_yellow_vip;
    }

    public void setIs_yellow_vip(String is_yellow_vip) {
        this.is_yellow_vip = is_yellow_vip;
    }

    public String getVip() {
        return vip;
    }

    public void setVip(String vip) {
        this.vip = vip;
    }

    public String getYellow_vip_level() {
        return yellow_vip_level;
    }

    public void setYellow_vip_level(String yellow_vip_level) {
        this.yellow_vip_level = yellow_vip_level;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }

    public String getIs_yellow_year_vip() {
        return is_yellow_year_vip;
    }

    public void setIs_yellow_year_vip(String is_yellow_year_vip) {
        this.is_yellow_year_vip = is_yellow_year_vip;
    }

    public String getConstellation() {
        return constellation;
    }

    public void setConstellation(String constellation) {
        this.constellation = constellation;
    }
}
QQ
public interface QQ {
    QQUserInfo getUserInfo();
}
QQImpl
package com.awbeci.ssb.core.social.qq.api.impl;

public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {

    private Logger logger = LoggerFactory.getLogger(getClass());

    //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);

        logger.info("【QQImpl】 QQ_URL_GET_OPENID={} result={}", QQ_URL_GET_OPENID, result);

        this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
    }

    public QQUserInfo getUserInfo() {
        String url = String.format(QQ_URL_GET_USER_INFO, appId, openId);
        String result = getRestTemplate().getForObject(url, String.class);

        logger.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);
            logger.info("userinfo={}", userInfo);
            return userInfo;
        } catch (Exception e) {
            throw new RuntimeException("獲取用戶信息失敗", e);
        }
    }
}
QQAutoConfig
@Configuration
public class QQAutoConfig extends SocialAutoConfigurerAdapter{

    @Value("${ssb.security.social.qq.app-id}")
    private String qqAppId;

    @Value("${ssb.security.social.qq.app-secret}")
    private String qqAppSecret;

    @Value("${ssb.security.social.qq.provider-id}")
    private String qqProviderId;

    @Override
    protected ConnectionFactory<?> createConnectionFactory() {
        return new QQConnectionFactory(qqProviderId, qqAppId, qqAppSecret);
    }
}
SocialAutoConfigurerAdapter
/**
 * spring social 1.1.6已經移除了該類,所以自己新建一下
 */
public abstract class SocialAutoConfigurerAdapter extends SocialConfigurerAdapter {

    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer configurer, Environment environment) {
        configurer.addConnectionFactory(createConnectionFactory());
    }

    protected abstract ConnectionFactory<?> createConnectionFactory();

}
QQAdapter
public class QQAdapter implements ApiAdapter<QQ> {
    public boolean test(QQ qq) {
        return true;
    }

    public void setConnectionValues(QQ qq, ConnectionValues connectionValues) {
        QQUserInfo userInfo = qq.getUserInfo();

        //openId 唯一標識
        connectionValues.setProviderUserId(userInfo.getOpenId());
        connectionValues.setDisplayName(userInfo.getNickname());
        connectionValues.setImageUrl(userInfo.getFigureurl_qq_1());
        connectionValues.setProfileUrl(null);
    }

    public UserProfile fetchUserProfile(QQ qq) {
        return null;
    }

    public void updateStatus(QQ qq, String s) {

    }
}
QQConnectionFactory
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
    public QQConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
    }
}
QQOAuth2Template
public class QQOAuth2Template extends OAuth2Template {

    private Logger logger = LoggerFactory.getLogger(getClass());

    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);
        logger.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);
    }

    @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;
    }

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

3、微信的實現

Wechat
public interface Wechat {
    WechatUserInfo getUserInfo(String openId);
}
WechatImpl
public class WechatImpl extends AbstractOAuth2ApiBinding implements Wechat {

    /**
     *
     */
    private ObjectMapper objectMapper = new ObjectMapper();
    /**
     * 獲取用戶信息的url
     */
    private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";

    /**
     * @param accessToken
     */
    public WechatImpl(String accessToken) {
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
    }

    /**
     * 默認註冊的StringHttpMessageConverter字符集爲ISO-8859-1,而微信返回的是UTF-8的,所以覆蓋了原來的方法。
     */
    protected List<HttpMessageConverter<?>> getMessageConverters() {
        List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
        messageConverters.remove(0);
        messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return messageConverters;
    }

    /**
     * 獲取微信用戶信息。
     */
    public WechatUserInfo getUserInfo(String openId) {
        String url = URL_GET_USER_INFO + openId;
        String response = getRestTemplate().getForObject(url, String.class);
        if(StringUtils.contains(response, "errcode")) {
            return null;
        }
        WechatUserInfo profile = null;
        try {
            profile = objectMapper.readValue(response, WechatUserInfo.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return profile;
    }

}
WechatUserInfo
public class WechatUserInfo {
    /**
     * 普通用戶的標識,對當前開發者帳號唯一
     */
    private String openid;
    /**
     * 普通用戶暱稱
     */
    private String nickname;
    /**
     * 語言
     */
    private String language;
    /**
     * 普通用戶性別,1爲男性,2爲女性
     */
    private String sex;
    /**
     * 普通用戶個人資料填寫的省份
     */
    private String province;
    /**
     * 普通用戶個人資料填寫的城市
     */
    private String city;
    /**
     * 國家,如中國爲CN
     */
    private String country;
    /**
     * 用戶頭像,最後一個數值代表正方形頭像大小(有0、46、64、96、132數值可選,0代表640*640正方形頭像),用戶沒有頭像時該項爲空
     */
    private String headimgurl;
    /**
     * 用戶特權信息,json數組,如微信沃卡用戶爲(chinaunicom)
     */
    private String[] privilege;
    /**
     * 用戶統一標識。針對一個微信開放平臺帳號下的應用,同一用戶的unionid是唯一的。
     */
    private String unionid;

    /**
     * @return the openid
     */
    public String getOpenid() {
        return openid;
    }
    /**
     * @param openid the openid to set
     */
    public void setOpenid(String openid) {
        this.openid = openid;
    }
    /**
     * @return the nickname
     */
    public String getNickname() {
        return nickname;
    }
    /**
     * @param nickname the nickname to set
     */
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    /**
     * @return the sex
     */
    public String getSex() {
        return sex;
    }
    /**
     * @param sex the sex to set
     */
    public void setSex(String sex) {
        this.sex = sex;
    }
    /**
     * @return the province
     */
    public String getProvince() {
        return province;
    }
    /**
     * @param province the province to set
     */
    public void setProvince(String province) {
        this.province = province;
    }
    /**
     * @return the city
     */
    public String getCity() {
        return city;
    }
    /**
     * @param city the city to set
     */
    public void setCity(String city) {
        this.city = city;
    }
    /**
     * @return the country
     */
    public String getCountry() {
        return country;
    }
    /**
     * @param country the country to set
     */
    public void setCountry(String country) {
        this.country = country;
    }
    /**
     * @return the headimgurl
     */
    public String getHeadimgurl() {
        return headimgurl;
    }
    /**
     * @param headimgurl the headimgurl to set
     */
    public void setHeadimgurl(String headimgurl) {
        this.headimgurl = headimgurl;
    }
    /**
     * @return the privilege
     */
    public String[] getPrivilege() {
        return privilege;
    }
    /**
     * @param privilege the privilege to set
     */
    public void setPrivilege(String[] privilege) {
        this.privilege = privilege;
    }
    /**
     * @return the unionid
     */
    public String getUnionid() {
        return unionid;
    }
    /**
     * @param unionid the unionid to set
     */
    public void setUnionid(String unionid) {
        this.unionid = unionid;
    }
    /**
     * @return the language
     */
    public String getLanguage() {
        return language;
    }
    /**
     * @param language the language to set
     */
    public void setLanguage(String language) {
        this.language = language;
    }

}
WechatAutoConfiguration
@Configuration
@ConditionalOnProperty(prefix = "ssb.security.social.wechat", name = "app-id")
@Order(2)
public class WechatAutoConfiguration extends SocialAutoConfigurerAdapter {

    @Value("${ssb.security.social.wechat.app-id}")
    private String appId;

    @Value("${ssb.security.social.wechat.app-secret}")
    private String appSecret;

    @Value("${ssb.security.social.wechat.provider-id}")
    private String providerId;

    /*
     * (non-Javadoc)
     *
     * @see
     * org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter
     * #createConnectionFactory()
     */
    @Override
    protected ConnectionFactory<?> createConnectionFactory() {
        return new WechatConnectionFactory(providerId, appId, appSecret);
    }
}
WechatAccessGrant
public class WechatAccessGrant extends AccessGrant {

    private String openId;

    public WechatAccessGrant() {
        super("");
    }

    public WechatAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
        super(accessToken, scope, refreshToken, expiresIn);
    }

    /**
     * @return the openId
     */
    public String getOpenId() {
        return openId;
    }

    /**
     * @param openId the openId to set
     */
    public void setOpenId(String openId) {
        this.openId = openId;
    }

}
WechatAdapter
public class WechatAdapter implements ApiAdapter<Wechat> {

    private String openId;

    public WechatAdapter() {}

    public WechatAdapter(String openId){
        this.openId = openId;
    }

    /**
     * @param api
     * @return
     */
    public boolean test(Wechat api) {
        return true;
    }

    /**
     * @param api
     * @param values
     */
    public void setConnectionValues(Wechat api, ConnectionValues values) {
        WechatUserInfo profile = api.getUserInfo(openId);
        values.setProviderUserId(profile.getOpenid());
        values.setDisplayName(profile.getNickname());
        values.setImageUrl(profile.getHeadimgurl());
    }

    /**
     * @param api
     * @return
     */
    public UserProfile fetchUserProfile(Wechat api) {
        return null;
    }

    /**
     * @param api
     * @param message
     */
    public void updateStatus(Wechat api, String message) {
        //do nothing
    }

}
WechatConnectionFactory
public class WechatConnectionFactory extends OAuth2ConnectionFactory<Wechat> {

    /**
     * @param appId
     * @param appSecret
     */
    public WechatConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new WechatServiceProvider(appId, appSecret), new WechatAdapter());
    }

    /**
     * 由於微信的openId是和accessToken一起返回的,所以在這裏直接根據accessToken設置providerUserId即可,不用像QQ那樣通過QQAdapter來獲取
     */
    @Override
    protected String extractProviderUserId(AccessGrant accessGrant) {
        if(accessGrant instanceof WechatAccessGrant) {
            return ((WechatAccessGrant)accessGrant).getOpenId();
        }
        return null;
    }

    /* (non-Javadoc)
     * @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant)
     */
    public Connection<Wechat> createConnection(AccessGrant accessGrant) {
        return new OAuth2Connection<Wechat>(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<Wechat> createConnection(ConnectionData data) {
        return new OAuth2Connection<Wechat>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
    }

    private ApiAdapter<Wechat> getApiAdapter(String providerUserId) {
        return new WechatAdapter(providerUserId);
    }

    private OAuth2ServiceProvider<Wechat> getOAuth2ServiceProvider() {
        return (OAuth2ServiceProvider<Wechat>) getServiceProvider();
    }


}
WechatOAuth2Template
public class WechatOAuth2Template 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";

    private Logger logger = LoggerFactory.getLogger(getClass());

    public WechatOAuth2Template(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);
        logger.info("----------- accessTokenRequestUrl = {} ------------",accessTokenRequestUrl);
        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);
    }

    private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {

        logger.info("獲取access_token, 請求URL: " + accessTokenRequestUrl.toString());

        String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);

        logger.info("獲取access_token, 響應內容: " + response);

        Map 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);
        }

        WechatAccessGrant accessToken = new WechatAccessGrant(
                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;
    }

}
WechatServiceProvider
public class WechatServiceProvider extends AbstractOAuth2ServiceProvider<Wechat> {

    /**
     * 微信獲取授權碼的url
     */
    private static final String URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
    /**
     * 微信獲取accessToken的url
     */
    private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";

    /**
     * @param appId
     * @param appSecret
     */
    public WechatServiceProvider(String appId, String appSecret) {
        super(new WechatOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
    }


    /* (non-Javadoc)
     * @see org.springframework.social.oauth2.AbstractOAuth2ServiceProvider#getApi(java.lang.String)
     */
    @Override
    public Wechat getApi(String accessToken) {
        return new WechatImpl(accessToken);
    }
}

4、QQ和微信相關配置

SsbSocialConfig
@Configuration
@EnableSocial
@Order(1)
public class SsbSocialConfig extends SocialConfigurerAdapter {

    @Value("${ssb.security.social.filter-processes-url}")
    private String filterProcessesUrl;

    @Value("${ssb.security.social.register-url}")
    private String registerUrl;

    @Autowired
    private DataSource dataSource;

    @Autowired(required = false)
    ConnectionSignUp connectionSignUp;

    // 後處理器
    @Autowired(required = false)
    SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;

    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {

        //建立jdbc的連接
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
                connectionFactoryLocator, Encryptors.noOpText());
        repository.setTablePrefix("xfind_");
        // 默認註冊用戶
//        if (connectionSignUp != null) {
//            repository.setConnectionSignUp(connectionSignUp);
//        }
        return repository;
    }

    /**
     * 自定義qq登錄路徑和註冊路徑
     *
     * @return
     */
    @Bean
    public SpringSocialConfigurer ssbSocialSecurityConfig() {
        SsbSpringSocialConfigurer configurer = new SsbSpringSocialConfigurer(filterProcessesUrl);
        //1、認證失敗跳轉註冊頁面
        // 跳轉到signUp controller,從session中獲取用戶信息並通過生成的uuid保存到redis裏面,然後跳轉bind頁面
        // 前端綁定後發送用戶信息到後臺bind controller,1)保存到自己系統用戶;2)保存一份userconnection表數據,Spring Social通過這裏面表數據進行判斷是否綁定
        configurer.signupUrl(registerUrl);
        //2、認證成功跳轉後處理器,跳轉帶token的成功頁面
        configurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
        return configurer;
    }

    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
        return new ProviderSignInUtils(connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator));
    }

    @Override
    public UserIdSource getUserIdSource() {
        return new AuthenticationNameUserIdSource();
    }
}
SsbSpringSocialConfigurer
public class SsbSpringSocialConfigurer extends SpringSocialConfigurer {

    private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;

    // 設置自定義url
    private String filterProcessesUrl;

    public SsbSpringSocialConfigurer(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }

    /**
     * 重寫qq登錄url
     *
     * @param object
     * @param <T>
     * @return
     */
    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(filterProcessesUrl);
        if (socialAuthenticationFilterPostProcessor != null) {
            socialAuthenticationFilterPostProcessor.process(filter);
        }
        return (T) filter;
    }

    public SocialAuthenticationFilterPostProcessor getSocialAuthenticationFilterPostProcessor() {
        return socialAuthenticationFilterPostProcessor;
    }

    public void setSocialAuthenticationFilterPostProcessor(SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor) {
        this.socialAuthenticationFilterPostProcessor = socialAuthenticationFilterPostProcessor;
    }
}
SocialAuthenticationFilterPostProcessor
public interface SocialAuthenticationFilterPostProcessor {
    void process(SocialAuthenticationFilter socialAuthenticationFilter);
}
SsbSocialAuthenticationFilterPostProcessor
@Component
public class SsbSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {

    @Autowired
    private AuthenticationSuccessHandler jsAuthenticationSuccessHandler;

    // 後處理器
    public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
        socialAuthenticationFilter.setAuthenticationSuccessHandler(jsAuthenticationSuccessHandler);
    }
}
SocialRedisHelper
/**
 * 將第三方用戶信息保存到redis裏面
 */
@Component
public class SocialRedisHelper {

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    private UsersConnectionRepository usersConnectionRepository;

    @Autowired
    private ConnectionFactoryLocator connectionFactoryLocator;

    public void saveConnectionData(String mkey, ConnectionData connectionData) {
        redisTemplate.opsForValue().set(getKey(mkey), connectionData, 10, TimeUnit.MINUTES);
    }

    public void saveStateUserId(String mkey, String userId) {
        redisTemplate.opsForValue().set(getKey(mkey), userId, 10, TimeUnit.MINUTES);
    }

    public String getStateUserId(String mkey) {
        String key = getKey(mkey);
        if (!redisTemplate.hasKey(key)) {
            throw new RuntimeException("無法找到緩存的第三方社交賬號信息");
        }
        return (String) redisTemplate.opsForValue().get(key);
    }


    public void doPostSignUp(String mkey,String userId){
        String key = getKey(mkey);
        if (!redisTemplate.hasKey(key)){
            throw new RuntimeException("無法找到緩存的第三方社交賬號信息");
        }
        ConnectionData connectionData = (ConnectionData) redisTemplate.opsForValue().get(key);
        Connection<?> connection = connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId())
                .createConnection(connectionData);
        usersConnectionRepository.createConnectionRepository(userId).addConnection(connection);
        redisTemplate.delete(key);
    }

    private String getKey(String mkey) {
        if (StringUtils.isEmpty(mkey)) {
            throw new RuntimeException("設置ID:mkey 不爲空");
        }
        return "awbeci:security:social.connect." + mkey;
    }
}
SsbAuthenticationSuccessHandler
@Component("ssbAuthenticationSuccessHandler")
public class SsbAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;
    /*
     * (non-Javadoc)
     *
     * @see org.springframework.security.web.authentication.
     * AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.
     * HttpServletRequest, javax.servlet.http.HttpServletResponse,
     * org.springframework.security.core.Authentication)
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        String name = authentication.getName();
//        String password = (String) authentication.getCredentials();
        if (header == null || !header.startsWith("Basic ")) {
            throw new UnapprovedClientAuthenticationException("請求頭中無client信息");
        }

        String[] tokens = extractAndDecodeHeader(header, request);
        assert tokens.length == 2;
        String clientId = tokens[0];
        String clientSecret = tokens[1];

        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

        if (clientDetails == null) {
            throw new UnapprovedClientAuthenticationException("clientId對應的配置信息不存在:" + clientId);
        } else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
            throw new UnapprovedClientAuthenticationException("clientSecret不匹配:" + clientId);
        }

        TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom");

        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

        OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(token));
    }

    private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {

        byte[] base64Token = header.substring(6).getBytes("UTF-8");
        byte[] decoded;
        try {
            decoded = Base64.decode(base64Token);
        } catch (IllegalArgumentException e) {
            throw new BadCredentialsException("Failed to decode basic authentication token");
        }
        String token = new String(decoded, "UTF-8");
        int delim = token.indexOf(":");
        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        }
        return new String[] { token.substring(0, delim), token.substring(delim + 1) };
    }
}
ApiUserDetailsService
@Component
public class ApiUserDetailsService implements UserDetailsService, SocialUserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;

    /*
     * (non-Javadoc)
     *
     * @see org.springframework.security.core.userdetails.UserDetailsService#
     * loadUserByUsername(java.lang.String)
     */
    // 這裏的username 可以是username、mobile、email
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("表單登錄用戶名:" + username);
        return buildUser(username);
    }

    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        logger.info("設計登錄用戶Id:" + userId);
        return buildUser(userId);
    }

    private SocialUser buildUser(String userId) {
        // 根據用戶名查找用戶信息
        //根據查找到的用戶信息判斷用戶是否被凍結
        String password = passwordEncoder.encode("123456");
        logger.info("數據庫密碼是:" + password);
        return new SocialUser(userId, password,
                true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_USER"));
    }
}
獲取社交賬號數據並保存到redis裏面待前端綁定時使用
    @GetMapping("/social/signUp")
    public void socialSignUp(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String uuid = UUID.randomUUID().toString();
        SocialUserInfo userInfo = new SocialUserInfo();
        Connection<?> connectionFromSession = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
        userInfo.setHeadImg(connectionFromSession.getImageUrl());
        userInfo.setNickname(connectionFromSession.getDisplayName());
        userInfo.setProviderId(connectionFromSession.getKey().getProviderId());
        userInfo.setProviderUserId(connectionFromSession.getKey().getProviderUserId());

        socialRedisHelper.saveConnectionData(uuid, connectionFromSession.createData());
        response.sendRedirect(bindUrl + "?mkey=" + uuid);
    }

5、資源服務的配置

@Autowired
private SpringSocialConfigurer awbeciSocialSecurityConfig;

@Override
public void configure(HttpSecurity http) throws Exception {
    http
            .apply(awbeciSocialSecurityConfig)
            .and()
            .authorizeRequests()                
            .anyRequest()
            .authenticated()
            .and()
            .csrf().disable();
}

6、application.properties

ssb.security.social.register-url=/social/signUp
ssb.security.social.filter-processes-url=/social-login
ssb.security.social.bind-url=https://website/social-bind/qq
ssb.security.social.callback-url=https://website/social-login
ssb.security.social.connect-url=https://website/social-connect

#QQ授權
ssb.security.social.qq.app-id=
ssb.security.social.qq.app-secret=

#provider-id是構造訪問qq授權地址,如:localhost/auth/qq,如果是微信,localhost/auth/wechat
#provider-id和login-url地址組合成的url應該是你qq互聯上面的網站回調域地址,如:/social-login/qq
ssb.security.social.qq.provider-id=qq
#https://www.xfindzp.com/social-bind/qq
ssb.security.social.qq.register-url=https://website/login/regist

#WeChat授權
ssb.security.social.wechat.app-id=
ssb.security.social.wechat.app-secret=
ssb.security.social.wechat.provider-id=wechat

7、測試

clipboard.png

clipboard.png

clipboard.png

clipboard.png

7、總結

1、其實還有一種情況就是有系統平臺賬戶,但是沒有跟社交賬號建立聯繫,這時候你又沒有登錄進行綁定,所以當你點擊授權的時候,後臺可以判斷該賬號是否已經存在,如果存在就綁定,不存在就註冊。
2、其實我感覺還有優化的空間,有時間我來優化一下代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章