使用SpringSocial开发QQ登录

⒈编写QQ用户对应的数据结构

 package cn.coreqi.social.qq.entities;

 /**
* 封装QQ的用户信息
*/
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;
/**
* 大小为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;
}
}

⒉编写一个QQ API接口用于获取QQ用户信息

 package cn.coreqi.social.qq.api;

 import cn.coreqi.social.qq.entities.QQUserInfo;

 public interface QQ {
/**
* 返回QQ中的用户信息
* @return
*/
QQUserInfo getUserInfo();
}

⒊编写一个QQ API接口实现

 package cn.coreqi.social.qq.api.impl;

 import cn.coreqi.social.qq.api.QQ;
import cn.coreqi.social.qq.entities.QQUserInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang.StringUtils;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy; import java.io.IOException; /**
* 获取用户信息
* 不能声明为单例,因为每个用户的验证是不同的
*/
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ { private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s"; //获取openid的请求地址
private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s"; //获取用户信息的请求地址 private String appid; //申请QQ登录成功后,分配给应用的appid
private String openid; //用户的ID,与QQ号码一一对应。 private ObjectMapper objectMapper = new ObjectMapper(); //用于序列化Json数据 public QQImpl(String accessToken,String appid){
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER); //将token作为查询参数
this.appid = appid; String url = String.format(URL_GET_OPENID,accessToken); //拼接成最终的openid的请求地址
String result = getRestTemplate().getForObject(url,String.class); System.out.println(result); this.openid = StringUtils.substringBetween(result,"\"openid\":\"","\"}"); } @Override
public QQUserInfo getUserInfo() {
String url = String.format(URL_GET_USERINFO,appid,openid); ////拼接成最终的获取用户信息的请求地址
String result = getRestTemplate().getForObject(url,String.class);
System.out.println(result);
QQUserInfo userInfo = null;
try {
userInfo = objectMapper.readValue(result,QQUserInfo.class);
userInfo.setOpenId(openid);
return userInfo;
} catch (Exception e) {
throw new RuntimeException("获取用户信息失败",e);
}
}
}

⒋编写QQ OAuth2认证流程模板类。

 package cn.coreqi.social.qq.connect;

 import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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; public class QQOAuth2Template extends OAuth2Template { private Logger logger = LoggerFactory.getLogger(getClass()); public 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("获取accessToke的响应:"+responseStr); String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&"); 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;
}
}

⒌编写QQ的OAuth2流程处理器的提供器

 package cn.coreqi.social.qq.connect;

 import cn.coreqi.social.qq.api.QQ;
import cn.coreqi.social.qq.api.impl.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider; /**
* 泛型是API接口的类型
*/
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> { private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize"; //获取授权码地址
private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token"; //获取用户令牌地址 private String appId; public QQServiceProvider(String appId,String appSecret) {
super(new QQOAuth2Template(appId,appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));
this.appId = appId;
} @Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken,appId);
}
}

⒍编写QQ API适配器,将从QQ API拿到的用户数据模型转换为Spring Social的标准用户数据模型。

 package cn.coreqi.social.qq.connect;

 import cn.coreqi.social.qq.api.QQ;
import cn.coreqi.social.qq.entities.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile; import java.io.IOException; /**
* 泛型是指当前API适配器适配API的类型是什么
*/
public class QQAdapter implements ApiAdapter<QQ> { /**
* 用来测试当前的API是否可用
* @param qq
* @return
*/
@Override
public boolean test(QQ qq) {
return true;
} /**
* 将服务提供商个性化的用户信息映射到ConnectionValues标准的数据化结构上
* @param qq
* @param connectionValues
*/
@Override
public void setConnectionValues(QQ qq, ConnectionValues connectionValues) {
QQUserInfo userInfo = qq.getUserInfo();
connectionValues.setDisplayName(userInfo.getNickname()); //显示的用户名称
connectionValues.setImageUrl(userInfo.getFigureurl_qq_1()); //用户的头像
connectionValues.setProfileUrl(null); //个人主页
connectionValues.setProviderUserId(userInfo.getOpenId()); //QQ的唯一标识
} /**
* 和上面的方法类似
* @param qq
* @return
*/
@Override
public UserProfile fetchUserProfile(QQ qq) {
return null;
} /**
*
* @param qq
* @param s
*/
@Override
public void updateStatus(QQ qq, String s) { }
}

⒎创建QQ连接工厂

 package cn.coreqi.social.qq.connect;

 import cn.coreqi.social.qq.api.QQ;
