这篇文章存在的原因是因为现在网络上流程的教程实现QQ登录的SpringBoot版本应该都是1.x 然而2.x以后有两个依赖已经出现了变动例如SocialAutoConfigurerAdapter这个类已经被删除,RelaxedPropertyRes这个类也是在1.4版本的也被删除了,作为修改和代替,注入ConnectionFactory的方法,我在SocialConfigurerAdapter这里的add方法中实现.
这篇是我的第一篇文章算是比较有意义下面的内容是从我的笔记里拷的格式可能会难看,大家见谅,如果代码有什么可以改进的地方请务必说出来.
如果有用的话也请留个言让我有点感觉
# Api (使用Token获取用户数据的) 每一个用户的API都是一个对象
写一个API接口里面有一个获取用户详情的方法 写一个UserInfo存储获取到的用户信息 写一个实现类
## QQ(API接口)
```
public interface QQ {
QQUserInfo getUserInfo();
}
```
## QQUserinfo(用户信息包装类)
```
@Data
@JsonIgnoreProperties(ignoreUnknown = true) //为了方便将QQ返回的用户信息包装成对象
public class QQUserInfo implements Serializable {
private String openid;
// 返回码
private String ret;
// 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
private String msg;
// 用户在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的100x100的头像,但40x40像素则是一定会有。;
private String figureurl_qq_2;
// 性别。 如果获取不到则默认返回"男"
private String gender;
}
```
## QQImql(API实现类)
```
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ{
// 申请QQ登录成功后,分配给应用的appid 这个写成配置文件的形式方便一点 每个第三方的appid都不一样
private ObjectMapper objectMapper = new ObjectMapper();
private String appId;
private String openid;
private static final String getOpenId = "https://graph.qq.com/oauth2.0/me?access_token=%s";
private static final String getUserInfo = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
public QQImpl(String accessToken,String appId){
// 将Token的发送策略设置为作为参数发起请求拼接到url里
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
RestTemplate restTemplate = getRestTemplate();
// 从配置文件里获取appid
this.appId = appId;
// 这个写法就是使用accessToken替换掉%s
// 拼接获取openid的url
// 这一步之所以要使用拼接的方式Token是因为这个方法每走完,Token还不是默认加入参数
String url = String.format(getOpenId,accessToken);
String result = restTemplate.getForObject(url,String.class);
// 截取openid 得到openid
this.openid = StringUtils.substringBetween(result, ""openid":"", ""}");
}
//发起获取用户信息的请求并将返回的字段包装成对象
@Override
public QQUserInfo getUserInfo() {
String url = String.format(getUserInfo,appId,openid);
String result = getRestTemplate().getForObject(url,String.class);
QQUserInfo qqUserInfo = null;
// 将这个字符串读取成一个userInfo对象
try {
qqUserInfo = objectMapper.readValue(result, QQUserInfo.class);
qqUserInfo.setOpenid(openid);
return qqUserInfo;
} catch (IOException e) {
throw new RuntimeException("获取用户信息失败",e);
}
}
}
```
# OAuth2Template (引导用户去QQ登录页面并且获取Toekn的)
重写一个Template不用spring提供的意义就是QQ的返回的Toekn类型spring提供的Tempalte解析不了
1.重写给父类的构造方法并且UseParametersForClientAuthentication为true
2.增加可以解析Token的方法 因为QQ的返回是html/text的需要加
3.使用创建Template的时候注入的引导用户进行登录的url和获取token的url
// 这个方法就是为了获取并且拆解Token
// 因为spring认为返回的Token的形式是Json所以默认按Json的方式去获取Token并且封装成一个Map
// 但是QQ的Token返回形式是一个字符串所以重写一个方法 去重写获得Token
//把响应的实体的格式按照qq的标准做一个自定义
```
public class QQOAuth2Template extends OAuth2Template {
Logger logger = LoggerFactory.getLogger(getClass());
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
// 因为exchangeForAccess方法会判断这个值之后才会带上ClientID和client_secret参数去发起访问
setUseParametersForClientAuthentication(true);
}
// 这个方法就是添加可以解析Token的格式
// 这个方法就没办法登录的原因不能解析qq传回来的text/html的返回格式
//加入一种读取text/html的返回结果的方法
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
// 这个方法就是为了获取并且拆解Token
// 因为spring认为返回的Token的形式是Json所以默认按Json的方式去获取Token并且封装成一个Map
// 但是QQ的Token返回形式是一个字符串所以重写一个方法 去重写获得Token
//把响应的实体的格式按照qq的标准做一个自定义
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
// 这个是响应实体 加了泛型就是意味这个响应实体是用String接收 发起请求获取Token
ResponseEntity<String> result = getRestTemplate().postForEntity(accessTokenUrl,parameters,String.class);
//分割这个响应实体 Token
String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(String.valueOf(result),"&");
//取出里面的内容
String accessToken = StringUtils.substringAfterLast(items[0],"=");
logger.info - 最佳的logger 来源和相关信息。("获取到的Token:"+accessToken);
//到期时间 是一个秒数为单位的
Long expiresIn = new Long(StringUtils.substringAfterLast(items[1],"="));
logger.info - 最佳的logger 来源和相关信息。("获取到的Token的刷新时间(秒):"+expiresIn);
//刷新Token的
String refreshToken = StringUtils.substringAfterLast(items[2],"=");
logger.info("刷新的Token refreshToken:"+refreshToken);
return new AccessGrant(accessToken,null,refreshToken,expiresIn);
}
}
```
# ServiceProvider (服务提供商也就是QQ 需要template和API)
两个静态常量一个是要将qq引导到哪个地址去授权一个是授权码拿到之后去哪个地址换Token
```
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
// 将用户引导到哪个地址进行用户认证
private static final String authorizeUrl = "QQ帐号安全登录";
// 使用授权码获取令牌的地址
private static final String accessTokenUrl = "https://graph.qq.com/oauth2.0/token";
private String appId;
//因为流程执行到获取到授权码用授权码去获取Token令牌的类我们使用的是spring提供的OAth2Template 这个类解析不了返回来的数据 只能解析Json和Form类性的 但是返回的Token的类型是txt/html 无法解析也就是获取不到Token
//也就成了登录失败 SpringSocial的登录失败处理器会让请求跳转到/sigin地址
public QQServiceProvider(String appId ,String appSecret) {
// 传入springSocial提供的OAuth2perations 实现类
super(new QQOAuth2Template(appId,appSecret,authorizeUrl,accessTokenUrl));
this.appId = appId;
}
@Override
public QQ getApi(String token) {
return new QQImpl(token,appId);
}
}
```
# QQAdapter (用户信息适配器 将获取到用户信息和Connection适配)
```
public class QQAdapter implements ApiAdapter<QQ> {
// 测试连通
@Override
public boolean test(QQ qq) {
return true;
}
@Override
public void setConnectionValues(QQ api, ConnectionValues connectionValues) {
QQUserInfo qqUserInfo = api.getUserInfo();
// 设置用户的名字
connectionValues.setDisplayName(qqUserInfo.getNickname());
// 设置用户的头像
connectionValues.setImageUrl(qqUserInfo.getFigureurl_qq_1());
// 这个是个人主页但是qq是没有的
connectionValues.setProfileUrl(null);
// qq号 openId
connectionValues.setProviderUserId(qqUserInfo.getOpenid());
}
@Override
public UserProfile fetchUserProfile(QQ qq) {
return null;
}
@Override
public void updateStatus(QQ qq, String s) {
}
}
```
# QQConnectionFactory
重写父类的构造方法 创建ConnectionFactory的地方在配置类上
```
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
public QQConnectionFactory(String providerId,String appid, String appSecret) {
super(providerId,new QQServiceProvider(appid,appSecret), new QQAdapter());
}
}
```
# social配置类
1.第一个方法是将一个操作数据库Social用户绑定的表的对象加入到spring容器
2.创建一个过滤器加入到spring容器并且在最后将它加入到security的过滤链中拦截QQ登录的请求 这个过滤器主要配置两个地方一个是QQ登录的URI的前半段也就是/auth另一个是
如果当前QQ用户没有和业务系统用户进行绑定将用户引导到哪个URI进行注册和绑定 绑定的逻辑需要我们去写注册的逻辑也有所不一样 也需要我们去写
3.向Spring容器加入一个工具类用于在用户进行绑定的时候获取QQ的用户信息和绑定的时候将用户的业务系统的id传给Social进行绑定账号
4.大多数教程使用的SpringBoot版本是1.5一下他们使用继承实现SocialAutoConfigurerAdapter这个接口去创建注入ConnectionFactory 但是2.0以上的boot版本移除了这个类
Connection的注入我使用第四个方法的样子去注入 ps:@ConditionalOnProperty这个注解在2.0以上的版本可以不需要去写prefix这个属性了可以直接在name属性写全路径
5.第五个方法是为了解决报错有什么用不知道
```
@Configuration
//这个注解是为了启动EnableSocial
@EnableSocial
@ConditionalOnProperty(prefix = "tidc.tao.security.social.qq",name="appId") //只有tidc.tao.security.social.qq.appId被配置了值这个配置类才生效
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private SecurityProperties securityProperties;
// @Autowired(required = false)
// TidcConnctionSignUp tidcConnctionSignUp;
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
// 数据源 第二个是查找ConnectionFactory 第三个是加密解密
JdbcUsersConnectionRepository jdbcUsersConnectionRepository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
// 如果你在这个表的名字的前面加一些前缀就需要这个配置
// jdbcUsersConnectionRepository.setTablePrefix("这里写前缀");
// if(tidcConnctionSignUp!=null){
// jdbcUsersConnectionRepository.setConnectionSignUp(tidcConnctionSignUp);
// }
return jdbcUsersConnectionRepository;
}
// 加入一个过滤器到Ioc容器中从securityConfig里取出来并加入到security的过滤链里
@Bean
public SpringSocialConfigurer springSocialConfigurer(){
//传入传入自定义的登录url的前半段
TidcSpringSocialConfigurer tidcSpringSocialConfigurer = new TidcSpringSocialConfigurer(securityProperties.getSocial().getQq().getFilterProcessesUrl());
// 传入注册页面在获取不到用户信息的时候跳转到注册页面
tidcSpringSocialConfigurer.signupUrl(securityProperties.getSocial().getQq().getSignUpUrl());
return tidcSpringSocialConfigurer;
}
// 这个工具类帮助我们在注册页面获得用户的社交信息和将用户的业务系统的id传给Social
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator));
}
@Override
public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
super.addConnectionFactories(connectionFactoryConfigurer, environment);
QQProperties qqProperties = securityProperties.getSocial().getQq();
connectionFactoryConfigurer.addConnectionFactory(new QQConnectionFactory(qqProperties.getProviderId(),qqProperties.getAppId(),qqProperties.getAppSecret()));
}
@Override
public UserIdSource getUserIdSource() {
// TODO Auto-generated method stub
return new AuthenticationNameUserIdSource();
}
}
```
这个就是控制登录和注册uri的类
```
public class TidcSpringSocialConfigurer extends SpringSocialConfigurer {
// 这个属性设置成可配置的形式
String filterProcessesUrl ;
public TidcSpringSocialConfigurer(String filterProcessesUrl){
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
// 这个方法里的参数就是SocialAuthenticationFilter
// 这个类就是为了改变qq登录的url的/auth部分
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
```
将过滤器加入到security的过滤链
```
.and()
// QQ登录的过滤器
.apply(tidcSocialConfigurer)
.and()
```
# 登录时的用户信息校验根据用户绑定的ID去查询用户信息(第二个方法和第二个继承的接口)
```
@Component
@CrossOrigin //解决跨域问题
public class MyUserDeatisService implements UserDetailsService , SocialUserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) {
//实现这个接口方法 从数据库里获取到一个完整的用户信息封装进UserDetails里
//因为UserDetails是个接口不能被实例化所以我们返回一个接口的实现类也就是SpringSecurity里的User
//这个user构造方法是username,password,四个bool值代表UserDetails的那四个方法的返回值一个为false登录就会失败
// 以及权限集合 下面那个方法的把一个字符串以逗号隔开分割成权限集合对象
//密码的对比他会自己对比
System.out.println(username+"登录了!!!!!");
Student student = userMapper.selectStudent(username.trim());
Teacher teacher = userMapper.selectTeacher(username);
String power = "ROLE_STUDENT";
if(student!=null) {
System.out.println(student);
return returnUser(student.getEmail(),student.getPassword(),power);
}
if(teacher!=null){
//这个return的user是spring的
System.out.println(teacher);
power = "ROLE_TEACHER,ROLE_STUDENT";
return returnUser(student.getEmail(),student.getPassword(),power);
}
return returnUser(student.getEmail(),student.getPassword(),power);
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
Integer i = Integer.parseInt(userId);
Student student = userMapper.selectStudent_id(i);
String power = "ROLE_STUDENT";
return (SocialUserDetails) returnUser(student.getEmail(),student.getPassword(),power);
}
public User returnUser(String email,String password,String power){
return new SocialUser(email,password,
true,true,true,true,
AuthorityUtils.commaSeparatedStringToAuthorityList(power));
}
}
```
# QQ用户进行绑定
```
public UserOV register(Student student,UserOV userOV,HttpServletRequest req) {
// 这里先根据第三方的用户信息来注册拿到用户的id
Student student2 = userMapper.selectStudent(student.getEmail().trim());
if(passwordEncoder.matches(student.getPassword(),student2.getPassword())){
// 接下来就是把用户的id传给SpringSocial让它把用户的业务系统id和第三方的用户信息绑定到一起
providerSignInUtils.doPostSignUp(String.valueOf(student2.getId()),new ServletWebRequest(req));
return userOV.setCode(200);
}
return userOV.setCode(500).setMessage("密码错误或者用户名不正确");
}
```