Spring Social是一个帮助我们连接社交媒体平台,方便在我们自己的应用上开发第三方登录认证等功能的Spring 类库。其中比较核心的类和接口,如下图所示,我们来一一解析。
首先我们简单回顾一下OAuth2,OAuth2主要包含两部分内容:认证和鉴权。
如果你对这部分内容,还不是很熟悉,先回看我的上一篇文章。请结合下面的这张图理解后面的文字。
首先在实现OAuth2登录认证的过程中,有多次我们自己开发的应用和社交媒体平台之间的的请求和响应。所以我们需要封装一个类用来处理标准的OAuth2认证专用的HTTP工具类,这个可以说是最重要的工作,Spring Security已经帮我们提供了OAuth2Operations接口,其默认的实现类是OAuth2Template,根据不同的平台的实现差异我们可能会需要自己来实现(微调)。认证过程中所有与OAuth2认证服务器交互的工作就全交给OAuth2Operations,最后返回给我们一个AccessToken。
对于开发者来说,只要将以上四个属性的值告诉OAuth2Operations。只要服务提供商是严格按照OAuth2标准开发的认证服务,剩下的与认证服务器交互的过程,我们就不需要处理了。
当我们获得了AccessToken之后,就有权限请求OAuth2资源服务器里面的资源了。各个社交媒体平台根据用户及业务不同提供的接口完全不同,这时我们需要用到RestTemplate,通用的HTTP工具类处理请求与响应。从图中可以看到,处理各种数据格式JSON、XML的类库,RestTemplate会自行根据环境判断使用哪一个。
那既然各个平台的业务接口各不相同,我们当然要自定义开发不同的接口实现APIImpl。此时我们应该需要一个统一的父类,包含accessToken和RestTemplate,这样我们的自定义接口实现就可以通过继承这个类获得并使用accessToken和RestTemplate。这个统一的父类叫做AbstractOAuth2Binding。它还帮助我们实现HTTP请求的参数携带,以及请求结果到对象的反序列化工作等。
至此,OAuth2Operations 和 自定义接口实现APIImpl,一个负责认证流程请求响应,一个负责资源请求响应。二者统一被封装为ServiceProvider-服务提供商。
通过实现上面的代码中的接口,我们自己的应用与社交媒体平台(服务提供商)的HTTP交互过程就已经可以被全部支持了。但是开发社交媒体登陆还有一个很重要的步骤就是:判定社交媒体平台响应的用户信息与我们自己的应用用户之间的关系。我们用一张数据库表来表示这个关系,而且必须是这张表(Spring Social专用,在spring-social-core包里面可以找到):
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(自开发应用的用户唯一标识),provider(服务提供商,社交媒体平台唯一标识),providerUserId (服务提供商用户的唯一标识)。通过这三个字段体现自开发应用的用户与服务提供商用户之间的关系,从而判定服务提供商的用户是否可以通过OAuth2认证登录我们的应用。(这张表里面的数据,是通过注册或者绑定操作加入进去的,与认证、鉴权过程无关)
通过实现上面代码中的接口,我们就可以拿到userId,我们自己开发的应用的用户的唯一标识。也表示利用社交媒体用户登录我们自己开发的应用成功了。但是,还有一个问题没有解决,你是登陆成功了,但是不意味着你可以访问本地应用中的所有资源。所以,我们根据userId查找当前用户信息UserDetails,并为他授权。
在我们之前的使用用户名密码登陆的案例中,是通过实现UserDetailsService和UserDetails接口来实现的。在社交媒体登录过程中,我们需要实现的接口是SocialUserDetailsService和SocialUserDetails。其实实现原理是一样的,就是用用户的唯一标识userId,加载该用户角色的权限信息。至此,Spring Security就知道了该用户的权限信息,可以有效的控制其访问权限。
以上过程的核心流程代码,都在SocialAuthenticationProvider中的authenticate方法中定义
Spring Social自动配置会在过滤器链中加入一个SocialAuthenticationFilter过滤器,该过滤器拦截社交媒体登录请求。
SocialAuthenticationFilter过滤器拦截的社交媒体登录请求的地址是{filterProcessesUrl}/{providerId}。filterProcessesUrl的默认值是“/auth”,如果你的服务提供商providerId(自定义)是github,那么你的社交媒体登录按钮请求的地址就应该是“/auth/github”,当然这两个值我们都可以修改。
要说明的是{filterProcessesUrl}/{providerId}在Spring Social既是认证请求的地址,也是服务提供商回调的地址。当用户点击"github登录"按钮,此时访问/{filterProcessesUrl}/{providerId}被拦截,此时用户没有被认证通过,所以跳转到GitHub授权页面(authorizeUrl)上,用户输入用户密码授权,在浏览器跳回到本地应用,仍然回到/{filterProcessesUrl}/{providerId}再次被拦截。
首先要检测用户是否授权使用第三方平台用户信息,如果没授权就直接抛出异常。如果用户授权了,就去执行OAuth2一系列的请求响应,获取授权码、AccessToken、Connection用户信息。这个过程代码在OAuth2AuthenticationService中被定义。
doAuthentication中授权过程,参考1.3、1.4小节内容。如果授权失败(该社交平台用户在本地应用中没有对应的用户),则跳转到signUpUrl。该页面是将本系统用户和“服务提供商”用户进行关系绑定页面。
注意:Spring Social实现的OAuth2认证鉴权流程中,使用到了session(如上图中的sessionStrategy代码)。所以当你的应用是一个无状态应用时,需要对Spring Social进行一定程度的改造。但是笔者从来没这么做过。简单的做法就是:使用session开发有状态应用,并且session保存的状态信息交给redis集中管理;或者开发无状态应用之前,确定该应用不需要社交媒体登录功能,比如某企业内网应用。
要想使用QQ登陆的功能,首先你必须是腾讯开发者。腾讯搞了一大堆的开放平台,有点乱。如果你还不是腾讯开发者,先去QQ互联网站https://connect.qq.com注册一下开发者。
以上所填信息真实完整,通常最多5个工作日即可审核完成,审核结果通过邮件通知。审核之后才能创建应用。下面的状态是开发者资格审核中的状态。
注意:本文旨在教大家如何创建一个QQ互联的测试应用,目的是为了学习。所有的填的内容都是以满足本地测试为目的。 如果你真的需要为一个生产环境开发QQ互联登录动能,请先准备好如下内容:
下面我们开始申请,如果您和我一样就是创建一个测试应用,我们什么也不用准备,现在就开始吧。但是测试账号只能使用当前的创建应用的QQ用户进行登录相关测试,这对我们来说就已经足够了。
现在这方法申请测试账号已经不好用了。目前实现QQ还是要申请真实的账号进行测试。域名和网站介绍相关内容都要正规填写,不能填写“127.0.0.1”之类的了。
在开发者资格审核通过之后,再次登录QQ互联网站,创建网站应用。
弹出框内选择创建网站应用
创建应用的应用名称和应用简介,随便填写一下即可,最后结果:审核不通过。但是不耽误我们测试使用。
最重要的是填写网站域名和回调地址,其他两项随便填。注意一件事情:大家看我的图中,回调地址写的是“/authqq/callback”,这样写不好,我是为了给大家在编码过程中演示一个问题及其解决方案,才使用了这个回调地址。大家在申请的时候回调地址“/auth/qq”是最好的。这个配置是可以修改的,所以申请时写错了也没关系。
填写完成之后等待审核。已经为我们分配了APP ID 和 APP KEY。并且获取了QQ登录接口的权限。
在进行网站QQ登录功能开发之前,建议开发者一定要仔细阅读该文档:QQ互联网站接入流程 ,这样在我们后续的开发中才能更顺畅。
<dependency>
<groupId>org.springframework.socialgroupId>
<artifactId>spring-social-securityartifactId>
<version>2.0.0.M4version>
dependency>
<dependency>
<groupId>org.springframework.socialgroupId>
<artifactId>spring-social-configartifactId>
<version>2.0.0.M4version>
dependency>
Spring Social的2.0.0.M4版本与Spring Boot2.0兼容。在Spring Boot2.0环境下不要使用Spring Social的1.x,改动比较大。但是大家也能看出来Spring Social 的2.0.0.M4版本比较新,新到在中央仓库中还没有这个jar,所以在pom.xml中需要我们新增一个非中央仓库地址。
<repositories>
<repository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/libs-milestoneurl>
<snapshots>
<enabled>falseenabled>
snapshots>
repository>
repositories>
在源码分析的章节,我们已经说过OAuth2Operations负责处理OAuth2的授权码请求、Access Token请求 ,即:OAuth2用户认证的标准流程,可以说是整个OAuth2认证中最关键的类。其默认的实现类是OAuth2Template。如果是标准的OAuth2结构,我们完全不需要针对OAuth2的认证过程开发任何代码。
但是QQ有点特殊,我们来看一下OAuth2Template源码:这个函数是请求获取AccessToken的函数,其默认将响应结果转成一个Map数据结构。
我们看一下:获取Access_Token接口文档。其文档中关键截图如下:
从截图中,我们可以看到相应的结果是一个用“&”分割的字符串,这种数据结构既不是JSON,也不是XML,是无法自动转成对象的,所以需要我们手动来改造一下,重写postForAccessGrant方法。
@Slf4j
public class QQOAuth2Template extends OAuth2Template {
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
// 设置带上 client_id、client_secret
setUseParametersForClientAuthentication(true);
}
/**
* 解析 QQ 返回的令牌
*/
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
// 返回格式:access_token=FE04********CCE2&expires_in=7776000&refresh_token=88E4***********BE14
String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
log.info("获取accessToke的响应:"+responseStr);
String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
String accessToken = StringUtils.substringAfterLast(items[0], "=");
Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
String refreshToken = StringUtils.substringAfterLast(items[2], "=");
return new AccessGrant(accessToken, null, refreshToken, expiresIn);
}
/**
* QQ 响应 ContentType=text/html;因此需要加入 text/html; 的处理器
*/
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charsets.UTF_8));
return restTemplate;
}
}
该用户信息即“SpringSocial社交媒体登录总图”中的User。即:社交媒体平台的用户信息。
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class QQUser {
private String openId;
//返回码:0表示获取成功
private String ret;
//返回错误信息,如果返回成功,错误信息为空串
private String msg;
//用户昵称
private String nickname;
//用户的头像30x30
private String figureurl;
//性别
private String gender;
}
以上信息是从响应数据中挑选了一些重要的信息进行封装,完整的响应数据结构参考:get_user_info接口定义。因为我们定义的信息不完整,为了避免映射字段找不到的异常,加上@JsonIgnoreProperties(ignoreUnknown = true)注解。该注解如果无法理解,可以自行学习很简单。
首先我们定义一个获取QQ用户信息的接口,接口只有一个方法如下:
public interface QQApi {
QQUser getUserInfo();
}
然后我们来定义QQAPI接口实现类,同时继承AbstractOAuth2ApiBinding。我们在源码解析章节已经说到了,AbstractOAuth2ApiBinding封装了accessToken以及RestTemplate,帮助我们实现HTTP请求的参数携带,以及请求结果到对象的反序列化工作等。
@Slf4j
public class QQApiImpl extends AbstractOAuth2ApiBinding implements QQApi {
private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
private String appId;
private String openId;
private ObjectMapper objectMapper = new ObjectMapper();
public QQApiImpl(String accessToken, String appId) {
//默认是使用header传递accessToken,而QQ比较特殊是用parameter传递token
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
this.appId = appId;
this.openId = getOpenId(accessToken);
log.info("QQ互联平台openId:{}",this.openId);
}
//通过接口获取openId
private String getOpenId(String accessToken) {
String url = String.format(URL_GET_OPENID, accessToken);
String result = getRestTemplate().getForObject(url, String.class);
return StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
}
//通过接口获取用户信息
@Override
public QQUser getUserInfo() {
try {
String url = String.format(URL_GET_USERINFO, appId, openId);
String result = getRestTemplate().getForObject(url, String.class);
QQUser userInfo = objectMapper.readValue(result, QQUser.class);
userInfo.setOpenId(openId);
return userInfo;
} catch (Exception e) {
throw new RuntimeException("获取用户信息失败", e);
}
}
}
我们自己开发的应用通过OAuth2协议与服务提供商进行交互,主要有两部分
我们需要将这两部分内容的封装结果告知ServiceProvider,从而可以被正确调用。代码如下:
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQApi> {
//OAuth2获取授权码的请求地址
private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
//OAuth2获取AccessToken的请求地址
private static final String URL_GET_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
private String appId;
public QQServiceProvider(String appId, String appSecret) {
super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_GET_ACCESS_TOKEN));
this.appId = appId;
}
@Override
public QQApi getApi(String accessToken) {
return new QQApiImpl(accessToken, appId);
}
}
不同的社交媒体平台(QQ、微信、GitHub)用户数据结构各式各样,但是Spring Social只认识Connection这一种用户信息结构。所以需要将QQUser与Connection进行适配。代码如下
public class QQApiAdapter implements ApiAdapter<QQApi> {
//测试Api连接是否可用
@Override
public boolean test(QQApi api) {
return true;
}
//QQApi 与 Connection 做适配(核心)
@Override
public void setConnectionValues(QQApi api, ConnectionValues values) {
QQUser user = api.getUserInfo();
values.setDisplayName(user.getNickname());
values.setImageUrl(user.getFigureurl());
values.setProviderUserId(user.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQApi api) {
return null;
}
@Override
public void updateStatus(QQApi api, String message) {
}
}
自定义OAuth2ConnectionFactory,通过QQServiceProvider发送请求,通过QQApiAdapter将请求结果转换为Connection。
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQApi> {
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServiceProvider(appId, appSecret), new QQApiAdapter());
}
}
QQConnectionFactory构造方法的第一个参数是providerId可以随便定义,但是最好要具有服务提供商的唯一性和可读性。比如:qq、wechat。第二个参数和第三个参数是在服务提供商创建应用申请的APP ID和APP KEY。
@Configuration
@EnableSocial
public class QQAutoConfiguration extends SocialConfigurerAdapter {
@Resource
private DataSource dataSource;
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository usersConnectionRepository =
new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
// 设置表前缀
usersConnectionRepository.setTablePrefix("sys_");
return usersConnectionRepository;
}
@Override
public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer,
Environment environment) {
connectionFactoryConfigurer.addConnectionFactory(
new QQConnectionFactory("qq", //这里配置什么取决于你的回调地址
"你申请的APP ID","你申请的APP KEY")); //这里可以优化为application配置
}
@Override
public UserIdSource getUserIdSource() {
return new AuthenticationNameUserIdSource();
}
}
@Configuration
public class QQFilterConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;
public QQFilterConfigurer() { }
public QQFilterConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
@SuppressWarnings("unchecked")
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
@Bean
public SpringSocialConfigurer qqFilterConfig() {
QQFilterConfigurer configurer = new QQFilterConfigurer("/login");
configurer.signupUrl("/bind.html");
configurer.postLoginUrl("/index");
return configurer;
}
@Resource
private SpringSocialConfigurer qqFilterConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.apply(qqFilterConfig).and()
...
}
<a href="/login/qq">QQ登录a>
如果报下面类似的表找不到的错误,需要先去建表。这张表是用于保存本平台用户与社交媒体平台用户关系的数据库表。
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException:
Table 'testdb.sys_userconnection' doesn't exist
在spring-social-core.jar里面找到org.springframework.social.connect.jdbc.JdbcUsersConnectionsRepository.sql。使用该文件里面的建表语句创建:[tablePrefix]UserConnection,其中tablePrefix替换为我们上一节的自定义配置。比如:“sys_”
create table sys_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 sys_UserConnection(userId, providerId, rank);
Spring Social依赖于session,所以不要设置无状态模式,否则无法正确跳转。
//.sessionManagement()
//.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
通过之前的文章讲解。我们知道ApiAdapter将QQ平台的用户标准数据结构QQUser转换为Spring Social用户标准的数据结构Connection。这两种用户信息仍然代表的是服务提供商的用户信息,那我们就面临着一个问题:如何通过服务提供商的用户信息Connection,得到我们自己开发的系统的用户信息?
首先,明确数据库里面有一张表UserConnection,这张表有三个核心字段userId、providerId、providerUserId。
通过这张表我们可以确定我们自己开发的应用的用户与服务提供商用户之间的关系,这张表里面的数据是用户注册或者绑定的时候插入的(后文会讲到如何实现)。
其次,现在已知Connection包含providerId和providerUserId,那么如何获取userId?答案就是使用UserConnectionRepository接口。Spring Social通过该接口查询UserConnection表,通过providerId和providerUserId获取userId。
至此,我们就拿到了本地系统的userId。Spring Social提供给我们两个接口,一个是SocialUserDetails,一个是SocialUserDetailsService,大家看到这两个接口是不是有点眼熟?对了,就是和我们使用用户名密码登录情景下的UserDetails和UserDetailsService是一样的,只不过一个是通过username加载UserDetails,一个是通过userId加载UserDetails。SocialUserDetails继承自UserDetails,所以我们的用户信息实体实现SocialUserDetails接口即可。
@Data
@AllArgsConstructor
public class MyUserDetails implements SocialUserDetails {
String password; //密码
String username; //用户名
boolean accountNonExpired; //是否没过期
boolean accountNonLocked; //是否没被锁定
boolean credentialsNonExpired; //是否没过期
boolean enabled; //账号是否可用
Collection<? extends GrantedAuthority> authorities; //用户的权限集合
@Override
public String getUserId() {
return username;
}
}
通常我们需要为用户生成一个userId并保存在数据库字段,我们这里就不做的那么麻烦了,直接使用username作为userId。
然后在原有的UserDetailsService实现上,新增SocialUserDetailsService的实现,即实现loadUserByUserId。
@Component
public class MyUserDetailsService implements UserDetailsService ,SocialUserDetailsService {
@Resource
private MyUserDetailsServiceMapper myUserDetailsServiceMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUserDetails myUserDetails = getMyUserDetails(username);
return myUserDetails;
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
MyUserDetails myUserDetails = getMyUserDetails(userId);
return myUserDetails;
}
private MyUserDetails getMyUserDetails(String username) {
//加载基础用户信息
MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(username);
//加载用户角色列表
List<String> roleCodes = myUserDetailsServiceMapper.findRoleByUserName(username);
//通过用户角色列表加载用户的资源权限列表
List<String> authorties = myUserDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);
//角色是一个特殊的权限,ROLE_前缀
roleCodes = roleCodes.stream()
.map(rc -> "ROLE_" +rc)
.collect(Collectors.toList());
authorties.addAll(roleCodes);
myUserDetails.setAuthorities(
AuthorityUtils.commaSeparatedStringToAuthorityList(
String.join(",",authorties)
)
);
return myUserDetails;
}
}
至此,我们通过userId得到了UserDetails信息,该信息中既包含用户基础信息,又包含用户权限信息。这样Spring Security就可以根据该信息控制用户登陆之后的访问权限。
至此,QQ登陆的核心功能我们就实现完成了,通过QQ登录按钮也可以正确的访问QQ扫码授权界面。如下:
因为我们还没有实现用户的注册和绑定功能,所以在qq扫码之后,仍然不能正确登陆。我们可以根据代码中如下打印信息,在userConnection表里面造一条用户注册绑定数据。用来测试。
[INFO ] 2019-12-09 21:38:08,050 com.zimug.bootlaunch.config.auth.qq.QQOAuth2Template -
获取accessToke的响应:access_token=4953C0123EEDC3BFE11EC7C14C85E7A1&expires_in=7776000&refresh_token=7917C5D8C5483273C8A91E80D1DF2ABB
[INFO ] 2019-12-09 21:38:08,583 com.zimug.bootlaunch.config.auth.qq.QQApiImpl -
QQ互联平台openId:ACEC828AB2EE206A155418FE2FC8E44C
测试过程需要注意:如果你在QQ互联注册的时候,填写的回调地址是:“http://www.zimug.com:8888/login/qq” ,然而你测试的时候,访问的是"http://localhost:8888/login.html"进行登录。这样QQ登陆之后是无法正确回调的,因为二者的域名对不上,QQ会做这方面的安全验证。
正确的做法是:你应该使用“http://www.zimug.com:8888/login.html” 进行登录。但是问题又来了,我这个“zimug.com”域名不是指向本机的,另外我也没有互联网ip(向移动、联通申请一个互联网ip要几万元一年)。我们可以本机的c:\windows\system32\drivers\etc\hosts文件来解决这个问题。windows10下面修改此文件非常麻烦,所以我使用一个工具:SwitchHosts
@Bean
public SpringSocialConfigurer qqFilterConfig() {
QQFilterConfigurer configurer = new QQFilterConfigurer("/login");
configurer.signupUrl("/bind.html");
configurer.postLoginUrl("/index");
return configurer;
}
}
<h2>用户绑定页面h2>
<form action="/qqbind" method="post">
<span>用户名称span><input type="text" name="username" id="username"/> <br>
<span>用户密码span><input type="password" name="password" id="password"/> <br>
<button type="button" onclick="bind()" >绑定button>
form>
<script>
function bind() {
var username = $("#username").val();
var password = $("#password").val();
if (username === "" || password === "") {
alert('用户名或密码不能为空');
return;
}
$.ajax({
type: "POST",
url: "/qqbind",
data: {
"username": username,
"password": password
},
success: function (json) {
alert(json.data);
if(json.isok){
location.href = "/index";
}
},
error: function (e) {
console.log(e.responseText);
}
});
}
script>
同时为了保障bind.html能被访问到,我们需要为“/bind.html”开放permitAll权限
开发一个Controller用于用户关系的绑定,需要为请求端点"/qqbind"开放permitAll权限
@RestController
public class QQBindController {
@Resource
private ProviderSignInUtils providerSignInUtils;
@Resource
private MyUserDetailsService myUserDetailsService;
@Resource
private PasswordEncoder passwordEncoder;
@PostMapping("/qqbind")
public AjaxResponse regist(@RequestParam String username,
@RequestParam String password, HttpServletRequest request) {
UserDetails userDetails = myUserDetailsService.loadUserByUserId(username);
boolean isMatch = passwordEncoder.matches(password,userDetails.getPassword());
if(userDetails.getUsername().equals(username)
&& isMatch){
//如果用户名密码正确,进行用户关系绑定
providerSignInUtils.doPostSignUp(username, new ServletWebRequest(request));
return AjaxResponse.success("用户绑定成功,请重新登录!");
}else{
return AjaxResponse.success("用户绑定失败,请检查用户名或密码是否正确!");
}
}
}
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
return new ProviderSignInUtils(connectionFactoryLocator,
getUsersConnectionRepository(connectionFactoryLocator)) {
};
}
当然,此处我们做的是用户关系绑定,实际上还有另外一种情况,就是当前用户还没有注册,也就是没有本系统用户。你也可以跳转到“注册页面”,注册的同时进行本地系统用户与“服务提供商”用户关系的绑定工作。实现原理和上面是几乎一致的。业务略有不同,只不过一个是从用户表加载数据判断用户名密码正确性,一个是向用户信息表里面插入用户信息数据。
我们在上一节中说到,doPostSignUp方法用于绑定本系统用户username与request.getSession中保存的“服务提供商”用户信息,将二者的关系保存到userconnection表中。那么session中的“服务提供商”用户信息数据,是什么时候放到session里面的呢?
其实我们在源码解析的章节里面已经讲到过:
private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
try {
if (!authService.getConnectionCardinality().isAuthenticatePossible()) {
return null;
} else {
token.setDetails(this.authenticationDetailsSource.buildDetails(request));
Authentication success = this.getAuthenticationManager().authenticate(token);
Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
this.updateConnections(authService, token, success);
return success;
}
} catch (BadCredentialsException var5) {
if (this.signupUrl != null) {
this.sessionStrategy.setAttribute(new ServletWebRequest(request),
ProviderSignInAttempt.SESSION_ATTRIBUTE,
new ProviderSignInAttempt(token.getConnection()));
throw new SocialAuthenticationRedirectException(
this.buildSignupUrl(request));
} else {
throw var5;
}
}
}
上面代码是SocialAuthenticationFilter中的代码,当捕获BadCredentialsException之后,先将 token.getConnection()即“服务提供商”用户信息保存到seesion里面,然后再跳转到SignupUrl。
大家看下面的张张图,页面内容是“用户绑定”,既然是用户信息绑定就是“谁”和“谁”绑定。下面这张图,只能体现出本系统用户需要输入用户名密码,然而“服务提供商”的用户信息完全没有任何显示,这样很不友好,所以我们应该优化一下。
怎么优化呢?上一小节我们已经说了“服务提供商”用户信息,就保存在session里面,我们把它取出来显示在页面上就可以了。怎么取?
@GetMapping("/qquser")
public AjaxResponse regist(HttpServletRequest request) {
Connection connection =
providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
return AjaxResponse.success(connection.getImageUrl());
}
Connection 中有很多的QQ用户信息,但是不需要全都显示出来,我这里就只返回了QQ用户头像。然后前端bind.html略微改造
~~~
window.onload = function () {
$.ajax({
type: "GET",
url: "/qquser",
success: function (json) {
document.getElementById("qqUserImg").src = json.data;
}
});
};
这里会显示qq用户的头像,qq用户头像有很多种,30*30 等等其他大小的头像,这个位置显示什么,取决于你QQApiAdapter、QQUser、Connection 之间如何做的适配。
学习资料参考:
https://www.kancloud.cn/hanxt/springsecurity/1836919
感谢字母歌!!!