import org.springframework.social.connect.support.OAuth2ConnectionFactory; public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> { /**
*
* @param providerId 我们给服务提供商的唯一标识
* @param appId 服务提供商给的AppId
* @param appSecret 服务提供商给的App密码
*/
public QQConnectionFactory(String providerId,String appId,String appSecret) {
super(providerId, new QQServiceProvider(appId,appSecret), new QQAdapter());
}
}

⒏创建UserConnection数据表

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

⒐为用户服务类实现SocialUserDetailsService ,用于从数据库中通过QQ Id 拿到业务系统用户

 /**
*
*/
package cn.coreqi.security; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.social.security.SocialUser;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component; /**
* @author fanqi
*
*/
@Component
public class MyUserDetailsService 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)
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("表单登录用户名:" + username);
return buildUser(username);
} @Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
logger.info("设计登录用户Id:" + userId);
return buildUser(userId);
} private SocialUserDetails buildUser(String userId) {
// 根据用户名查找用户信息
//根据查找到的用户信息判断用户是否被冻结
String password = passwordEncoder.encode("123456");
logger.info("数据库密码是:"+password);
return new SocialUser(userId, password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
} }

⒑创建QQ登陆配置类

 package cn.coreqi.social.qq.connect;

 import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.connect.ConnectionFactory; /**
* QQ登录配置
*/
@Configuration
public class QQAutoConfig extends SocialAutoConfigurerAdapter {
@Override
protected ConnectionFactory<?> createConnectionFactory() {
String providerId = "qq"; //第三方id,用来决定发起第三方登录的url,默认是weixin
String appId = "";
String appSecret = "";
return new QQConnectionFactory(providerId, appId, appSecret);
}
}

⒒自定义我们自己的SpringSocial配置

 package cn.coreqi.social.config;

 import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer; public class CoreqiSpringSocialConfig extends SpringSocialConfigurer { /**
*
* @param object
* @param <T>
* @return
*/
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter)super.postProcess(object);
filter.setFilterProcessesUrl("/coreqi/auth");
return (T) filter;
}
}
SpringSocialConfigurer 会在 configure方法中声明一个 SocialAuthenticationFilter,我们可以继承SpringSocialConfigurer达到自定义我们的SpringSocial配置需求。

⒓声明一个SpringSocial的配置类
 package cn.coreqi.social.config;

 import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.encrypt.Encryptors;
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.ConnectionSignUp;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.social.security.SpringSocialConfigurer; import javax.sql.DataSource; @Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter { @Autowired
private DataSource dataSource; @Autowired(required = false)
private ConnectionSignUp connectionSignUp; /**
*
* @param connectionFactoryLocator 作用是去根据条件去查找应该用那个connectionFactory,因为系统中可能有很多的connectionFactory。
* @return
*/
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
//第三个参数的作用是把插入到数据库的数据进行加解密
JdbcUsersConnectionRepository jdbcUsersConnectionRepository = new JdbcUsersConnectionRepository(dataSource,connectionFactoryLocator, Encryptors.noOpText());
//jdbcUsersConnectionRepository.setTablePrefix(); //设置数据表的前缀
if(connectionSignUp != null){
jdbcUsersConnectionRepository.setConnectionSignUp(connectionSignUp);
}
return jdbcUsersConnectionRepository;
} /**
* 声明后还需要加在SpringSecurity过滤器链上
* @return
*/
@Bean
public SpringSocialConfigurer coreqiSocialSecurityConfig(){
CoreqiSpringSocialConfig config = new CoreqiSpringSocialConfig();
config.signupUrl("/registry"); //当从业务系统中无法找到OAuth快捷登陆的用户,那么将用户引导到注册页面中
return config;
} //1.注册过程中如何拿到SpringSocial信息
//2.注册完成后如何把业务系统的用户ID传给SpringSocial
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator));
}
}
⒔应用我们的过滤器配置
 package cn.coreqi.config;

 import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.social.security.SpringSocialConfigurer; public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SpringSocialConfigurer coreqiSocialSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.apply(coreqiSocialSecurityConfig);
}
}

 package cn.coreqi.social.qq.connect;

 import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component; /**
* 当没有从数据库中查找到第三方登录的用户,那么将执行ConnectionSignUp的execute方法生成新的用户id并存储到数据库中
*/
@Component
public class CoreqiConnectionSignUp implements ConnectionSignUp {
@Override
public String execute(Connection<?> connection) {
return connection.getDisplayName();
}
}
上一篇:阿里云被挖矿进程wnTKYg入侵的解决方法


下一篇:ListView嵌套GridView