[Spring Security + OAuth2 + JWT introduction to actual combat] 13. Third party QQ login and source code analysis

Keywords: Programming Spring Database Lombok Apache

Source code analysis

Because spring security social is implemented based on social authentication filter, let's start with social authentication filter


When it comes to Filter, there must be a corresponding configuration class
SocialAuthenticationFilter is no exception. Its corresponding configuration class is SpringSocialConfigurer. To make the project run, let's prepare the configuration first:

SocialConfig

core project com.spring.security.social path

package com.spring.security.social;


import com.spring.security.properties.SecurityProperties;
import com.spring.security.social.qq.connet.QQConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.UserIdSource;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
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;
import org.springframework.social.security.AuthenticationNameUserIdSource;
import org.springframework.social.security.SpringSocialConfigurer;

import javax.sql.DataSource;

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        // Specify the prefix of the table. The suffix is fixed. It is in the location of the JdbcUsersConnectionRepository
        repository.setTablePrefix("sys_");
        return repository;
    }

    @Bean
    public SpringSocialConfigurer hkSocialSecurityConfig(){
        // Default configuration class for component assembly
        // Include filter SocialAuthenticationFilter added to security filter chain
        //Custom login path
        String filterProcessesUrl = "/auth";
        HkSpringSocialConfigurer configurer = new HkSpringSocialConfigurer(filterProcessesUrl);
        return configurer;
    }

    // Add a processor about ProviderId=qq. Note that clientId and clientSecret need to apply for qq Internet by themselves
    @Override
    public void addConnectionFactories(
            ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
        connectionFactoryConfigurer.addConnectionFactory(
                new QQConnectionFactory("qq", "clientId", "clientSecret"));
    }

    /**
     * You must create a UserIdSource, otherwise an error will be reported
     *
     * @return
     */
    @Override
    public UserIdSource getUserIdSource() {
        return new AuthenticationNameUserIdSource();
    }

}

HkSpringSocialConfigurer

Ditto directory creation

package com.spring.security.social;

import lombok.Data;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;

/**
 * Set login address
 */
@Data
public class HkSpringSocialConfigurer extends SpringSocialConfigurer {
    private String filterProcessesUrl;

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

    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(filterProcessesUrl);
        return (T) filter;
    }
}

Create the following data tables in the database:

create table sys_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);

Add authentication module

@Autowired
private SpringSocialConfigurer hkSocialSecurityConfig;

.and()
//Join social sign in
.apply(hkSocialSecurityConfig)

The above two configurations can make the program run.


Visit / auth/qq and enter social authentication filter

    private static final String DEFAULT_FILTER_PROCESSES_URL = "/auth";

    private String filterProcessesUrl = DEFAULT_FILTER_PROCESSES_URL;
    // Intercept requests that meet the requirements
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        //If the requested url is / auth/qq, then the extracted ProviderId is qq
        String providerId = getRequestedProviderId(request);
        if (providerId != null){
            Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
            // Check if there is a SocialAuthenticationService handling ProviderId=qq
            return authProviders.contains(providerId);
        }
        return false;
    }

    //Analyze the requested url and extract the ProviderId from the url.
    //If the requested url is / auth/qq, then the extracted ProviderId is qq
    private String getRequestedProviderId(HttpServletRequest request) {
        String uri = request.getRequestURI();
        int pathParamIndex = uri.indexOf(';');

        if (pathParamIndex > 0) {
            // strip everything after the first semi-colon
            uri = uri.substring(0, pathParamIndex);
        }

        // uri must start with context path
        uri = uri.substring(request.getContextPath().length());

        // remaining uri must start with filterProcessesUrl
        if (!uri.startsWith(filterProcessesUrl)) {
            return null;
        }
        uri = uri.substring(filterProcessesUrl.length());

        // expect /filterprocessesurl/provider, not /filterprocessesurlproviderr
        if (uri.startsWith("/")) {
            return uri.substring(1);
        } else {
            return null;
        }
    }
