上篇文章,我们开发完了从提供商(QQ)获取用户信息的代码。
获取到的用户信息会被springsocial封装成了一个Connection对象
。而我们的目的是让spring-security/spring-social拿着该对象与我们系统内的对象进行比对校验,如果该对象可以和我们数据库里的某个对象信息完全匹配,则生成一个校验成功的对象Authentication
,并将其放入SecurityContext
,这也就完成了第三方登陆的整个流程。
这篇文章将在上几篇文章的基础上继续介绍利用QQ进行第三方登陆的整个流程开发。
UserConnection表是springsocial为我们准备的一张用来维护我们的系统和QQ、微信、微博等第三方应用之间关系的表,建表语句可在如下包目录结构下找到:
注意:
建表时可能会遇到一些问题,我专门为此写过一篇博客:【MySQL报错处理】[Err] 1064 、[Err] 1055、JdbcUsersConnectionRepository.sql建表报错
UserConnection表的表结构
如下,前三个字段是一个联合主键,通过这三个字段其实就可以将我们系统的用户和提供商(QQ,微信等)唯一进行关联起来。表中的其他字段也都比较好理解,这里不再过多阐述。
前面几篇文章基本完成了如下流程的开发:
用户点击第三方登陆按钮(如QQ登陆按钮)—> 跳转到QQ授权页面 —> 用户通过扫码等方式进行授权 —> 利用springsocial走oauth2协议的整个流程(获取授权码,通过授权码换accesstoken)—> 拿着accesstoken从QQ拿到openid,并拿着accesstoken,openid等从QQ获取到用户信息 —> 将获取到的用户信息,accesstoken,openid等封装成Connection对象。
要想拿着Connection对象与我们系统内的用户对象进行比对校验,从而完成第三方登陆的整个流程的话,接下来要做的第一件事就是去UserConnection表里通过Connection对象查找有没有对应的userId
,这个过程springsocial已经帮我们写完了,但是要求我们配置一个UsersConnectionRepository对象的具体实现类。我们使用的实现类为JdbcUsersConnectionRepository,其具体配置如下:
package com.nrsc.security.core.social;
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.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.security.SpringSocialConfigurer;
import javax.sql.DataSource;
/** * @author : Sun Chuan * @date : 2019/8/7 20:57 * Description: * UsersConnectionRepository的实现类,用来拿着Connection对象查找UserConnection表中是否与之相对应的userId * userId就是我们系统中的唯一标识,这个应该由各个系统自己根据业务去指定,比如说你系统里的username是唯一的, * 则这个useId就可以是username */
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
/** * 第二个参数的作用:根据条件查找该用哪个ConnectionFactory来构建Connection对象 * 第三个参数的作用: 对插入到userconnection表中的数据进行加密和解密 */
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
//设置userconnection表的前缀
repository.setTablePrefix("nrsc_");
return repository;
}
}
上一步中通过Connection对象拿到了userId这还没算完,因为spring-security还需要像校验用户名+密码登陆时那样去看一下该账户是否过期,是否被锁定等。springsocial也像用户名+密码校验时通过username获取用户信息一样,提供了一个方法loadUserByUserId
,我们只需要实现SocialUserDetailsService接口并将其注入到spring容器,springsocial就可以调用到该方法。
由于loadUserByUserId
方法的返回值为SocialUserDetails
,其实就是UserDetails
的子类,因此可以将该方法和之前用户名+密码校验时实现的loadUserByUsername
放在一起,代码如下:
package com.nrsc.security.server.security;
import lombok.extern.slf4j.Slf4j;
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;
@Component("NRSCDetailsService")
@Slf4j
public class NRSCDetailsService implements UserDetailsService , SocialUserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
// @Override
// public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//
// //点击页面的登陆时,你实际需要做的事为
// //1.拿着用户名去数据库里查询其密码
// //2.将拿到的用户名和密码封装到User里进行返回
//
// log.info("用户名为:" + username);
// //假设下面的密码是根据用户名获得的
// String password = passwordEncoder.encode("123456");
// log.info("password:" + password);
// //return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
//// return new User(username, password, true,true,true,false,
//// AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
// return new NrscUser(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
// }
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("表单登录用户名:" + username);
return buildUser(username);
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
log.info("设计登录用户Id:" + userId);
return buildUser(userId);
}
private SocialUserDetails buildUser(String userId) {
// 根据用户名查找用户信息
//根据查找到的用户信息判断用户是否被冻结
String password = passwordEncoder.encode("123456");
log.info("数据库密码是:"+password);
return new SocialUser(userId, password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
springsoical为我们弄好了SocialAuthenticationFilter的配置类,我们只需要将其注入到spring容器,然后通过apply()的方式加入到spring-security的过滤器链中就可以了。
我将该配置类注入spring容器的代码写在了本文第3部分的SocialConfig配置类中
,代码如下:
/** * 通过apply()该实例,可以将SocialAuthenticationFilter加入到spring-security的过滤器链 */
@Bean
public SpringSocialConfigurer nrscSocialSecurityConfig() {
// 默认配置类,进行组件的组装
// 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
return springSocialConfigurer;
}
代码这里不贴了,可以直接通过本篇提交代码的commit记录来查看:
项目源码地址https://github.com/nieandsun/security
代码如下:
<h3>社交登录h3>
<a href="/auth/qq">QQ登录a>
SocialAuthenticationFilter会拦截所有路径中含有/auth的请求
/qq其实是在构建ConnectionFactory
对象时第一个参数对应的值。
即当一个请求的url中有/auth/qq时,spring-security会让SocialAuthenticationFilter过滤器将其拦截下来,然后利用我们写的QQConnectionFactory对象来走oauth2流程+构建connection对象。
点击QQ登陆会跳转到QQ的授权页面,但我们跳转到的页面里显示redirect uri is illegal(100010)
,具体原因将在接下来的博客中进行解决。