图一
基于SpringSocial实现qq登录,要走一个OAuth流程,拿到服务提供商qq返回的用户信息。
由上篇介绍的可知,用户信息被封装在了Connection里,所以最终要拿到Connection
1,Connection <---- ConnectionFactory:拿到一个Connection,就需要一个ConnectionFactory工厂
2,ConnectionFactory:需要ServiceProvider 服务提供商、ApiAdapter api适配器,在服务提供商和业务系统user之间转换
3,ServiceProvider :需要 OAuthe2Operations、Api 读取用户信息
4,在数据库中建数据库表
上图中括号里都是Spring Social对对应接口的默认实现,代码中使用它们。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
开始开发:
1,构建ServiceProvider
1.1 构建ServiceProvider需要的Api(api 用来获取用户信息,就是图一的第6步,自定义QQ接口,继承 Spring Social的默认实现 AbstractOAuth2ApiBinding )
代码:
接口
public interface QQ { /** * 获取qq用户信息*/ QQUserInfo getUserInfo() throws Exception; }
实现类:
package com.imooc.security.core.social.qq.api;/** * 流程中的Api。 * ClassName: QQImpl * @Description: * ***********注意************* * 这个是多例的,每个用户不一样进来他们的accessToken、openid是不一样的 * 所以不能@Component声明为spring组件!!! * ***********注意************* * @author lihaoyang * @date 2018年3月6日 */ public class QQImpl extends AbstractOAuth2ApiBinding implements QQ{ /** * 调用qq的get_user_info * 1:3个参数,OAuth2.0协议的通用三个参数: * access_token: 父类已提供 * appid://申请QQ登录成功后,分配给应用的appid * openid://用户的ID,与QQ号码一一对应。 * * 2:2个路径 * 获取openid的路径: * https://graph.qq.com/oauth2.0/me?access_token=YOUR_ACCESS_TOKEN * 获取用户信息的路径: * https://graph.qq.com/user/get_user_info?access_token=*************&oauth_consumer_key=12345&openid= */ private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s"; private static final String URL_GET_USRE_INFO = "https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=12345&openid=%s"; private String appId; private String openId; private ObjectMapper objectMapper = new ObjectMapper(); /** * 实例化时获取openid * <p>Description: </p> * @param accessToken * @param appId */ public QQImpl(String accessToken , String appId){ //父类默认构造会把accessToken放在请求头里,这是不符合qq要求的放在url参数里的,所以掉一下作为参数的构造 super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER); this.appId = appId; String url = String.format(URL_GET_USRE_INFO, accessToken); String result = getRestTemplate().getForObject(url, String.class);//调用父类的restTemplate发请求,获取openid System.err.println(result); //{"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} //截取openid this.openId = StringUtils.substringBetween(result, "\"openid\"", "}"); } @Override public QQUserInfo getUserInfo() throws Exception { //accessToken已在父类挂在了参数 String url = String.format(URL_GET_USRE_INFO, appId,openId); String result = getRestTemplate().getForObject(url, String.class); System.err.println(result); QQUserInfo userInfo = objectMapper.readValue(result, QQUserInfo.class); return userInfo; } }
AbstractOAuth2ApiBinding:
********* 登录QQ互联官网get_user_info接口http://wiki.connect.qq.com/get_user_info *************
需要的3个参数:
QQUserInfo照着qq要求的构造:
1.2,有了Api,就可以构建ServiceProvider,因为ServiceProvider需要的第三个参数OAuthOperations可以使用Spring Social提供的OAuth2Template
代码:
package com.imooc.security.core.social.qq.connect; import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider; import org.springframework.social.oauth2.OAuth2Template; import com.imooc.security.core.social.qq.api.QQ; import com.imooc.security.core.social.qq.api.QQImpl; /** * QQ服务提供商 * ClassName: QQServiceProvider * @Description: * 需要继承spring social的默认实现AbstractOAuth2ServiceProvider * 泛型是指获取用户信息的Api,类型就是QQ * @author lihaoyang * @date 2018年3月6日 */ public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ>{ private String appId; //将用户引导到获取授权码的地址 private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize"; //拿着授权码申请令牌token的地址 private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token"; /** * 返回ServiceProvider需要的OAuthOperations * <p>Description: </p> * @param appId 不同的应用是不一样的 * @param appSecret 不同的应用是不一样的 */ public QQServiceProvider(String appId , String appSecret) { super(new OAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN)); } /** * 返回ServiceProvider需要的Api */ @Override public QQ getApi(String accessToken) { return new QQImpl(accessToken, appId); } }
到此为止,Service已构建好
2,构建ConnectionFactory工厂,产生Connection
新建QQAdapter,实现ApiAdapter接口:
package com.imooc.security.core.social.qq.connect; import org.springframework.social.connect.ApiAdapter; import org.springframework.social.connect.ConnectionValues; import org.springframework.social.connect.UserProfile; import com.imooc.security.core.social.qq.api.QQ; import com.imooc.security.core.social.qq.api.QQUserInfo; /** * 在服务提供商qq和第三方应用之间做用户信息的转换 * ClassName: QQAdapter * @Description: * 在服务提供商qq和第三方应用之间做用户信息的转换 * 实现 ApiAdapter接口,泛型是API接口,对应我们的QQ接口 * @author lihaoyang * @date 2018年3月6日 */ public class QQAdapter implements ApiAdapter<QQ>{ /** * 测试当前api是否可用,测试qq是否可用 */ @Override public boolean test(QQ api) { //就不掉了,直接true return true; } /** * Connection和api之间的适配 * ConnectionValues: * 创建Connection需要的数据项 * 从api中获取数据,给ConnectionValues设置值 */ @Override public void setConnectionValues(QQ api, ConnectionValues values) { //获取用户信息 QQUserInfo userInfo = api.getUserInfo(); values.setDisplayName(userInfo.getNickname());//展示名,qq用户名 values.setImageUrl(userInfo.getFigureurl_1()); //qq头像 values.setProfileUrl(null); //个人主页,qq没有,微博有 values.setProviderUserId(userInfo.getOpenId()); } /** * */ @Override public UserProfile fetchUserProfile(QQ api) { // TODO Auto-generated method stub return null; } /** * 某些社交如微博才有 */ @Override public void updateStatus(QQ api, String message) { // do nothing } }
构建ConnectionFactory:QQConnectionFactory类实现ConnectionFactory接口,把构造器需要的 ServiceProvider (QQServiceProvider) ApiAdapter (QQAdapter) 给他
package com.imooc.security.core.social.qq.connect; import org.springframework.social.connect.support.OAuth2ConnectionFactory; import com.imooc.security.core.social.qq.api.QQ; /** * 创建Connection工厂 * ClassName: QQConnectionFactory * @Description: * 创建Connection工厂 * 继承默认实现 OAuth2ConnectionFactory * 泛型:Api是什么,就是我们的QQ * @author lihaoyang * @date 2018年3月7日 */ public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> { /** * * 需要两个对象: * 1,ServiceProvider --> QQServieProvider * 2,ApiAdapter --> QQApiAdapter * <p>Description: </p> * @param providerId * @param appId * @param appSecret */ public QQConnectionFactory(String providerId, String appId , String appSecret) { super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter()); } }
有了ConnectionFactory,就可以自己产生Connection了,我们不用管产生Connection的过程。
第四步:配置JdbcUsersConnectionRepository
有了Connection,还需要把Connection中的数据保存到数据库中,所以还需要JdbcUsersConnectionRepository。Spring已经写好了,只需要配置一下即可。
新建SocialConfig配置类,继承SocialConfigurerAdapter,注入数据源
package com.imooc.security.core.social; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; 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.UsersConnectionRepository; import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository; /** * SpringSocial相关配置 * ClassName: SocialConfig * @Description: 社交相关配置 * @author lihaoyang * @date 2018年3月7日 */ @Configuration @EnableSocial //把Spring Social相关的特性启动 public class SocialConfig extends SocialConfigurerAdapter{ @Autowired private DataSource dataSource; /** * 配置JdbcUsersConnectionRepository */ @Override public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) { /** * 参数: * 1, dataSource:数据源 注进来 * 2,connectionFactoryLocator: * 根据条件去查找需要的 ConnectionFactory,因为系统里可能有多个ConnectionFactory,如qq,微信等,使用默认穿的 * 3,textEncryptor:把插入到数据库的数据加密解密 */ return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());//先不加密 } }
新建存Connection数据的表,该sql在JdbcUsersConnectionRepository所在的包里:org.springframework.social.connect.jdbc
sql:
-- This SQL contains a "create table" that can be used to create a table that JdbcUsersConnectionRepository can persist -- connection in. It is, however, not to be assumed to be production-ready, all-purpose SQL. It is merely representative -- of the kind of table that JdbcUsersConnectionRepository works with. The table and column names, as well as the general -- column types, are what is important. Specific column types and sizes that work may vary across database vendors and -- the required sizes may vary across API providers. 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);
字段说明:
userId :业务系统用户id
providerId :服务提供商id,是qq、微信、微博...
providerUserId :openId
rank :等级
displayName :昵称
profileUrl :主页
imageUrl :头像
accessToken :
secret :
refreshToken :
expireTime :
在数据库执行一下,这个表名不能变,但是可以根据公司需要加前缀,如加上imooc_
若加上了前缀,,则配置 JdbcUsersConnectionRepository 时需要指定一下前缀:
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
repository.setTablePrefix("imooc_");
用户表单登录时,我们建了MyUserDetailsService 用 实现了SpringSecurity的UserDetailsService接口通过用户名来获取用户信息;社交登录,spring提供了SocialUserDetailsService接口,通过userId获取UserDetail用户信息,在MyUserDetailsService 也实现SocialUserDetailsService来处理社交登录,SocialUserDetails 是UserDetails的实现
package com.imooc.security.browser;/** * UserDetailsService是SpringSecurity的一个接口, * 只有一个方法:根据用户名获取用户详情 * ClassName: MyUserDetailService * @Description: * SocialUserDetailsService:SpringSocial查询用户信息的接口 * @author lihaoyang * @date 2018年2月28日 */ @Component public class MyUserDetailsService_ implements UserDetailsService,SocialUserDetailsService{ private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private PasswordEncoder passwordEncoder; /** * UserDetails接口,实际可以自己实现这个接口,返回自己的实现类 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("表单登录用户名:"+username); //根据用户名查询用户信息 //User:springsecurity 对 UserDetails的一个实现 //为了演示在这里用passwordEncoder加密一下密码,实际中在注册时就加密,此处直接拿出密码 // String password = passwordEncoder.encode("123456"); // System.err.println("加密后密码: "+password); // //参数:用户名|密码|是否启用|账户是否过期|密码是否过期|账户是否锁定|权限集合 // return new User(username,password,true,true,true,true,AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); 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"); System.err.println("加密后密码: "+password); //参数:用户名|密码|是否启用|账户是否过期|密码是否过期|账户是否锁定|权限集合 return new SocialUser(userId,password,true,true,true,true,AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
第五步:配置appId、
QQProperties:
package com.imooc.security.core.properties; import org.springframework.boot.autoconfigure.social.SocialProperties; /** * QQ登录相关配置 * ClassName: QQProperties * @Description: TODO * @author lihaoyang * @date 2018年3月7日 */ public class QQProperties extends SocialProperties { private String providerId = "qq"; public String getProviderId() { return providerId; } public void setProviderId(String providerId) { this.providerId = providerId; } }
SocialProperties 是spring提供的:
加一层SocialProperties:
package com.imooc.security.core.properties; /** * 第三方登录相关配置 * ClassName: SocialProperties * @Description: TODO * @author lihaoyang * @date 2018年3月7日 */ public class SocialProperties { private QQProperties qq = new QQProperties(); public QQProperties getQq() { return qq; } public void setQq(QQProperties qq) { this.qq = qq; } }
QQAutoConfig:
package com.imooc.security.core.social.qq.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter; import org.springframework.context.annotation.Configuration; import org.springframework.social.connect.ConnectionFactory; import com.imooc.security.core.properties.QQProperties; import com.imooc.security.core.properties.SecurityProperties; import com.imooc.security.core.social.qq.connect.QQConnectionFactory; /** * * ClassName: QQAutoConfig * @Description: * @ConditionalOnProperty: 配置里有imooc.security.social.qq.app-id 这个类才会生效 * @author lihaoyang * @date 2018年3月7日 */ @Configuration @ConditionalOnProperty(prefix = "imooc.security.social.qq" , name = "app-id") public class QQAutoConfig extends SocialAutoConfigurerAdapter { @Autowired private SecurityProperties securityProperties; @Override protected ConnectionFactory<?> createConnectionFactory() { QQProperties qqConfig = securityProperties.getSocial().getQq(); return new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret()); } }
在demo项目的application.properties 里配置:
imooc.security.social.qq.app-id =
imooc.security.social.qq.app-secret =
欢迎关注个人公众号一起交流学习: