【Spring Security + OAuth2 + JWT入门到实战】13. QQ登录加源码分析

别说谁变了你拦得住时间么 提交于 2020-03-08 19:20:01

源码分析 

因为spring security social是基于SocialAuthenticationFilter实现的,所以咱们从SocialAuthenticationFilter入手开始分析


一说到Filter,必定有一个对应的配置类
SocialAuthenticationFilter也不例外,它对应的配置类就是SpringSocialConfigurer,为了让项目运行起来,咱们先把配置准备好:

SocialConfig

core项目com.spring.security.social路径

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());
        // 指定表前缀,后缀是固定的,在JdbcUsersConnectionRepository所在位置
        repository.setTablePrefix("sys_");
        return repository;
    }

    @Bean
    public SpringSocialConfigurer hkSocialSecurityConfig(){
        // 默认配置类,进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        //自定义登录路径
        String filterProcessesUrl = "/auth";
        HkSpringSocialConfigurer configurer = new HkSpringSocialConfigurer(filterProcessesUrl);
        return configurer;
    }

    // 添加关于ProviderId=qq的处理器。注意clientId和clientSecret需要自己去qq互联申请
    @Override
    public void addConnectionFactories(
            ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
        connectionFactoryConfigurer.addConnectionFactory(
                new QQConnectionFactory("qq", "clientId", "clientSecret"));
    }

    /**
     * 必须要创建一个UserIdSource,否则会报错
     *
     * @return
     */
    @Override
    public UserIdSource getUserIdSource() {
        return new AuthenticationNameUserIdSource();
    }

}

HkSpringSocialConfigurer

同上目录创建

package com.spring.security.social;

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

/**
 * 设置 登录  注册地址
 */
@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 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);

加入认证模块

@Autowired
private SpringSocialConfigurer hkSocialSecurityConfig;

.and()
//加入社交登录
.apply(hkSocialSecurityConfig)

以上两个配置,就完全可以让程序运行起来了。


访问/auth/qq,进入SocialAuthenticationFilter中

    private static final String DEFAULT_FILTER_PROCESSES_URL = "/auth";

    private String filterProcessesUrl = DEFAULT_FILTER_PROCESSES_URL;
    // 拦截符合要求的请求
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        //假如请求的url是/auth/qq,那么ProviderId提取出来就是qq
        String providerId = getRequestedProviderId(request);
        if (providerId != null){
            Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
            // 检查是否存在处理ProviderId=qq的SocialAuthenticationService
            return authProviders.contains(providerId);
        }
        return false;
    }

    //分析请求的url,从url中提取ProviderId。
    //假如请求的url是/auth/qq,那么ProviderId提取出来就是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;
        }
    }
从上面代码中,我们发现需要配置一个ProviderId=qq的SocialAuthenticationService,我们找到SocialAuthenticationServiceLocator的实现类 SocialAuthenticationServiceRegistry,发现里面有一个专门用于添加SocialAuthenticationService的方法
public void addAuthenticationService(SocialAuthenticationService<?> authenticationService) {
        addConnectionFactory(authenticationService.getConnectionFactory());
        authenticationServices.put(authenticationService.getConnectionFactory().getProviderId(), authenticationService);
    }
通过debug分析,发现在SocialConfiguration类中的下面代码会创建SocialAuthenticationServiceRegistry并且调用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();
        }
    }

最终,通过对上面代码的分析,我们可以重写SocialConfigurerAdapter的addConnectionFactories方法,也就是咱们上面的SocialConfig类:

    // 添加关于ProviderId=qq的处理器。注意clientId和clientSecret需要自己去qq互联申请
    @Override
    public void addConnectionFactories(
            ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
        connectionFactoryConfigurer.addConnectionFactory(
                new QQConnectionFactory("qq", "clientId", "clientSecret"));
    }

如果出现addConnectionFactories无法重写把spring-cloud-starter-oauth2换成最新版本

QQConnectionFactory

core项目com.spring.security.social.qq.connet路径创建QQConnectionFactory类继承OAuth2ConnectionFactory

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泛型是读取用户资料的接口,报错先不用管最后再做接口

