使用SpringSocial开发QQ登录
⒈编写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登录成功后,分配给应用的appid 22 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 @Override 40 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 @Override 23 protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) { 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 @Override 38 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<QQ> { 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 @Override 24 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<QQ> { 15 16 /** 17 * 用来测试当前的API是否可用 18 * @param qq 19 * @return 20 */ 21 @Override 22 public boolean test(QQ qq) { 23 return true; 24 } 25 26 /** 27 * 将服务提供商个性化的用户信息映射到ConnectionValues标准的数据化结构上 28 * @param qq 29 * @param connectionValues 30 */ 31 @Override 32 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 qq 43 * @return 44 */ 45 @Override 46 public UserProfile fetchUserProfile(QQ qq) { 47 return null; 48 } 49 50 /** 51 * 52 * @param qq 53 * @param s 54 */ 55 @Override 56 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<QQ> { 7 8 /** 9 * 10 * @param providerId 我们给服务提供商的唯一标识 11 * @param appId 服务提供商给的AppId 12 * @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 fanqi 21 * 22 */ 23 @Component 24 public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService { 25 26 private Logger logger = LoggerFactory.getLogger(getClass()); 27 28 @Autowired 29 private PasswordEncoder passwordEncoder; 30 31 /* 32 * (non-Javadoc) 33 * 34 * @see org.springframework.security.core.userdetails.UserDetailsService# 35 * loadUserByUsername(java.lang.String) 36 */ 37 @Override 38 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 39 logger.info("表单登录用户名:" + username); 40 return buildUser(username); 41 } 42 43 @Override 44 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 @Configuration 11 public class QQAutoConfig extends SocialAutoConfigurerAdapter { 12 @Override 13 protected ConnectionFactory<?> createConnectionFactory() { 14 String providerId = "qq"; //第三方id,用来决定发起第三方登录的url,默认是weixin 15 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 object 11 * @param <T> 12 * @return 13 */ 14 @Override 15 protected <T> 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 @Configuration 19 @EnableSocial 20 public class SocialConfig extends SocialConfigurerAdapter { 21 22 @Autowired 23 private DataSource dataSource; 24 25 @Autowired(required = false) 26 private ConnectionSignUp connectionSignUp; 27 28 /** 29 * 30 * @param connectionFactoryLocator 作用是去根据条件去查找应该用那个connectionFactory,因为系统中可能有很多的connectionFactory。 31 * @return 32 */ 33 @Override 34 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 * @return 47 */ 48 @Bean 49 public SpringSocialConfigurer coreqiSocialSecurityConfig(){ 50 CoreqiSpringSocialConfig config = new CoreqiSpringSocialConfig(); 51 config.signupUrl("/registry"); //当从业务系统中无法找到OAuth快捷登陆的用户,那么将用户引导到注册页面中 52 return config; 53 } 54 55 //1.注册过程中如何拿到SpringSocial信息 56 //2.注册完成后如何把业务系统的用户ID传给SpringSocial 57 @Bean 58 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 @Autowired 10 private SpringSocialConfigurer coreqiSocialSecurityConfig; 11 @Override 12 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 @Component 11 public class CoreqiConnectionSignUp implements ConnectionSignUp { 12 @Override 13 public String execute(Connection<?> connection) { 14 return connection.getDisplayName(); 15 } 16 }