From the above code, we found that we need to configure a SocialAuthenticationService with ProviderId=qq. We found the implementation class of SocialAuthenticationServiceLocator It is found that there is a special method for adding social authentication service in the social authentication service registry
public void addAuthenticationService(SocialAuthenticationService<?> authenticationService) {
        addConnectionFactory(authenticationService.getConnectionFactory());
        authenticationServices.put(authenticationService.getConnectionFactory().getProviderId(), authenticationService);
    }
Through debug analysis, it is found that the following code in the SocialConfiguration class will create the SocialAuthenticationServiceRegistry and call addAuthenticationService
    @Bean
    public ConnectionFactoryLocator connectionFactoryLocator() {
        if (securityEnabled) {
            SecurityEnabledConnectionFactoryConfigurer cfConfig = new SecurityEnabledConnectionFactoryConfigurer();
            for (SocialConfigurer socialConfigurer : socialConfigurers) {
                socialConfigurer.addConnectionFactories(cfConfig, environment);
            }
            return cfConfig.getConnectionFactoryLocator();
        } else {
            DefaultConnectionFactoryConfigurer cfConfig = new DefaultConnectionFactoryConfigurer();
            for (SocialConfigurer socialConfigurer : socialConfigurers) {
                socialConfigurer.addConnectionFactories(cfConfig, environment);
            }
            return cfConfig.getConnectionFactoryLocator();
        }
    }

Finally, through the analysis of the above code, we can override the addconnectionfactors method of the SocialConfigurerAdapter, which is the SocialConfig class above:

    // Add a processor about ProviderId=qq. Note that clientId and clientSecret need to apply for qq Internet by themselves
    @Override
    public void addConnectionFactories(
            ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
        connectionFactoryConfigurer.addConnectionFactory(
                new QQConnectionFactory("qq", "clientId", "clientSecret"));
    }

If addconnectionfactors appears, spring cloud starter oauth2 cannot be replaced with the latest version

QQConnectionFactory

The path of core project com.spring.security.social.qq.connect creates the QQConnectionFactory class, which inherits oauthconnectionfactory

package com.spring.security.social.qq.connet;

import com.spring.security.social.qq.api.QQ;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;

public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {

    public QQConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
    }
}

OAuth2ConnectionFactory generic is an interface for reading user data. When an error is reported, it is not necessary to worry about it before making an interface

Continue to follow up the request of / auth/qq. When we add a ConnectionFactory for qq, the request will be executed backward to:

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (detectRejection(request)) {
            if (logger.isDebugEnabled()) {
                logger.debug("A rejection was detected. Failing authentication.");
            }
            throw new SocialAuthenticationException("Authentication failed because user rejected authorization.");
        }
        
        Authentication auth = null;
        Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
        //Get ProviderId=qq in / auth/qq
        String authProviderId = getRequestedProviderId(request);
        if (!authProviders.isEmpty() && authProviderId != null && authProviders.contains(authProviderId)) {
            // Get social authentication service according to ProviderId=qq
            SocialAuthenticationService<?> authService = authServiceLocator.getAuthenticationService(authProviderId);
            // Get Authentication through social Authentication service
            auth = attemptAuthService(authService, request, response);
            if (auth == null) {
                throw new AuthenticationServiceException("authentication failed");
            }
        }
        return auth;
    }
private Authentication attemptAuthService(final SocialAuthenticationService<?> authService, final HttpServletRequest request, HttpServletResponse response) 
            throws SocialAuthenticationRedirectException, AuthenticationException {
        // Get token through social authentication service
        final SocialAuthenticationToken token = authService.getAuthToken(request, response);
        if (token == null) return null;
        
        Assert.notNull(token.getConnection());
        // Get the Authentication in the SecurityContext. If it is not logged in, it is null
        Authentication auth = getAuthentication();
        if (auth == null || !auth.isAuthenticated()) {
            // Update the obtained third-party user information and return to Authentication
            return doAuthentication(authService, request, token);
        } else {
            // If it is not null, the third-party user information form in the database will be checked. If it does not exist, it will be added to the form
            addConnection(authService, request, token, auth);
            return null;
        }       
    }   

Follow the code into the final social authenticationtoken token = authservice.getauthtoken (request, response); that is, OAuth2AuthenticationService

public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
        // Get request parameter code
        String code = request.getParameter("code");
        // If you don't get the code, it means it's not a callback request. According to oauth2.0 protocol, you need to create a request to enter the third-party login page
        if (!StringUtils.hasText(code)) {
            //Prepare request parameters
            OAuth2Parameters params =  new OAuth2Parameters();
            params.setRedirectUri(buildReturnToUrl(request));
            setScope(request, params);
            params.add("state", generateState(connectionFactory, request));
            addCustomParameters(params);
            // Throw an exception and prepare to jump to the third party login page
            throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
        } else if (StringUtils.hasText(code)) {
            // If the code is not empty, it means the third-party callback request. Then you need to intercept the code value to request the accessToken
            try {
                // The callback url should be consistent with the previous callback url
                String returnToUrl = buildReturnToUrl(request);
                // The point is, here is to obtain the accessToken
                AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
                // Setting ConnectionValues through ApiAdapter
                Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
                return new SocialAuthenticationToken(connection, null);
            } catch (RestClientException e) {
                logger.debug("failed to exchange for access", e);
                return null;
            }
        } else {
            return null;
        }
    }
AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
After analyzing the above code, getConnectionFactory().getOAuthOperations() execution logic is to obtain OAuth2Operations in QQOAuth2ServiceProvider in QQConnectionFactory. In general, it is OAuth2Template, But because the response data format returned by QQ server is html, OAuth2Template cannot be processed. We need to customize OAuth2Template. QQ returns datagram: access_token = fe04 ************* cce2 & expires_in = 7776000 & refresh_token = 88e4
 

QQOAuth2Template

The path of core project com.spring.security.social.qq.connect creates the QQOAuth2Template class

package com.spring.security.social.qq.connet;

import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.Charset;

/**
 * QQ Custom Template resolution
 */
public class QQOAuth2Template extends OAuth2Template {

    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        // Set to true to place clientId and clientSecret in the request parameters
        setUseParametersForClientAuthentication(true);
    }

    /**
     * Custom send request and parse
     *
     * @param accessTokenUrl
     * @param parameters
     * @return
     */
    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
        String result = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
        System.out.println("Response data:" + result);
        return createAccessGrant(result);
    }


    /**
     * After the AccessGrant is created according to the returned structure and returned successfully, the Access Token can be obtained in the returned package. Such as:
     * access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
     *
     * @param responseResult
     * @return
     */
    private AccessGrant createAccessGrant(String responseResult) {
        String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseResult, "&");
        String access_token = StringUtils.substringAfterLast(items[0], "=");
        String expires_in = StringUtils.substringAfterLast(items[1], "=");
        String refresh_token = StringUtils.substringAfterLast(items[2], "=");
        return new AccessGrant(
                access_token, StringUtils.EMPTY, refresh_token, Long.valueOf(expires_in));
    }


    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }
}

QQServiceProvider

core project com.spring.security.social.qq.connect create QQServiceProvider

package com.spring.security.social.qq.connet;


import com.spring.security.social.qq.api.QQ;
import com.spring.security.social.qq.api.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;

public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
    private String appId;

    //Guide the user to the url of the authentication server to obtain the Authorization Code
    private static final String URL_AUTHORIZE="https://graph.qq.com/oauth2.0/authorize";

    //The url of the Authorization Code returned by the authentication server obtains the Access Token through the Authorization Code
    private static final String URL_ACCESSTOKEN="https://graph.qq.com/oauth2.0/token";

    public QQServiceProvider(String appId,String appSecret) {
        //String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl
        super(new QQOAuth2Template(appId,appSecret,URL_AUTHORIZE,URL_ACCESSTOKEN));
        this.appId = appId;
    }

    @Override
    public QQ getApi(String accessToken) {

        return new QQImpl(accessToken, appId);
    }
}