继续跟进/auth/qq这个请求,当我们添加了一个针对qq的ConnectionFactory后,请求将向后执行至:

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();
        //获取/auth/qq中的ProviderId=qq
        String authProviderId = getRequestedProviderId(request);
        if (!authProviders.isEmpty() && authProviderId != null && authProviders.contains(authProviderId)) {
            // 根据ProviderId=qq获取SocialAuthenticationService
            SocialAuthenticationService<?> authService = authServiceLocator.getAuthenticationService(authProviderId);
            // 通过SocialAuthenticationService获取Authentication
            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 {
        // 通过SocialAuthenticationService获取token
        final SocialAuthenticationToken token = authService.getAuthToken(request, response);
        if (token == null) return null;
        
        Assert.notNull(token.getConnection());
        // 获取SecurityContext中的Authentication,未登录的话就是null
        Authentication auth = getAuthentication();
        if (auth == null || !auth.isAuthenticated()) {
            // 将获取到的第三方用户信息做一个更新,并返回Authentication
            return doAuthentication(authService, request, token);
        } else {
            // 如果不是null,会检查数据库中的第三方用户信息表单,不存在就会添加进表单
            addConnection(authService, request, token, auth);
            return null;
        }       
    }   

跟着代码进final SocialAuthenticationToken token = authService.getAuthToken(request, response);也就是OAuth2AuthenticationService

public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
        // 获取请求参数code
        String code = request.getParameter("code");
        // 如果没有拿到code,说明不是回调请求,根据oauth2.0协议,就要创建一个请求进入第三方登录页面
        if (!StringUtils.hasText(code)) {
            //准备请求参数
            OAuth2Parameters params =  new OAuth2Parameters();
            params.setRedirectUri(buildReturnToUrl(request));
            setScope(request, params);
            params.add("state", generateState(connectionFactory, request));
            addCustomParameters(params);
            // 抛出异常,准备跳转至第三方登录页面
            throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
        } else if (StringUtils.hasText(code)) {
            // 如果code不为空,那么意味着就是第三方回调请求,那么就需要截取code值去请求accessToken
            try {
                // 回调url,要保持和之前的回调url一致
                String returnToUrl = buildReturnToUrl(request);
                // 重点在于这里,这里是获取accessToken
                AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
                // 通过ApiAdapter设置ConnectionValues
                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);
分析上面这段代码,getConnectionFactory().getOAuthOperations()执行逻辑是获取QQConnectionFactory中QQOAuth2ServiceProvider里面的OAuth2Operations,一般情况下就是OAuth2Template, 但是因为qq服务器返回的响应数据格式是html所以OAuth2Template处理不了,我们需要自定义OAuth2Template。QQ返回数据报:access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
 

QQOAuth2Template

core项目com.spring.security.social.qq.connet路径创建QQOAuth2Template类

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自定义Template  解析
 */
public class QQOAuth2Template extends OAuth2Template {

    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        // 设置成true才会将clientId和clientSecret放到请求参数中
        setUseParametersForClientAuthentication(true);
    }

    /**
     * 自定义发送请求并解析
     *
     * @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("响应数据:" + result);
        return createAccessGrant(result);
    }


    /**
     * 根据返回的结构创建AccessGrant 成功返回,即可在返回包中获取到Access Token。 如:
     * 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项目com.spring.security.social.qq.connet创建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;

    //将用户导向到认证服务器的url  获取Authorization Code
    private static final String URL_AUTHORIZE="https://graph.qq.com/oauth2.0/authorize";

    //认证服务器返回授权码的url  通过Authorization Code获取Access Token
    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);
    }
}

后续通过AcessGrant设置Connection

// 通过ApiAdapter设置ConnectionValues
Connection<S> connection = getConnectionFactory().createConnection(accessGrant);

也就是调用下面的setConnectionValues方法

QQAdapter

core项目com.spring.security.social.qq.connet创建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> {

    /**
     * 测试QQ服务端是否可用
     * @param qq
     * @return
     */
    @Override
    public boolean test(QQ qq) {
        return true;
    }

    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        QQUserInfo userInfo = api.getUserInfo();
        //用户名称
        values.setDisplayName(userInfo.getNickname());
        //用户头像
        values.setImageUrl(userInfo.getFigureurl_qq_1());
        //个人主页
        values.setProfileUrl(null);
        //服务商用户ID   openId
        values.setProviderUserId(userInfo.getOpenId());
    }

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

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

    }
}

