Social Login, also known as Social Login, means that users of a website can use Tencent QQ, renren, kaixin001, sina weibo, sohu weibo, Tencent weibo, taobao, douban, MSN, Google and other Social media accounts to Login to the website.

preface

In the previous chapter, Spring Security source analysis three -Spring Social Social login process, we have implemented the use of Spring Social+Security QQ Social login. In this chapter, we will implement wechat social login. (The general process of wechat and QQ login is the same, but there are some details of the difference, we will simply implement the following)

The preparatory work

  1. Familiar with OAuth2.0 protocol standard, wechat login is an authorized login based on authorization_code mode in OAuth2.0;
  2. Wechat open platformApply for website application development, obtainappidandappsecret
  3. Familiar with the website application wechat login development guide
  4. Refer to the spring-Security source code to analyze the preparations for the spring-Social Social login process

To facilitate testing, bloggers rented AppID and appSecret from a bao for a month

appid wxfd6965ab1fc6adb2
appsecret 66bb4566de776ac699ec1dbed0cc3dd1

The directory structure

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/12/160e8a694ca55e2a~tplv-t2oaga2asx-image.image

reference

  1. apiDefine the public interface for the API binding
  2. configSome configuration information of wechat
  3. connectSome of the classes needed to establish a connection with the service provider.

Define the interface that returns user information

public interface Weixin {
    WeixinUserInfo getUserInfo(String openId);
}
Copy the code

Here we see that wechat has an openId parameter relative to QQ’s getUserInfo. This is because wechat’s OpenID is returned together with access_token in step 5 of OAuth2.0’s authentication process diagram in wechat documents. The Access_token class AccessGrant. Java does not have An OpenID. So we need to extend Spring Social’s token-fetching class (accessgrant.java) ourselves;

Handle access_token class returned by wechat (add OpenID)

@Data
public class WeixinAccessGrant extends AccessGrant{

    private String openId;

    public WeixinAccessGrant(a) {
        super("");
    }

    public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
        super(accessToken, scope, refreshToken, expiresIn); }}Copy the code

Realize the interface to return user information

public class WeiXinImpl extends AbstractOAuth2ApiBinding implements Weixin {

    /** * Get user information from the 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);
    }

    /** * Get user information **@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;
    }

    /** * replace the default ISO-8859-1 encoding * with UTF-8@return* /
    @Override
    protectedList<HttpMessageConverter<? >> getMessageConverters() { List<HttpMessageConverter<? >> messageConverters =super.getMessageConverters();
        messageConverters.remove(0);
        messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        returnmessageConverters; }}Copy the code

Compared with QQ’s access to user information, wechat’s implementation class lacks the request to obtain OpenID through access_token. Openid is obtained from the self-defined extension class WeixinAccessGrant.

WeixinOAuth2Template processes the token information returned by wechat

@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
       
         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, request URL:"+accessTokenRequestUrl.toString());

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

        log.info("Access access_token, response:"+response);

        Map<String, Object> result = null;
        try {
            result = new ObjectMapper().readValue(response, Map.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // Return null if error code is returned
        if(StringUtils.isNotBlank(MapUtils.getString(result, "errcode"))){
            String errcode = MapUtils.getString(result, "errcode");
            String errmsg = MapUtils.getString(result, "errmsg");
            throw new RuntimeException("Failed to obtain 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;
    }

    /** * build the request to get the authorization code. That is, the address that directs users to wechat. * /
    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);
    }

    / * * * WeChat return contentType is HTML/text, add the corresponding HttpMessageConverter to deal with. * /
    protected RestTemplate createRestTemplate(a) {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        returnrestTemplate; }}Copy the code

Compared with QQ token class, there are three more global variables and exchangeForAccess method is copied. This is because wechat is passing the parameters appID and secret instead of the standard client_id and client_secret when obtaining the access_token through code.

WeixinServiceProvider Connects to the service provider

public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {

    /** * wechat obtains the authorization code url */
    private static final String WEIXIN_URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
    /** * the url for wechat to obtain accessToken (wechat already returns openId when retrieving accessToken) */
    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 newWeiXinImpl(accessToken); }}Copy the code

WeixinConnectionFactory Factory class for the connection service provider

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

    /** * Since wechat openId is returned with accessToken, we can set providerUserId directly according to accessToken, instead of retrieving */ through QQAdapter like QQ
    @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.Ac cessGrant) */
    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.C onnectionData) */
    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(a) {
        return(OAuth2ServiceProvider<Weixin>) getServiceProvider(); }}Copy the code

WeixinAdapter ADAPTS the data model returned by the wechat API to Spring Social’s standard model

public class WeixinAdapter implements ApiAdapter<Weixin> {

    private String openId;

    public WeixinAdapter(a) {}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) {}}Copy the code

WeixinAuthConfig creates the factory and sets the data source

@Configuration
public class WeixinAuthConfig extends SocialAutoConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private ConnectionSignUp myConnectionSignUp;

    @Override
    protectedConnectionFactory<? > 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 request, bind wechat return connect/weixinConnected view * /connect/weixin DELETE request, unbind return connect/weixinConnect view *@return* /
    @Bean({"connect/weixinConnect"."connect/weixinConnected"})
    @ConditionalOnMissingBean(name = "weixinConnectedView")
    public View weixinConnectedView(a) {
        return newSocialConnectView(); }}Copy the code

Social login configuration class

Because social login by SocialAuthenticationFilter filter interception, if has been configured in the previous chapter, this chapter does not need to be configured.

The effect is as follows:

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/12/160e8a694d2ca6ed~tplv-t2oaga2asx-image.image

The code download

Download it from my Github, github.com/longfeizhen…