admin管理员组文章数量:1567558
上一篇文章《Spring Security技术栈开发企业级认证与授权(十三)Spring Social集成第三方登录验证开发流程介绍》主要是介绍了
OAuth2
协议的基本内容以及Spring Social
集成第三方登录验证的基本流程。那么在前篇文章的基础上,我们在本篇文章中将介绍Spring Social
集成
我们继续将上一篇文章的图贴到这里,对着图片开发相应的模块。
一、开发获取用户QQ信息的接口
在前一篇文章中介绍到,Spring Social
封装了OAuth
协议的标准步骤,我们只需要配置第三方应用的认证服务器地址即可,就可以获取到访问令牌Access Token
,拿着这个令牌就可以获取到用户信息了,QQ
互联的文档中介绍到,要正确获取到用户的基础信息之前,还需要通过Access Token
来获取到用户的OpenID
,这个OpenID
是每一个用户使用QQ
登录到你的系统都会产生一个唯一的ID
。如下图所示:
要获取到OpenID
, 需要访问下面的API
地址,带上正确的access_token
参数即可。
内容 | 说明 |
---|---|
请求URL | https://graph.qq/oauth2.0/me |
请求方法 | GET |
请求参数 | access_token |
返回内容 | callback( {“client_id”:“YOUR_APPID”,“openid”:“YOUR_OPENID”} ); |
正确访问API
,拿到返回内容之后,可以对内容进行解析,获取到OpenID
,然后再访问获取用户信息的接口,携带必需的参数,从而拿到用户的信息。获取用户信息,相关说明如下表所以:
内容 | 说明 |
---|---|
请求URL | https://graph.qq/user/get_user_info |
请求方法 | GET |
请求参数 | access_token=ACCESS_TOKEN&oauth_consumer_key=APP_ID&openid=OPENID |
返回内容 | 返回内容是JSON格式的字符串,具体字段和说明如下表所示 |
获取用户信息JSON
返回体说明:
参数说明 | 描述 |
---|---|
ret | 返回码 |
msg | 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码 |
is_lost | 是否丢失,0否,1是 |
nickname | 用户在QQ空间的昵称 |
figureurl | 大小为30×30像素的QQ空间头像URL |
figureurl_1 | 大小为50×50像素的QQ空间头像URL |
figureurl_2 | 大小为100×100像素的QQ空间头像URL |
figureurl_qq_1 | 大小为40×40像素的QQ头像URL |
figureurl_qq_2 | 大小为100×100像素的QQ头像URL |
gender | 性别。 如果获取不到则默认返回"男" |
province | 省份 |
city | 城市 |
year | 出生年月 |
constellation | 星座 |
is_yellow_vip | 是否是黄钻,0否,1是 |
vip | 是否是QQ会员,0否,1是 |
yellow_vip_level | 黄钻等级 |
level | QQ等级 |
is_yellow_year_vip | 是否是黄钻年费会员,0否,1是 |
那么错误的返回体就很简单: { "ret":1002, "msg":"请先登录" }
。
那么这一些操作我们该如何在代码中体现呢?先来写一个获取用户信息的接口QQ
,代码如下:
package com.lemon.security.core.social.qq.api;
/**
* 获取QQ用户信息的接口
*
* @author jiangpingping
* @date 2019-02-05 11:30
*/
public interface QQ {
/**
* 获取QQ用户的信息
*
* @return QQ用户信息
*/
QQUserInfo getUserInfo();
}
其中实体类QQUserInfo
则是封装了从腾讯服务器获取到的用户基础信息,具体的代码如下所示:
package com.lemon.security.core.social.qq.api;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
/**
* QQ用户信息
*
* @author jiangpingping
* @date 2019-02-05 11:32
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class QQUserInfo {
/**
* 用户的OpenId
*/
private String openId;
/**
* 返回码
*/
private Integer ret;
/**
* 返回消息,如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码
*/
private String msg;
/**
* 是否丢失0否,1是
*/
@JsonProperty("is_lost")
private Integer isLost;
/**
* 用户在QQ空间的昵称
*/
private String nickname;
/**
* 大小为30×30像素的QQ空间头像URL
*/
@JsonProperty("figureurl")
private String figureUrl30;
/**
* 大小为50×50像素的QQ空间头像URL
*/
@JsonProperty("figureurl_1")
private String figureUrl50;
/**
* 大小为100×100像素的QQ空间头像URL
*/
@JsonProperty("figureurl_2")
private String figureUrl100;
/**
* 大小为40×40像素的QQ头像URL
*/
@JsonProperty("figureurl_qq_1")
private String figureUrlQq40;
/**
* 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100x100的头像,但40x40像素则是一定会有
*/
@JsonProperty("figureurl_qq_2")
private String figureUrlQq100;
/**
* 性别。 如果获取不到则默认返回"男"
*/
private String gender;
/**
* 省份
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 出生年份
*/
private String year;
/**
* 星座
*/
private String constellation;
/**
* 是否是黄钻,0否,1是
*/
@JsonProperty("is_yellow_vip")
private String isYellowVip;
/**
* 是否是会员,0否,1是
*/
private String vip;
/**
* 黄钻等级
*/
@JsonProperty("yellow_vip_level")
private String yellowVipLevel;
/**
* 等级
*/
private String level;
/**
* 是否是黄钻年费VIP,0否,1是
*/
@JsonProperty("is_yellow_year_vip")
private String isYellowYearVip;
}
上面的代码中,使用Jackson
将JSON
字符串序列化为QQUserInfo
实例对象的时候,将带有下划线的字段值映射到了对应的驼峰字段上,使用的Jackson
的@JsonProperty
注解来完成的。有了接口和实体类,我们自然需要写一个实现类,具体的信息获取代码都在实现类中。
package com.lemon.security.core.social.qq.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
import java.io.IOException;
/**
* 获取QQ用户信息的实现类
*
* @author jiangpingping
* @date 2019-02-05 11:34
*/
@Slf4j
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
/**
* Open ID的获取链接,它需要传递令牌,也就是OAuth协议的前五步获取到的数据访问令牌
*/
private static final String URL_GET_OPEN_ID = "https://graph.qq/oauth2.0/me?access_token=%s";
/**
* 获取用户信息的链接:https://graph.qq/user/get_user_info?access_token=YOUR_ACCESS_TOKEN&oauth_consumer_key=YOUR_APP_ID&openid=YOUR_OPENID
* 其中,access_token会被父类AbstractOAuth2ApiBinding处理,在请求之前,会被拼接到请求链接中,故这里删除即可
*/
private static final String URL_GET_USER_INFO = "https://graph.qq/user/get_user_info?oauth_consumer_key=%s&openid=%s";
/**
* appId是腾讯要求的应用ID,需要开发者去QQ互联上申请,对应的参数字段是oauth_consumer_key
*/
private String appId;
/**
* openId是腾讯对应用和用户之间的关系管理的一个参数,用户在一个应用的openID唯一
*/
private String openId;
private ObjectMapper objectMapper = new ObjectMapper();
public QQImpl(String accessToken, String appId) {
// 这里的父类构造方法传入两个参数,第二个参数的意思是在构造方法中构建restTemplate的时候,将accessToken作为请求参数集成到请求链接中
// 父类的默认构造也就是一个参数的构造,默认行为是将参数放到了请求头中,这个就和QQ的API接口要求的传参方式不一样了
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
this.appId = appId;
// 获取openId
String url = String.format(URL_GET_OPEN_ID, accessToken);
String result = getRestTemplate().getForObject(url, String.class);
// 返回的数据结构体为:callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
}
@Override
public QQUserInfo getUserInfo() {
String url = String.format(URL_GET_USER_INFO, appId, openId);
String result = getRestTemplate().getForObject(url, String.class);
log.info("获取到用户的信息为:{}", result);
try {
QQUserInfo userInfo = objectMapper.readValue(result, QQUserInfo.class);
// 这里需要将openId存储到userInfo中
userInfo.setOpenId(openId);
log.info("封装后的UserInfo为:{}", userInfo);
return userInfo;
} catch (IOException e) {
e.printStackTrace();
log.error("转换QQ用户信息失败:{}", e.getMessage());
throw new RuntimeException(e);
}
}
}
QQImpl
类中的注释写的很详细,读者一看就明白。这里还重点说明三点:
QQImpl
继承了AbstractOAuth2ApiBinding
,这在上一篇文章中也介绍了AbstractOAuth2ApiBinding
帮助我们完成了一些基础操作,方便我们快速开发。QQImpl
的构造方法中调用了父类AbstractOAuth2ApiBinding
的两个参数的构造方法,在父类的构造方法中,我们将第二个参数设置为TokenStrategy.ACCESS_TOKEN_PARAMETER
,这样在父类的构造方法中构建RestTemplate
对象的时候,就会将accessToken
放到请求参数中,如果调用一个参数的父类构造方法,那么它默认的行为是将accessToken
放到请求头中,这就和QQ
互联要求的请求方式不一样了。- 没有将
QQImpl
标注为Spring Bean
,这是因为Spring Bean
是单例的,这里的每一个用户应该对应一个QQImpl
对象。当用户选择QQ
登录的时候,就会去创建一个QQImpl
对象,在调用构造方法的时候,就会去事先设定好的链接获取该用户在应用中唯一的OpenID
,拿到OpenID
后就会调用getUserInfo
方法来获取用户信息。
二、开发QQServiceProvider
开发完获取用户的QQ
信息的接口后,那么接着开发QQServiceProvider
,OAuth2Operations
是不需要我们开发的,Spring Social
提供了OAuth2Template
,已经帮我们封装好了OAuth
协议规定的基础步骤,我们直接调用即可,在调用之前,需要配置好授权的URL
和获取Access Token
的URL
。
package com.lemon.security.core.social.qq.connect;
import com.lemon.security.core.social.qq.api.QQ;
import com.lemon.security.core.social.qq.api.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2Template;
/**
* QQ的Service Provider
*
* @author jiangpingping
* @date 2019-02-05 13:13
*/
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
/**
* 引导用户授权的URL,获取授权码
*/
private static final String URL_AUTHORIZE = "https://graph.qq/oauth2.0/authorize";
/**
* 获取令牌的URL
*/
private static final String URL_ACCESS_TOKEN = "https://graph.qq/oauth2.0/token";
private String appId;
public QQServiceProvider(String appId, String appSecret) {
// 使用Spring Social的默认的OAuth2Template
super(new OAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
this.appId = appId;
}
@Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken, appId);
}
}
QQServiceProvider
的代码编写还是很简单的,AbstractOAuth2ServiceProvider
用到的泛型是API
的接口类型,在这里配置了授权的URL
和获取Access Token
的URL
,然后调用AbstractOAuth2ServiceProvider
的构造方法就可以获得了Access Token
的值,OAuth
协议中规定的参数传递等步骤都由Spring Social
提供的OAuth2Template
来完成了。也许你有一个疑问,在OAuth
协议中,在获取授权和获取Access Token
的时候都会设置一个参数redirect_uri
,但是我们并没有设置这个参数啊?Spring Social
是如何帮助我们设置的呢?这里暂时不回答这个问题,请接着往下阅读,后面将会为您解释这个参数设置问题。至此,我们已经开发完了与第三方服务提供商相关的代码,也就是第一幅图的最右边需要的代码。
三、开发ConnectionFactory
从上一篇文章可知,Connection
是一个接口,它有一个实现类OAuth2Connection
,该实现类中封装了与用户相关的信息,这些信息,比如DisplayName
(显示名称),ProfileUrl
(主页地址),ImageUrl
(头像地址)等基本信息,这些信息是Spring Social
所规定的用户信息(固定字段),我们现在要做的就是将拿到的用户信息转换成OAuth2Connection
所封装的用户信息。生成Connection
实现类对象需要用到ConnectionFactory
工厂,而创建ConnectionFactory
对象就需要用到我们开发的QQServiceProvider
,还有一个ApiAdapter
实现类对象,前者我们已经开发好了,那么现在就需要开发ApiAdapter
的实现类,从ApiAdapter
这个名称可以看出,它就是一个适配器,负责将从第三方应用拿到的用户基础数据转换成OAuth2Connection
的封装的数据,但是进入ApiAdapter
的源码看到,我们并不是直接将数据转换成OAuth2Connection
封装的属性值,而是设置到ConnectionValues
中,后期的转换工作交给Spring Social
来完成。分析到这里,我们可以开始编写ApiAdapter
实现类的代码了,具体代码如下所示:
package com.lemon.security.core.social.qq.connect;
import com.lemon.security.core.social.qq.api.QQ;
import com.lemon.security.core.social.qq.api.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;
/**
* @author jiangpingping
* @date 2019-02-05 15:05
*/
public class QQAdapter implements ApiAdapter<QQ> {
/**
* 这个方法用来判断QQ服务是否可用
*
* @param api API接口
* @return 是否可用
*/
@Override
public boolean test(QQ api) {
return true;
}
/**
* 将API中获取到的用户信息转换成创建Connection所需的值
*
* @param api 用户信息获取API
* @param values 创建Connection所需的值
*/
@Override
public void setConnectionValues(QQ api, ConnectionValues values) {
QQUserInfo userInfo = api.getUserInfo();
values.setDisplayName(userInfo.getNickname());
values.setImageUrl(userInfo.getFigureUrlQq40());
// QQ用户信息接口没有主页这个值
values.setProfileUrl(null);
values.setProviderUserId(userInfo.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQ api) {
return null;
}
@Override
public void updateStatus(QQ api, String message) {
}
}
这里主要是编写了setConnectionValues
方法的代码,将从QQ
获取到的数据封装到了ConnectionValues
中。现在有了QQServiceProvider
和QQAdapter
,那么就可以来开发ConnectionFactory
的实现类了,这里贴出代码:
package com.lemon.security.core.social.qq.connect;
import com.lemon.security.core.social.qq.api.QQ;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
/**
* @author jiangpingping
* @date 2019-02-05 17:15
*/
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
/**
* QQ Connection Factory的构造方法
*
* @param providerId 第三方服务提供商的ID,如facebook,qq,wechat
* @param appId 第三方服务提供商给予的应用ID
* @param appSecret 第三方服务提供商给予的应用Secret
*/
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
}
}
写到这里,主要的内容算是写完了,其中UsersConnectionRepository
这一块内容封装了对UserConnection
表的基础操作,是不需要我们开发的,我们要做的就是将JdbcUsersConnectionRepository
配置进来即可,主要代码如下:
package com.lemon.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 jiangpingping
* @date 2019-02-05 17:23
*/
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
private final DataSource dataSource;
@Autowired
public SocialConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
// 创建一个JDBC连接仓库,需要dataSource、connectionFactory加载器,对存到数据库中的加密策略,这里选择不做加密,信息原样存入数据库
// 这里创建的JdbcUsersConnectionRepository可以设置UserConnection表的前缀
return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
}
@Bean
public SpringSocialConfigurer lemonSocialSecurityConfig() {
return new SpringSocialConfigurer();
}
}
这里使用注解@EnableSocial
启用社交登录,并配置了JdbcUsersConnectionRepository
,代码中Encryptors.noOpText()
表示将用户信息以明文的方式存储到数据库中,也可以以加密的方式进行存储。并将SpringSocialConfigurer
的实例对象交给了Spring
来管理。最后将SpringSocialConfigurer
的对象注入到了BrowserSecurityConfig
中,并apply
到配置代码中(详情请关注码云上的代码chapter014),如下所示:
@Autowired
private SpringSocialConfigurer lemonSocialSecurityConfig;
http.apply(lemonSocialSecurityConfig);
现在需要写一些基础配置类,比如appId
、appSecret
以及providerId
等,这些内容必须支持开发者自定义,因为每个开发者的appId
、appSecret
肯定是不一样的,providerId
可以提供一个默认值,但是也得提供一个可配置的值。接下来写配置方面的内容。
四、开发基础配置类
我们开发一个配置类来接收来自配置文件中的值,定义配置类名称为QQProperties
,该类继承SocialProperties
,在SocialProperties
中,已经存在了appId
和appSecret
,QQProperties
继承了SocialProperties
,就相当于已经有了appId
和appSecret
两个属性,再添加一个providerId
属性即可,且设置默认值为qq
,代码如下:
package com.lemon.security.core.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.autoconfigure.social.SocialProperties;
/**
* @author jiangpingping
* @date 2019-02-05 17:56
*/
@Getter
@Setter
public class QQProperties extends SocialProperties {
private String providerId = "qq";
}
由于我们当前开发的仅仅是QQ
登录,后面还会开发微信登录,这两者都是属于第三方登录,所以我们再封装一层属性,写一个SocialProperties
类,代码如下:
package com.lemon.security.core.properties;
import lombok.Getter;
import lombok.Setter;
/**
* @author jiangpingping
* @date 2019-02-05 17:59
*/
@Getter
@Setter
public class SocialProperties {
private QQProperties qq = new QQProperties();
}
然后再将代码private SocialProperties social = new SocialProperties();
加入到SecurityProperties
中,完整代码如下:
package com.lemon.security.core.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author lemon
* @date 2018/4/5 下午3:08
*/
@Data
@ConfigurationProperties(prefix = "com.lemon.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
private SocialProperties social = new SocialProperties();
}
这样设置以后,我们就可以在application.properties
中设置appId
、appSecret
以及providerId
了,例如:
com.lemon.security.social.qq.appId=xxxxxx
com.lemon.security.social.qq.appSecret=xxxxxx
com.lemon.security.social.qq.providerId=xxxxxx
以上最后一个字段名称appId
可以替换为app-id
,appSecret
和providerId
同理,Spring
读取配置文件是支持横杠转换为驼峰形式的参数。
我们还需要写一个自动配置类,当检测到用户在application.properties
中配置了属性com.lemon.security.social.qq.appId
后,就应该将QQConnectionFactory
实例化,并交给Spring
来管理。也就是说,只要开发者开发的系统中配置了属性com.lemon.security.social.qq.appId
后,说明该系统就支持QQ
登录,那么就应该实例化QQConnectionFactory
,且该工厂类是单例的,负责创建与用户信息相关的Connection
。自动配置类的代码如下所示:
package com.lemon.security.core.social.qq.config;
import com.lemon.security.core.properties.QQProperties;
import com.lemon.security.core.properties.SecurityProperties;
import com.lemon.security.core.social.qq.connect.QQConnectionFactory;
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;
/**
* @author jiangpingping
* @date 2019-02-05 18:03
*/
@Configuration
@ConditionalOnProperty(prefix = "com.lemon.security.social.qq", name = "app-id")
public class QQAutoConfiguration extends SocialAutoConfigurerAdapter {
private final SecurityProperties securityProperties;
@Autowired
public QQAutoConfiguration(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
@Override
protected ConnectionFactory<?> createConnectionFactory() {
QQProperties qqProperties = securityProperties.getSocial().getQq();
return new QQConnectionFactory(qqProperties.getProviderId(), qqProperties.getAppId(), qqProperties.getAppSecret());
}
}
自动配置类写完了,整体的代码算是基本完成了。我们现在在lemon-security-browser
项目中的默认登录页面后面加上QQ
登录,页面代码如下:
<h2>社交登录</h2>
<!-- /auth是类SocialAuthenticationFilter规定的,/qq是providerId -->
<a href="/auth/qq"><img src="http://qzonestyle.gtimg/qzone/vas/opensns/res/img/Connect_logo_3.png"></a>
页面显示的效果图如下:
这里的QQ
登录按钮地址为什么是/auth/qq
?这是因为Spring Social
对社交登录的拦截地址做了默认值,它拦截的请求地址就是/auth
,而后面的/qq
则是providerId
,这是默认规则。具体的默认定义可以去看Spring Social
的类SocialAuthenticationFilter
,它源代码最底部有一个常量DEFAULT_FILTER_PROCESSES_URL
,它的值就是/auth
,也就是说该拦截器会拦截/auth
的请求,并对其进行验证。现在我们启动项目,来验证一下QQ
登录的功能是否完善。我们在8080
端口启动demo
项目,然后直接访问默认的登录页面,并点击QQ
登录,我们跳转到了QQ
登录授权页面,如下所示:
我们发现回调地址是非法的,我们仔细观察地址栏的链接,我把它拷贝到这里:
https://graph.qq/oauth2.0/show?which=error&display=pc&error=100010&client_id=101547587&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fqq&state=e567fd76-6b53-4572-84e5-8a0e93defb47
从上面的地址可以看出来,redirect_uri
参数我们在之前并没有设置,这里很明显是Spring Social
帮助我们完成了这部分操作,这也就回答了之前遗留下来为什么不用我们自己设置redirect_uri
参数的问题。现在一起来分析一下这个redirect_uri
参数,它的值如下所示:
http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fqq
这里的回调地址是经过编码后的地址,还原后就是:
http://localhost:8080/auth/qq
这地址不就是我们设置的QQ
登录的地址吗?对的,回调地址就是这个QQ
登录地址。但是为什么会出现这种“回调地址非法”
的问题呢?原因是因为回调地址和我们在QQ
互联平台上创建的应用的时候设置的回调地址不一致导致的,我在开发这一块的时候,设置的回调地址是http://www.itlemon/auth/qq
,两者是不一致的,所以就会提示回调地址非法,由于我设置的http协议的回调地址,所以默认访问的是应用所在服务器的80
端口,所以我们需要将demo
项目的启动端口改成80
端口,然后再借助软件switchhosts
将本地www.itlemon
指向127.0.0.1
,这样的话,访问http://www.itlemon
就会映射到本地的应用上来,准备工作做好以后,我们再次启动项目,访问登录页面http://www.itlemon/login.html
,点击QQ
登录,跳转页面如下图所示:
这就说明正确地到达了QQ
登录授权页面了,扫码就可以进行登录操作了。我现在扫码来授权一下,看看接下来会发生什么,扫码后如下图所示:
我明明授权了,为什么不是直接展示用户认证信息,而是出现这种未授权的信息呢?还有一个问题,那就是社交登录默认拦截的是/auth
,providerId
也默认是qq
,我该如何来实现自定义社交登录拦截地址呢?那么接下来我们一起来解决这两个问题。
五、解决遗留的两个问题
1)解决第一个问题
首先解决自定义配置社交登录拦截路径的问题,我们在配置类SocialConfig
中实例化了一个SpringSocialConfigurer
的Spring Bean
,在这个Bean
中直接返回的是SpringSocialConfigurer
的实例对象,在这个类的configure
方法中,如下所示:
@Override
public void configure(HttpSecurity http) throws Exception {
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
UsersConnectionRepository usersConnectionRepository = getDependency(applicationContext, UsersConnectionRepository.class);
SocialAuthenticationServiceLocator authServiceLocator = getDependency(applicationContext, SocialAuthenticationServiceLocator.class);
SocialUserDetailsService socialUsersDetailsService = getDependency(applicationContext, SocialUserDetailsService.class);
SocialAuthenticationFilter filter = new SocialAuthenticationFilter(
http.getSharedObject(AuthenticationManager.class),
userIdSource != null ? userIdSource : new AuthenticationNameUserIdSource(),
usersConnectionRepository,
authServiceLocator);
RememberMeServices rememberMe = http.getSharedObject(RememberMeServices.class);
if (rememberMe != null) {
filter.setRememberMeServices(rememberMe);
}
if (postLoginUrl != null) {
filter.setPostLoginUrl(postLoginUrl);
filter.setAlwaysUsePostLoginUrl(alwaysUsePostLoginUrl);
}
if (postFailureUrl != null) {
filter.setPostFailureUrl(postFailureUrl);
}
if (signupUrl != null) {
filter.setSignupUrl(signupUrl);
}
if (connectionAddedRedirectUrl != null) {
filter.setConnectionAddedRedirectUrl(connectionAddedRedirectUrl);
}
if (defaultFailureUrl != null) {
filter.setDefaultFailureUrl(defaultFailureUrl);
}
http.authenticationProvider(
new SocialAuthenticationProvider(usersConnectionRepository, socialUsersDetailsService))
.addFilterBefore(postProcess(filter), AbstractPreAuthenticatedProcessingFilter.class);
}
在这个方法中,首先创建了一个SocialAuthenticationFilter
对象,最后将其加到了AbstractPreAuthenticatedProcessingFilter
这个过滤器之前,在加入之前,调用了postProcess
方法,而这个postProcess
方法是可以被覆盖掉的,在这里我们可以对SocialAuthenticationFilter
进行个性化处理,在个性化处理的过程中将社交登录的拦截路径设置到其中,我们在项目lemon-security-core的social
包下开发一个配置类,来覆盖一下postProcess
方法,代码如下:
package com.lemon.security.core.social;
import lombok.AllArgsConstructor;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
/**
* 配置社交登录的拦截路径
*
* @author jiangpingping
* @date 2019-02-12 19:33
*/
@AllArgsConstructor
public class LemonSpringSocialConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;
@Override
@SuppressWarnings("unchecked")
protected <T> T postProcess(T object) {
// 获取父类的处理结果
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
写完这个代码以后,我们在SocialConfig
类中就不能在实例化SpringSocialConfigurer
了,而是要实例化我们自己写的那个LemonSpringSocialConfigurer
类了,在实例化之前,需要修改一些配置,SocialProperties
类修改后代码如下:
package com.lemon.security.core.properties;
import lombok.Getter;
import lombok.Setter;
/**
* @author jiangpingping
* @date 2019-02-05 17:59
*/
@Getter
@Setter
public class SocialProperties {
/**
* 这个属性是为了设置自定义社交登录拦截路径的
*/
private String filterProcessesUrl = "/auth";
private QQProperties qq = new QQProperties();
}
那么修改后的SocialConfig
类如下所示:
package com.lemon.security.core.social;
import com.lemon.security.core.properties.SecurityProperties;
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 jiangpingping
* @date 2019-02-05 17:23
*/
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
private final DataSource dataSource;
private final SecurityProperties securityProperties;
@Autowired
public SocialConfig(DataSource dataSource, SecurityProperties securityProperties) {
this.dataSource = dataSource;
this.securityProperties = securityProperties;
}
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
// 创建一个JDBC连接仓库,需要dataSource、connectionFactory加载器,对存到数据库中的加密策略,这里选择不做加密,信息原样存入数据库
// 这里创建的JdbcUsersConnectionRepository可以设置UserConnection表的前缀
return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
}
@Bean
public SpringSocialConfigurer lemonSocialSecurityConfig() {
String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
return new LemonSpringSocialConfigurer(filterProcessesUrl);
}
}
到这里,我们就解决了不能自定义拦截社交登录的路径问题了,但是要注意的是,当我们没有使用默认的/auth
拦截路径的时候,在配置文件中配置的路径一定要和在QQ
互联网站上创建的应用配置的回调地址一致,否则还会被提示“回调地址非法”
的错误。在这里,我把QQ
互联上登记的应用的回调地址改成了http://www.itlemon/authentication/qq
,所以我需要在demo项目中添加一个配置com.lemon.security.social.filterProcessesUrl=/authentication
,并且将默认的登录页面QQ
登录按钮地址改成了/authentication/qq
。
2)解决第二个问题
使用手机授权登录以后,为什么会出现这个提示:
我们查看日志可以知道,我们在手机上点击登录以后,页面自动跳转到http://www.itlemon/signin
这个链接上,因为我们没有对这个链接进行任何配置,所以默认需要认证后才可以访问,但是我们刚刚QQ登录就是一个授权登录行为,但是授权后却没有进入到系统中,还被系统拦截要求登录认证,这就说明在走OAuth认证过程中出现了问题,然后默认跳转到这个链接上进行重新认证,所以就出现了需要身份认证的提示。但是为什么会自动跳转到/signin
这个链接上呢?这就需要我们到Spring Social
的相关源码中找原因,在找原因之前,我们一起来分析一下Spring Social
集成QQ
登录的主要流程,熟悉流程之后,找原因也就方便很多了,这里贴出流程图如下所示:
类似于用户名密码、手机登录,这里的QQ
登录的核心原理是一模一样的,只是多了一点OAuth
的流程,分步骤讲解如下。
- 当用户点击
QQ
登录按钮的时候,链接/authentication/qq
会被SocialAuthenticationFilter
所拦截,该过滤器的内部获取了一个SocialAuthenticationService
实现类对象,默认是OAuth2AuthenticationService
,它会调用我们自己写的QQConnectionFactory
,而QQConnectionFactory
里有QQServiceProvider
,QQServiceProvider
里有OAuth2Template
来帮助我们完成OAuth
的基础步骤并拿到QQ
用户数据。 - 拿到数据以后,也就是生成了
Connection
以后,就会拿着这个Connection
数据来封装一个SocialAuthenticationToken
对象,并将这个对象标记为“未认证”
。 - 进一步将
SocialAuthenticationToken
传递到了AuthenticationManager
中,AuthenticationManager
会根据传入的Token
类型找到合适的AuthenticationProvider
来处理它,这里就会找到SocialAuthenticationProvider
来处理它,而SocialAuthenticationProvider
就会调用UserConnectionRepository
来从业务系统的数据库中来查找业务系统的用户。 - 查找业务系统的用户过程实际是
UserConnectionRepository
调用我们自己写的UserDetailService
的实现类(这里的实现类由于加入了第三方登录,已经进行了简单修改,这里不做介绍,读者可以看案例中的代码)来完成的,找到用户以后(找不到的情况待会详细说明,这里仅仅假设可以找到业务系统中的用户),将封装成SocialUserDetails
,并设置为“已认证”
,将认证结果存储到SecurityContext
中。
这就是Spring Social
使用第三方服务提供商存储的用户信息进行认证的一个核心原理,和使用用户名和密码的方式唯一的区别是,用户名密码认证的数据来源是用户填写的登录表单,而QQ
登录的数据则来源于QQ
服务器,其他的核心步骤都是一模一样的。后面讲解的微信登录原理也是一样的。
分析完了Spring Social
开发第三方登录的原理以后,我们在源码中打断点,来找一下究竟是在认证过程中走OAuth
步骤中的哪一步出现了问题,导致链接跳转到了http://www.itlemon/signin
上。我们依次在上图中的各个类或者接口的实现类的关键步骤上打断点,我们依次打断点,而不是一次性打完,我们跟着代码走,然后一步一步打断点。
1)在SocialAuthenticationFilter类上打断点
我们进入到类SocialAuthenticationFilter
中,然后在其attemptAuthentication
方法合适位置打断点,如下图所示:
我们来分析一下上面的代码,第一个断点出,首先根据请求判断用户是否拒绝授权,如果用户拒绝授权,那么将抛出一个异常,紧接着封装一个Authentication
实现类对象,暂时为null
,第二个断点,其内部是从一个Map
中拿到ProviderId
,所以拿到的结果是一个包含qq
的Set
集合,第三个断点是从请求中获取到ProviderId
,我们的请求链接是/authentication/qq
,所以拿到的结果也是qq
,具体里面的实现逻辑也很简答,读者跟进去一看便知。紧接着就是一个判断,判断ProviderId
是否为空,判断从请求中获取到的ProviderId
是否为空,并且两者是否包含关系,如果都满足的话,那么该请求就是一个第三方登录认证的请求。第四个断点是获取一个SocialAuthenticationService
对象,第六个断点是开始尝试走认证流程,这个断点我们需要进入到方法中看一看。
上图中第一个断点是获取Token
,这个Token
是SocialAuthenticationToken
的对象,是认证过程中的数据载体,而不是我们之前所说的访问令牌Access Token
,这一点要注意。第一个断点我们需要进入到其中进行分析。第二个断点是从SecurityContext
中获取认证信息,以用来判断是否已经认证过了,如果没有认证,将进入到第三个断点方法中进行认证,第三个断点我们也需要进入到其中进行分析。首先来分析第一个断点:
2)在OAuth2AuthenticationService类上打断点
我们进入到的是类OAuth2AuthenticationService
的getAuthToken
方法,该方法首先判断请求中是否带参数code
,我们都很清楚,在OAuth2
协议中,code
参数是用户授权后才能拿到,也就说在引导用户授权之前,是没有code
参数的,用户同意授权之后,会返回code
给我们的应用,然后我们的应用拿着code
去请求第三方授权服务器换取访问令牌Access Token
(如果对协议这一块不了解的,可以查看我前一篇文章),如果我们第一次访问,那么就就有code
这个值,那么它就会抛出一个异常,捕获到异常之后将我们的请求重定向到QQ
授权页面,等用户授权后,将会重定向到我们一开始的那个/authentication/qq
上,再次被拦截后,走到这里,此时链接上是带有code
值,这个时候就会走到else if
块中,这时候,就会拿到我们的code
去申请令牌,exchangeForAccess
就是OAuth2Template
的方法,里面封装申请令牌的必要参数并发送post
请求获取令牌,拿到令牌封装的AccessGrant
对象之后,就通过ConnectionFactory
去调用QQProviderService
来创建Connection
实现类对象,最后将这个Connection
数据封装成SocialAuthenticationToken
去接着走下面的认证流程。我们从代码中分析到,当我们点击QQ
登录的时候,走到这个类的第一个if
代码块就结束了,就进入了QQ
授权页面,然后我们扫码授权之后,就走到else if
代码块继续走下面的认证流程,这个时候,就与OAuth
协议没有关系了。
我们之前分析到的问题是点击授权后跳到了http://www.itlemon/signin
上,然后被Spring Security
拦截,显示没有授权,说明并没有走接下来的认证流程了,而是在走OAuth
的流程就出现了问题。好了,我们不接着往下打断点了,就暂时打到这里,我们来启动项目,扫码授权,看看到底会出现上面问题。
我们点击QQ
登录后,请求到达了这里,目前页面还没有跳到QQ
授权页面,如下图所示:
我们让代码继续走,这时候,网页已经跳转到了授权页面。我们扫码授权,然后再次被SocialAuthenticationFilter
拦截并走到getAuthToken
方法中,这次一步一步走,看看会发生什么,授权后,此时code
就带有值了,如下图所示:
我们接着往下走,直到走到拿着code
去换取Access Token
并封装AccessGrant
的时候,发现这一步发生了异常,也就是直接跳到了catch
块中,我们一起看看到底发生了什么异常:
从图中可以看出,报的错是:Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html]
,错误中也就是说没有找到合适的Converter
来转换从QQ
服务器返回的内容,也就是说QQ
服务器返回来的内容无法被Spring Social
来转换,那么我们来看看Spring Social
默认的转换器和QQ
返回来的内容都是什么。
我们进入到exchangeForAccess
方法中,如下图所示:
首先是封装OAuth
协议规定的参数,然后就是发送了一个POST
请求,我们继续进入到postForAccessGrant
方法中一探究竟,它的代码只有一行,如下所示:
return extractAccessGrant(getRestTemplate().postForObject(accessTokenUrl, parameters, Map.class));
它首先是获取了RestTemplate
对象,RestTemplate
都是以JSON
交互数据的,也就是说它接受的类型是application/json
类型的数据,并将接收到的数据封装到一个Map
集合中。最后从Map
中提取access_token
,scope
和refresh_token
来封装AccessGrant
对象,也就是说,Spring Social
希望返回的是一个JSON
,但QQ
服务器真正返回的确实text/html
,所以在这里转换失败了,我紧接着QQ
互联文档看看QQ
服务器返回的数据格式,如下所示:access_token=FE04******CCE2&expires_in=7776000&refresh_token=88E4******BE14
,很明显,这不是一个JSON
数据。
我们还是回到OAuth2AuthenticationService
类的getAuthToken
方法里,那么在获取Access Token
的时候发生了数据转换异常,那么就会进入到getAuthToken的catch
代码块中,那么getToken
方法就会返回null
,那么SocialAuthenticationFilter
的attemptAuthService
方法的第一行代码就返回了null
,那么整个attemptAuthService
方法就会返回null
,那么该类的attemptAuthentication
方法就会抛出SocialAuthenticationException
的异常,那么接着就会进入到AbstractAuthenticationProcessingFilter
类的doFilter
方法中,并被其catch
代码块捕获,代码块中的代码如下如所示:
我们进入到unsuccessfulAuthentication
方法中,代码如下:
上图的最后一行代码是失败处理器在处理当前请求,我们回到SocialAuthenticationFilter
类中,SocialAuthenticationFilter
类的构造方法设置了失败处理器,我们一起来看看构造方法:
从断点出可以看出,DEFAULT_FAILURE_URL
的值正是“/signin”
,这也就解释了为什么我们在QQ
授权页面扫码授权之后,跳转到了“/signin”
,这是因为我们在获取Access Token
的过程中转换数据发生了异常,然后被SocialAuthenticationFilter
类的失败处理器处理了,重定向到了“/signin”
上,这也就导致了后面我们项目拦截了该请求,出现了如下画面:
我们通过分析源码,通过打断点的方式,找到了问题的原因所在,那么我们现在开始着手解决这个问题吧。在处理之前,我们一起来看看类OAuth2Template的postForAccessGrant
方法,它代码里通过调用getRestTemplate
方法来获取了RestTemplate
对象,那么我们进入到该方法中,如下所示:
在创建RestTemplate
对象的时候,我们从代码中可以看出,该方法仅仅只添加了三个数据转换器,分别是:FormHttpMessageConverter
、FormMapHttpMessageConverter
、MappingJackson2HttpMessageConverter
。前两个只能处理application/x-www-form-urlencoded
类型的数据和multipart/form-data
类型的数据的,而第三个是处理application/json
类型的数据的,这是不符合我们要求的,那么我们需要在写一个方法来覆盖它,我们拿到从父类创建好的RestTemplate
中添加一个StringHttpMessageConverter
,该Converter
就可以处理ContentType
为text/html
的数据,因为QQ
服务器返回来的数据形式是access_token=FE04******CCE2&expires_in=7776000&refresh_token=88E4******BE14
,它并不是JSON
数据,那么我们还需要重写postForAccessGrant
方法,这样我们就可以自定义处理access_token=FE04******CCE2&expires_in=7776000&refresh_token=88E4******BE14
类型的数据了,而不是直接将QQ
服务器返回来的数据当做JSON
来处理。我们在包connect
下再写一个类QQOAuth2Template
,代码如下所示:
package com.lemon.security.core.social.qq.connect;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
/**
* @author jiangpingping
* @date 2019-02-17 00:03
*/
@Slf4j
public class QQOAuth2Template extends OAuth2Template {
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
// 因为OAuth2Template的exchangeCredentialsForAccess方法,在封装OAuth协议的时候,默认不会带上client_id和client_secret
// 也就是说默认的useParametersForClientAuthentication值为false,所以这里需要改成true
setUseParametersForClientAuthentication(true);
}
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
// 添加一个StringHttpMessageConverter,他能处理text/html类型的数据
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
String responseString = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
log.info("获取access token的响应为:{}", responseString);
// QQ服务器返回的数据类型为access_token=FE04******CCE2&expires_in=7776000&refresh_token=88E4******BE14
String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseString, "&");
// 分割数据
String accessToken = StringUtils.substringAfterLast(items[0], "=");
Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
String refreshToken = StringUtils.substringAfterLast(items[2], "=");
// 封装AccessGrant对象
return new AccessGrant(accessToken, null, refreshToken, expiresIn);
}
}
上述代码写完以后,我们还需要修改一下QQServiceProvider
的部分代码,在QQServiceProvider
的构造方法中,如下所示:
public QQServiceProvider(String appId, String appSecret) {
// 使用Spring Social的默认的OAuth2Template
super(new OAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
this.appId = appId;
}
现在需要修改为:
public QQServiceProvider(String appId, String appSecret) {
// 不能再使用Spring Social的默认的OAuth2Template,而需要我们自定义的QQOAuth2Template
super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
this.appId = appId;
}
当然,加入了社交登录以后,我们还需要重构一下UserDetailsServiceImpl
类,这个类主要是负责从数据库读取用户信息来封装UserDetails
对象,这里修改如下所示:
package com.lemon.security.web.authentication;
import lombok.extern.slf4j.Slf4j;
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.bcrypt.BCryptPasswordEncoder;
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;
/**
* @author jiangpingping
* @date 2019-02-05 17:53
*/
@Component
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService, SocialUserDetailsService {
private PasswordEncoder passwordEncoder;
public UserDetailsServiceImpl() {
this.passwordEncoder = new BCryptPasswordEncoder();
}
@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 = "123456";
String encodedPassword = passwordEncoder.encode(password);
log.info("加密后的密码为: {}", encodedPassword);
// 这里查询该账户是否过期,这里使用固定代码,假设没有过期
boolean accountNonExpired = true;
// 这里查询该账户被删除,假设没有被删除
boolean enabled = true;
// 这里查询该账户认证是否过期,假设没有过期
boolean credentialsNonExpired = true;
// 查询该账户是否被锁定,假设没有被锁定
boolean accountNonLocked = true;
// 关于密码的加密,应该是在创建用户的时候进行的,这里仅仅是举例模拟
return new SocialUser(userId, encodedPassword,
enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
我们再次重启demo
项目,点击QQ
登录,然后扫码授权,这时候,我们发现,又发生了刚才的那种情况:
这是为什么呢?我们观察项目的控制台,发现控制台打印出来的日志提示,我们的请求再次被重定向到了http://www.itlemon/signup
上,这很明显是跳转到了一个注册的链接上,这也就让我们回想起以前使用QQ
登录一个新的网站的时候,网站的大部分操作都是在我们授权之后,跳转到了一个需要我们绑定该网站账号密码或者注册的页面,那么这个问题该如何解决呢?请关注我的下一篇文章《Spring Security技术栈开发企业级认证与授权(十五)解决Spring Social集成QQ登录后的注册问题》。
Spring Security技术栈开发企业级认证与授权系列文章列表:
Spring Security技术栈学习笔记(一)环境搭建
Spring Security技术栈学习笔记(二)RESTful API详解
Spring Security技术栈学习笔记(三)表单校验以及自定义校验注解开发
Spring Security技术栈学习笔记(四)RESTful API服务异常处理
Spring Security技术栈学习笔记(五)使用Filter、Interceptor和AOP拦截REST服务
Spring Security技术栈学习笔记(六)使用REST方式处理文件服务
Spring Security技术栈学习笔记(七)使用Swagger自动生成API文档
Spring Security技术栈学习笔记(八)Spring Security的基本运行原理与个性化登录实现
Spring Security技术栈学习笔记(九)开发图形验证码接口
Spring Security技术栈学习笔记(十)开发记住我功能
Spring Security技术栈学习笔记(十一)开发短信验证码登录
Spring Security技术栈学习笔记(十二)将短信验证码验证方式集成到Spring Security
Spring Security技术栈学习笔记(十三)Spring Social集成第三方登录验证开发流程介绍
Spring Security技术栈学习笔记(十四)使用Spring Social集成QQ登录验证方式
Spring Security技术栈学习笔记(十五)解决Spring Social集成QQ登录后的注册问题
Spring Security技术栈学习笔记(十六)使用Spring Social集成微信登录验证方式
示例代码下载地址:
项目已经上传到码云,欢迎下载,内容所在文件夹为
chapter014
。
更多干货分享,欢迎关注我的微信公众号:爪哇论剑(微信号:itlemon)
版权声明:本文标题:Spring Security技术栈学习笔记(十四)使用Spring Social集成QQ登录验证方式 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/xitong/1726185472a1059471.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论