Set up Connection through AcessGrant later

// Setting ConnectionValues through ApiAdapter
Connection<S> connection = getConnectionFactory().createConnection(accessGrant);

That is to call the following setConnectionValues method

QQAdapter

core project com.spring.security.social.qq.connect create QQAdapter

package com.spring.security.social.qq.connet;

import com.spring.security.social.qq.api.QQ;
import com.spring.security.social.qq.api.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;

public class QQAdapter implements ApiAdapter<QQ> {

    /**
     * Test whether QQ server is available
     * @param qq
     * @return
     */
    @Override
    public boolean test(QQ qq) {
        return true;
    }

    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        QQUserInfo userInfo = api.getUserInfo();
        //User name
        values.setDisplayName(userInfo.getNickname());
        //User head
        values.setImageUrl(userInfo.getFigureurl_qq_1());
        //Personal homepage
        values.setProfileUrl(null);
        //Service provider user ID openid
        values.setProviderUserId(userInfo.getOpenId());
    }

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

    @Override
    public void updateStatus(QQ qq, String s) {

    }
}

After getting the token, go back to the doAuthentication operation in the SocialAuthenticationFilter.attemptAuthService method

private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
        try {
            if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
            token.setDetails(authenticationDetailsSource.buildDetails(request));
            // According to whether the current third-party user information exists in the database
            // If there is no exception, throw it and jump to the registration page
            Authentication success = getAuthenticationManager().authenticate(token);
            Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
            // If the user already exists, update the data
            updateConnections(authService, token, success);         
            return success;
        } catch (BadCredentialsException e) {
            // If the user needs to register, skip the registration page
            if (signupUrl != null) {
                // Here, the ConnectionData will be stored in the session, which can be obtained through ProviderSignInUtils
                sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
                throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
            }
            throw e;
        }
    }

Note here:

Authentication success = getAuthenticationManager().authenticate(token);

This code will jump to ProviderManager.authenticate first, and then enter SocialAuthenticationProvider.authenticate

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type");
        Assert.isTrue(!authentication.isAuthenticated(), "already authenticated");
        SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication;
        String providerId = authToken.getProviderId();
        Connection<?> connection = authToken.getConnection();
        // Query whether the current third-party data exists in the database, and throw exceptions if not
        // Query through JdbcUsersConnectionRepository
        String userId = toUserId(connection);
        if (userId == null) {
            throw new BadCredentialsException("Unknown access token");
        }

        UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
        if (userDetails == null) {
            throw new UsernameNotFoundException("Unknown connected account id");
        }

        return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
    }

Source code of JdbcUsersConnectionRepository

public List<String> findUserIdsWithConnection(Connection<?> connection) {
        ConnectionKey key = connection.getKey();
        List<String> localUserIds = jdbcTemplate.queryForList("select userId from " + tablePrefix + "UserConnection where providerId = ? and providerUserId = ?", String.class, key.getProviderId(), key.getProviderUserId());
        // According to the code logic, if connectionSignUp is set, a new userid will be created automatically, and exceptions will not be thrown and the registration page will not be entered
        if (localUserIds.size() == 0 && connectionSignUp != null) {
            String newUserId = connectionSignUp.execute(connection);
            if (newUserId != null)
            {
                createConnectionRepository(newUserId).addConnection(connection);
                return Arrays.asList(newUserId);
            }
        }
        return localUserIds;
    }

Here, according to the source code analysis, the third-party login is basically over. The remaining user data reading classes are


QQ

The core project om.spring.security.social.qq.api path creates a QQ interface

package com.spring.security.social.qq.api;


/**
 * Get user information
 */
public interface QQ {
    QQUserInfo getUserInfo();
}

QQImpl

Same as above

package com.spring.security.social.qq.api;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang.StringUtils;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;


