⒈编写QQ用户对应的数据结构
1 package cn.coreqi.social.qq.entities; 2 3 /** 4 * 封装QQ的用户信息 5 */ 6 public class QQUserInfo { 7 8 /** 9 * 返回码 10 */ 11 private String ret; 12 /** 13 * 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。 14 */ 15 private String msg; 16 /** 17 * 18 */ 19 private String openId; 20 /** 21 * 不知道什么东西,文档上没写,但是实际api返回里有。 22 */ 23 private String is_lost; 24 /** 25 * 省(直辖市) 26 */ 27 private String province; 28 /** 29 * 市(直辖市区) 30 */ 31 private String city; 32 /** 33 * 出生年月 34 */ 35 private String year; 36 /** 37 * 用户在QQ空间的昵称。 38 */ 39 private String nickname; 40 /** 41 * 大小为30×30像素的QQ空间头像URL。 42 */ 43 private String figureurl; 44 /** 45 * 大小为50×50像素的QQ空间头像URL。 46 */ 47 private String figureurl_1; 48 /** 49 * 大小为100×100像素的QQ空间头像URL。 50 */ 51 private String figureurl_2; 52 /** 53 * 大小为40×40像素的QQ头像URL。 54 */ 55 private String figureurl_qq_1; 56 /** 57 * 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。 58 */ 59 private String figureurl_qq_2; 60 /** 61 * 性别。 如果获取不到则默认返回”男” 62 */ 63 private String gender; 64 /** 65 * 标识用户是否为黄钻用户(0:不是;1:是)。 66 */ 67 private String is_yellow_vip; 68 /** 69 * 标识用户是否为黄钻用户(0:不是;1:是) 70 */ 71 private String vip; 72 /** 73 * 黄钻等级 74 */ 75 private String yellow_vip_level; 76 /** 77 * 黄钻等级 78 */ 79 private String level; 80 /** 81 * 标识是否为年费黄钻用户(0:不是; 1:是) 82 */ 83 private String is_yellow_year_vip; 84 85 86 public String getRet() { 87 return ret; 88 } 89 90 public void setRet(String ret) { 91 this.ret = ret; 92 } 93 94 public String getMsg() { 95 return msg; 96 } 97 98 public void setMsg(String msg) { 99 this.msg = msg;100 }101 102 public String getOpenId() {103 return openId;104 }105 106 public void setOpenId(String openId) {107 this.openId = openId;108 }109 110 public String getIs_lost() {111 return is_lost;112 }113 114 public void setIs_lost(String is_lost) {115 this.is_lost = is_lost;116 }117 118 public String getProvince() {119 return province;120 }121 122 public void setProvince(String province) {123 this.province = province;124 }125 126 public String getCity() {127 return city;128 }129 130 public void setCity(String city) {131 this.city = city;132 }133 134 public String getYear() {135 return year;136 }137 138 public void setYear(String year) {139 this.year = year;140 }141 142 public String getNickname() {143 return nickname;144 }145 146 public void setNickname(String nickname) {147 this.nickname = nickname;148 }149 150 public String getFigureurl() {151 return figureurl;152 }153 154 public void setFigureurl(String figureurl) {155 this.figureurl = figureurl;156 }157 158 public String getFigureurl_1() {159 return figureurl_1;160 }161 162 public void setFigureurl_1(String figureurl_1) {163 this.figureurl_1 = figureurl_1;164 }165 166 public String getFigureurl_2() {167 return figureurl_2;168 }169 170 public void setFigureurl_2(String figureurl_2) {171 this.figureurl_2 = figureurl_2;172 }173 174 public String getFigureurl_qq_1() {175 return figureurl_qq_1;176 }177 178 public void setFigureurl_qq_1(String figureurl_qq_1) {179 this.figureurl_qq_1 = figureurl_qq_1;180 }181 182 public String getFigureurl_qq_2() {183 return figureurl_qq_2;184 }185 186 public void setFigureurl_qq_2(String figureurl_qq_2) {187 this.figureurl_qq_2 = figureurl_qq_2;188 }189 190 public String getGender() {191 return gender;192 }193 194 public void setGender(String gender) {195 this.gender = gender;196 }197 198 public String getIs_yellow_vip() {199 return is_yellow_vip;200 }201 202 public void setIs_yellow_vip(String is_yellow_vip) {203 this.is_yellow_vip = is_yellow_vip;204 }205 206 public String getVip() {207 return vip;208 }209 210 public void setVip(String vip) {211 this.vip = vip;212 }213 214 public String getYellow_vip_level() {215 return yellow_vip_level;216 }217 218 public void setYellow_vip_level(String yellow_vip_level) {219 this.yellow_vip_level = yellow_vip_level;220 }221 222 public String getLevel() {223 return level;224 }225 226 public void setLevel(String level) {227 this.level = level;228 }229 230 public String getIs_yellow_year_vip() {231 return is_yellow_year_vip;232 }233 234 public void setIs_yellow_year_vip(String is_yellow_year_vip) {235 this.is_yellow_year_vip = is_yellow_year_vip;236 }237 }
⒉编写一个QQ API接口用于获取QQ用户信息
1 package cn.coreqi.social.qq.api; 2 3 import cn.coreqi.social.qq.entities.QQUserInfo; 4 5 public interface QQ { 6 /** 7 * 返回QQ中的用户信息 8 * @return 9 */10 QQUserInfo getUserInfo();11 }
⒊编写一个QQ API接口实现
1 package cn.coreqi.social.qq.api.impl; 2 3 import cn.coreqi.social.qq.api.QQ; 4 import cn.coreqi.social.qq.entities.QQUserInfo; 5 import com.fasterxml.jackson.databind.ObjectMapper; 6 import org.apache.commons.lang.StringUtils; 7 import org.springframework.social.oauth2.AbstractOAuth2ApiBinding; 8 import org.springframework.social.oauth2.TokenStrategy; 9 10 import java.io.IOException;11 12 /**13 * 获取用户信息14 * 不能声明为单例,因为每个用户的验证是不同的15 */16 public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {17 18 private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s"; //获取openid的请求地址19 private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s"; //获取用户信息的请求地址20 21 private String appid; //申请QQ登录成功后,分配给应用的appid22 private String openid; //用户的ID,与QQ号码一一对应。23 24 private ObjectMapper objectMapper = new ObjectMapper(); //用于序列化Json数据25 26 public QQImpl(String accessToken,String appid){27 super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER); //将token作为查询参数28 this.appid = appid;29 30 String url = String.format(URL_GET_OPENID,accessToken); //拼接成最终的openid的请求地址31 String result = getRestTemplate().getForObject(url,String.class);32 33 System.out.println(result);34 35 this.openid = StringUtils.substringBetween(result,"\"openid\":\"","\"}");36 37 }38 39 @Override40 public QQUserInfo getUserInfo() {41 String url = String.format(URL_GET_USERINFO,appid,openid); 拼接成最终的获取用户信息的请求地址42 String result = getRestTemplate().getForObject(url,String.class);43 System.out.println(result);44 QQUserInfo userInfo = null;45 try {46 userInfo = objectMapper.readValue(result,QQUserInfo.class);47 userInfo.setOpenId(openid);48 return userInfo;49 } catch (Exception e) {50 throw new RuntimeException("获取用户信息失败",e);51 }52 }53 }
⒋编写QQ OAuth2认证流程模板类。
1 package cn.coreqi.social.qq.connect; 2 3 import org.apache.commons.lang.StringUtils; 4 import org.slf4j.Logger; 5 import org.slf4j.LoggerFactory; 6 import org.springframework.http.converter.StringHttpMessageConverter; 7 import org.springframework.social.oauth2.AccessGrant; 8 import org.springframework.social.oauth2.OAuth2Template; 9 import org.springframework.util.MultiValueMap;10 import org.springframework.web.client.RestTemplate;11 import java.nio.charset.Charset;12 13 public class QQOAuth2Template extends OAuth2Template {14 15 private Logger logger = LoggerFactory.getLogger(getClass());16 17 public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {18 super(clientId, clientSecret, authorizeUrl, accessTokenUrl);19 setUseParametersForClientAuthentication(true);20 }21 22 @Override23 protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMapparameters) {24 String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);25 26 logger.info("获取accessToke的响应:"+responseStr);27 28 String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");29 30 String accessToken = StringUtils.substringAfterLast(items[0], "=");31 Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));32 String refreshToken = StringUtils.substringAfterLast(items[2], "=");33 34 return new AccessGrant(accessToken, null, refreshToken, expiresIn);35 }36 37 @Override38 protected RestTemplate createRestTemplate() {39 RestTemplate restTemplate = super.createRestTemplate();40 restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));41 return restTemplate;42 }43 }
⒌编写QQ的OAuth2流程处理器的提供器
1 package cn.coreqi.social.qq.connect; 2 3 import cn.coreqi.social.qq.api.QQ; 4 import cn.coreqi.social.qq.api.impl.QQImpl; 5 import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider; 6 7 /** 8 * 泛型是API接口的类型 9 */10 public class QQServiceProvider extends AbstractOAuth2ServiceProvider{11 12 private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize"; //获取授权码地址13 private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token"; //获取用户令牌地址14 15 private String appId;16 17 18 public QQServiceProvider(String appId,String appSecret) {19 super(new QQOAuth2Template(appId,appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));20 this.appId = appId;21 }22 23 @Override24 public QQ getApi(String accessToken) {25 return new QQImpl(accessToken,appId);26 }27 }
⒍编写QQ API适配器,将从QQ API拿到的用户数据模型转换为Spring Social的标准用户数据模型。
1 package cn.coreqi.social.qq.connect; 2 3 import cn.coreqi.social.qq.api.QQ; 4 import cn.coreqi.social.qq.entities.QQUserInfo; 5 import org.springframework.social.connect.ApiAdapter; 6 import org.springframework.social.connect.ConnectionValues; 7 import org.springframework.social.connect.UserProfile; 8 9 import java.io.IOException;10 11 /**12 * 泛型是指当前API适配器适配API的类型是什么13 */14 public class QQAdapter implements ApiAdapter{15 16 /**17 * 用来测试当前的API是否可用18 * @param qq19 * @return20 */21 @Override22 public boolean test(QQ qq) {23 return true;24 }25 26 /**27 * 将服务提供商个性化的用户信息映射到ConnectionValues标准的数据化结构上28 * @param qq29 * @param connectionValues30 */31 @Override32 public void setConnectionValues(QQ qq, ConnectionValues connectionValues) {33 QQUserInfo userInfo = qq.getUserInfo();34 connectionValues.setDisplayName(userInfo.getNickname()); //显示的用户名称35 connectionValues.setImageUrl(userInfo.getFigureurl_qq_1()); //用户的头像36 connectionValues.setProfileUrl(null); //个人主页37 connectionValues.setProviderUserId(userInfo.getOpenId()); //QQ的唯一标识38 }39 40 /**41 * 和上面的方法类似42 * @param qq43 * @return44 */45 @Override46 public UserProfile fetchUserProfile(QQ qq) {47 return null;48 }49 50 /**51 *52 * @param qq53 * @param s54 */55 @Override56 public void updateStatus(QQ qq, String s) {57 58 }59 }
⒎创建QQ连接工厂
1 package cn.coreqi.social.qq.connect; 2 3 import cn.coreqi.social.qq.api.QQ; 4 import org.springframework.social.connect.support.OAuth2ConnectionFactory; 5 6 public class QQConnectionFactory extends OAuth2ConnectionFactory{ 7 8 /** 9 *10 * @param providerId 我们给服务提供商的唯一标识11 * @param appId 服务提供商给的AppId12 * @param appSecret 服务提供商给的App密码13 */14 public QQConnectionFactory(String providerId,String appId,String appSecret) {15 super(providerId, new QQServiceProvider(appId,appSecret), new QQAdapter());16 }17 }
⒏创建UserConnection数据表
1 create table UserConnection (userId varchar(255) not null, 2 providerId varchar(255) not null, 3 providerUserId varchar(255), 4 `rank` int not null, 5 displayName varchar(255), 6 profileUrl varchar(512), 7 imageUrl varchar(512), 8 accessToken varchar(512) not null, 9 secret varchar(512),10 refreshToken varchar(512),11 expireTime bigint,12 primary key (userId, providerId, providerUserId));13 create unique index UserConnectionRank on UserConnection(userId, providerId, `rank`);
⒐为用户服务类实现SocialUserDetailsService ,用于从数据库中通过QQ Id 拿到业务系统用户
1 /** 2 * 3 */ 4 package cn.coreqi.security; 5 6 import org.slf4j.Logger; 7 import org.slf4j.LoggerFactory; 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.security.core.authority.AuthorityUtils;10 import org.springframework.security.core.userdetails.UserDetails;11 import org.springframework.security.core.userdetails.UserDetailsService;12 import org.springframework.security.core.userdetails.UsernameNotFoundException;13 import org.springframework.security.crypto.password.PasswordEncoder;14 import org.springframework.social.security.SocialUser;15 import org.springframework.social.security.SocialUserDetails;16 import org.springframework.social.security.SocialUserDetailsService;17 import org.springframework.stereotype.Component;18 19 /**20 * @author fanqi21 *22 */23 @Component24 public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {25 26 private Logger logger = LoggerFactory.getLogger(getClass());27 28 @Autowired29 private PasswordEncoder passwordEncoder;30 31 /*32 * (non-Javadoc)33 * 34 * @see org.springframework.security.core.userdetails.UserDetailsService#35 * loadUserByUsername(java.lang.String)36 */37 @Override38 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {39 logger.info("表单登录用户名:" + username);40 return buildUser(username);41 }42 43 @Override44 public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {45 logger.info("设计登录用户Id:" + userId);46 return buildUser(userId);47 }48 49 private SocialUserDetails buildUser(String userId) {50 // 根据用户名查找用户信息51 //根据查找到的用户信息判断用户是否被冻结52 String password = passwordEncoder.encode("123456");53 logger.info("数据库密码是:"+password);54 return new SocialUser(userId, password,55 true, true, true, true,56 AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));57 }58 59 }
⒑创建QQ登陆配置类
1 package cn.coreqi.social.qq.connect; 2 3 import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter; 4 import org.springframework.context.annotation.Configuration; 5 import org.springframework.social.connect.ConnectionFactory; 6 7 /** 8 * QQ登录配置 9 */10 @Configuration11 public class QQAutoConfig extends SocialAutoConfigurerAdapter {12 @Override13 protected ConnectionFactory createConnectionFactory() {14 String providerId = "qq"; //第三方id,用来决定发起第三方登录的url,默认是weixin15 String appId = "";16 String appSecret = "";17 return new QQConnectionFactory(providerId, appId, appSecret);18 }19 }
⒒自定义我们自己的SpringSocial配置
1 package cn.coreqi.social.config; 2 3 import org.springframework.social.security.SocialAuthenticationFilter; 4 import org.springframework.social.security.SpringSocialConfigurer; 5 6 public class CoreqiSpringSocialConfig extends SpringSocialConfigurer { 7 8 /** 9 *10 * @param object11 * @param12 * @return13 */14 @Override15 protected T postProcess(T object) {16 SocialAuthenticationFilter filter = (SocialAuthenticationFilter)super.postProcess(object);17 filter.setFilterProcessesUrl("/coreqi/auth");18 return (T) filter;19 }20 }
SpringSocialConfigurer 会在 configure方法中声明一个 SocialAuthenticationFilter,我们可以继承SpringSocialConfigurer达到自定义我们的SpringSocial配置需求。 ⒓声明一个SpringSocial的配置类
1 package cn.coreqi.social.config; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.context.annotation.Bean; 5 import org.springframework.context.annotation.Configuration; 6 import org.springframework.security.crypto.encrypt.Encryptors; 7 import org.springframework.social.config.annotation.EnableSocial; 8 import org.springframework.social.config.annotation.SocialConfigurerAdapter; 9 import org.springframework.social.connect.ConnectionFactoryLocator;10 import org.springframework.social.connect.ConnectionSignUp;11 import org.springframework.social.connect.UsersConnectionRepository;12 import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;13 import org.springframework.social.connect.web.ProviderSignInUtils;14 import org.springframework.social.security.SpringSocialConfigurer;15 16 import javax.sql.DataSource;17 18 @Configuration19 @EnableSocial20 public class SocialConfig extends SocialConfigurerAdapter {21 22 @Autowired23 private DataSource dataSource;24 25 @Autowired(required = false)26 private ConnectionSignUp connectionSignUp;27 28 /**29 *30 * @param connectionFactoryLocator 作用是去根据条件去查找应该用那个connectionFactory,因为系统中可能有很多的connectionFactory。31 * @return32 */33 @Override34 public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {35 //第三个参数的作用是把插入到数据库的数据进行加解密36 JdbcUsersConnectionRepository jdbcUsersConnectionRepository = new JdbcUsersConnectionRepository(dataSource,connectionFactoryLocator, Encryptors.noOpText());37 //jdbcUsersConnectionRepository.setTablePrefix(); //设置数据表的前缀38 if(connectionSignUp != null){39 jdbcUsersConnectionRepository.setConnectionSignUp(connectionSignUp);40 }41 return jdbcUsersConnectionRepository;42 }43 44 /**45 * 声明后还需要加在SpringSecurity过滤器链上46 * @return47 */48 @Bean49 public SpringSocialConfigurer coreqiSocialSecurityConfig(){50 CoreqiSpringSocialConfig config = new CoreqiSpringSocialConfig();51 config.signupUrl("/registry"); //当从业务系统中无法找到OAuth快捷登陆的用户,那么将用户引导到注册页面中52 return config;53 }54 55 //1.注册过程中如何拿到SpringSocial信息56 //2.注册完成后如何把业务系统的用户ID传给SpringSocial57 @Bean58 public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){59 return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator));60 }61 }
⒔应用我们的过滤器配置
1 package cn.coreqi.config; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 6 import org.springframework.social.security.SpringSocialConfigurer; 7 8 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 9 @Autowired10 private SpringSocialConfigurer coreqiSocialSecurityConfig;11 @Override12 protected void configure(HttpSecurity http) throws Exception {13 http.apply(coreqiSocialSecurityConfig);14 }15 }
⒕
1 package cn.coreqi.social.qq.connect; 2 3 import org.springframework.social.connect.Connection; 4 import org.springframework.social.connect.ConnectionSignUp; 5 import org.springframework.stereotype.Component; 6 7 /** 8 * 当没有从数据库中查找到第三方登录的用户,那么将执行ConnectionSignUp的execute方法生成新的用户id并存储到数据库中 9 */10 @Component11 public class CoreqiConnectionSignUp implements ConnectionSignUp {12 @Override13 public String execute(Connection connection) {14 return connection.getDisplayName();15 }16 }