在拿到token后,回到SocialAuthenticationFilter.attemptAuthService方法中的doAuthentication操作

private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
        try {
            if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
            token.setDetails(authenticationDetailsSource.buildDetails(request));
            // 根据数据库中是否存在当前第三方用户信息
            // 不存在就抛异常并跳转进注册页面
            Authentication success = getAuthenticationManager().authenticate(token);
            Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
            // 假如已经存在这个用户,就更新数据
            updateConnections(authService, token, success);         
            return success;
        } catch (BadCredentialsException e) {
            // 如果是需要注册的用户,就跳注册页面
            if (signupUrl != null) {
                // 这里会把ConnectionData存储进session,可以通过ProviderSignInUtils获取
                sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
                throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
            }
            throw e;
        }
    }

注意这里:

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

这段代码会先跳转至ProviderManager.authenticate,再进入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();
        // 查询数据库是否存在当前第三方数据,不存在就抛异常
        // 通过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));
    }

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());
        // 根据代码逻辑,发现假如设置了connectionSignUp,那么就会自动创建一个newUserId,就不会抛异常,也就不会进入注册页面
        if (localUserIds.size() == 0 && connectionSignUp != null) {
            String newUserId = connectionSignUp.execute(connection);
            if (newUserId != null)
            {
                createConnectionRepository(newUserId).addConnection(connection);
                return Arrays.asList(newUserId);
            }
        }
        return localUserIds;
    }

到这里,根据源码分析第三方登录基本告一段落,剩下的几个读取用户资料类分别是


QQ

core项目om.spring.security.social.qq.api路径创建QQ接口

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


/**
 * 获取用户信息
 */
public interface QQ {
    QQUserInfo getUserInfo();
}

QQImpl

同上面目录

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 {
    //应用ID
    private String appId;

    //用户OpenID
    private String openId;

    //获取用户openId的URL
    private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";

    //获取用户信息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();

    /**
     * 获取openid
     * 响应: callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
     *
     * @param accessToken
     * @param appId
     */
    public QQImpl(String accessToken, String appId) {
        super(accessToken, TokenStrategy.AUTHORIZATION_HEADER);
        this.appId = appId;

        // 之所以需要获取openId,是因为需要通过openId获取用户信息
        this.openId = requestOpenId(accessToken);

    }

    /**
     * 获取openId
     *
     * <p>响应: callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
     *
     * @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("获取openId:" + result);
        String first = StringUtils.substringBeforeLast(result, "\"");
        return StringUtils.substringAfterLast(first, "\"");
    }

    /**
     * 获取用户信息
     *
     * <p>https://wiki.connect.qq.com/get_user_info
     *
     * @return
     */
    @Override
    public QQUserInfo getUserInfo() {
        //发送请求
        String url = String.format(URL_GET_USERINFO, appId, openId);
        String result = getRestTemplate().getForObject(url, String.class);
        System.out.println("获取用户资料:" + result);

        try {
            QQUserInfo qqUserInfo = objectMapper.readValue(result, QQUserInfo.class);
            qqUserInfo.setOpenId(openId);
            return qqUserInfo;
        } catch (Exception e) {
            throw new RuntimeException("获取用户信息失败");
        }
    }
}

QQUserInfo

同上面目录

参数参考:https://wiki.connect.qq.com/get_user_info

import lombok.Getter;
import lombok.Setter;

/** qq用户信息 */
@Getter
@Setter
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;
    /** 大小为30×30像素的QQ空间头像URL。 */
    private String figureurl;
    /** 大小为50×50像素的QQ空间头像URL。 */
    private String figureurl_1;
    /** 大小为100×100像素的QQ空间头像URL。 */
    private String figureurl_2;

    private String figureurl_type;
    private String figureurl_qq;
    /** 大小为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;

    private String constellation;
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!