public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
    //Application of ID
    private String appId;

    //User OpenID
    private String openId;

    //Get the URL of user openId
    private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";

    //Get user information url
    private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";

    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * Get openid
     * Response: callback ({"client" Id ":" your "appid", "openid": "your" opened "});
     *
     * @param accessToken
     * @param appId
     */
    public QQImpl(String accessToken, String appId) {
        super(accessToken, TokenStrategy.AUTHORIZATION_HEADER);
        this.appId = appId;

        // The reason why you need to obtain openId is that you need to obtain user information through openId
        this.openId = requestOpenId(accessToken);

    }

    /**
     * Get openId
     *
     * <p>Response: callback ({"client" Id ":" your "appid", "openid": "your" opened "});
     *
     * @param accessToken
     * @return
     */
    private String requestOpenId(String accessToken) {
        String url = String.format(URL_GET_OPENID, accessToken);
        String result = getRestTemplate().getForObject(url, String.class);
        System.out.println("Obtain openId: " + result);
        String first = StringUtils.substringBeforeLast(result, "\"");
        return StringUtils.substringAfterLast(first, "\"");
    }

    /**
     * Get user information
     *
     * <p>https://wiki.connect.qq.com/get_user_info
     *
     * @return
     */
    @Override
    public QQUserInfo getUserInfo() {
        //Send request
        String url = String.format(URL_GET_USERINFO, appId, openId);
        String result = getRestTemplate().getForObject(url, String.class);
        System.out.println("Access to user information:" + result);

        try {
            QQUserInfo qqUserInfo = objectMapper.readValue(result, QQUserInfo.class);
            qqUserInfo.setOpenId(openId);
            return qqUserInfo;
        } catch (Exception e) {
            throw new RuntimeException("Failed to get user information");
        }
    }
}

QQUserInfo

Same as above

Parameter reference: https://wiki.connect.qq.com/get_user_info

import lombok.Getter;
import lombok.Setter;

/** qq User information */
@Getter
@Setter
public class QQUserInfo {
    /** Return code */
    private String ret;
    /** If RET < 0, there will be a corresponding error message prompt, and all the returned data will be encoded with UTF-8. */
    private String msg;
    /** */
    private String openId;
    /** I don't know what. It's not written in the document, but it's in the actual api return. */
    private String is_lost;
    /** Province (municipality) */
    private String province;
    /** City (municipality directly under the central government) */
    private String city;
    /** Date of birth */
    private String year;
    /** User's nickname in QQ space. */
    private String nickname;
    /** QQ space image URL with size of 30 × 30 pixels. */
    private String figureurl;
    /** QQ space image URL with size of 50 × 50 pixels. */
    private String figureurl_1;
    /** QQ space image URL with size of 100 × 100 pixels. */
    private String figureurl_2;

    private String figureurl_type;
    private String figureurl_qq;
    /** QQ image URL with size of 40 × 40 pixels. */
    private String figureurl_qq_1;
    /** QQ image URL with size of 100 × 100 pixels. It should be noted that not all users have QQ's 100 × 100 head image, but 40 × 40 pixels are certain to have it. */
    private String figureurl_qq_2;
    /** Gender. If not, return to "male" by default */
    private String gender;
    /** Identify whether the user is a yellow diamond user (0: No; 1: Yes). */
    private String is_yellow_vip;
    /** Identify whether the user is a yellow diamond user (0: No; 1: Yes) */
    private String vip;
    /** Yellow drill grade */
    private String yellow_vip_level;
    /** Yellow drill grade */
    private String level;
    /** Identify whether it is an annual fee yellow diamond user (0: No; 1: Yes) */
    private String is_yellow_year_vip;

    private String constellation;
}

It should be noted that the configured callback address is consistent with the < a href = "/ auth / QQ" > QQ login < / a > request address

You can modify the local hosts file to test

Posted by raymie7 on Sun, 08 Mar 2020 05:20:00 -0700