文章目录

  • OAuth简介
    • OAuth协议要解决的问题
  • spring-social简介
  • QQ登录(实例)
    • 1、实现Api与ServiceProvider
    • 2、获取到用户信息之后的处理
    • 3、一些需要的配置与整合
    • 4、一个简单的小结
  • 几个重要的问题(有源码的解读)
    • 1、QQ登录的路径问题
    • 2、内部引发signin的跳转
    • 3、如果自定义获取token
  • 总结

OAuth简介

关于OAuth协议,这篇博客不会花大篇幅去介绍,因为有很多大牛已经总结的不错了。实在不敢班门弄斧。

较为详细的博客总结——OAuth协议简介

OAuth协议要解决的问题

应用场景:如果我们需要开发一个系统,需要读取用户微信好友列表信息。如果直接读取用户的微信密码,这肯定不现实,只能通过相关的授权协议来实现,OAuth协议就解决了这个问题。

OAuth协议的大体流程步骤如下:图中有三个角色,资源所有者,第三方应用,服务提供商。为了方便理解,资源所有者可以类比为客户,服务提供商可以类比为微信,第三方应用可以类比为我们自己的应用,我们需要读取用户在微信的好友列表。(图参照于我们学习的某课网的课程)

OAuth其中用户同意授权的步骤(第二步)有如下几种模式——1、授权码模式,2、密码模式,3、简化模式,4、客户端模式

其中简化模式和客户端模式使用的不多。我们本篇博客总结的时候,暂时总结授权码模式,后续再总结密码模式。

授权码模式的认证流程,授权码模式是认证流程最完成,最安全的认证模式

在授权码模式中,用户同意授权的动作是在第三方服务器上完成的,在向用户发放授权码之后,再去申请令牌,这样一定程度上避免了伪造令牌的情况。

spring-social简介

spring-social就是为我们封装了下图中的流程

spring-social几乎为我们封装了上述图片中的1~7步,我么不需要再繁重的组装每一步内容,只需要在一定程度上根据spring-social定义的接口,配置好相关的请求url和参数,即可获取第三方社交的基本用户信息。上述流程中第一步到第五步,都是一个标准的流程,但是针对第六步,由于每个服务商提供的用户信息不同,所以是一个定制化的流程。

spring-social在spring-security过滤器链中加入了一个过滤器,这个过滤器就封装了流程。

从上述的第一步到第六步,都需要和服务提供商交互,spring-social为我们提供了一个固定的抽象类——ServiceProvider(有基于OAuth2和OAuth1协议,本篇介绍OAuth2协议)由于不同服务提供商的基础用户信息有差异,针对此,spring-social提供了对应的Api(AbstractOAuth2ApiBinding

最后的第七步,是我们自己对用户信息的处理,spring-social针对这一步的处理,提供了一个ConnectionFactory ,spring-social把每次与服务提供商交互获取一次用户信息看成发起一次connection。其中的ApiAdapter是将不同的第三方用户信息,转换为标准的Connection的用户信息。除此之外,spring-social还在数据库层面为我们存储了与第三方用户的关联信息。

代码层面的构造

QQ登录(实例)

下面开始基于spring-social实现QQ登录。

准备的东西:需要在QQ互联平台上注册好相关的appId和appsecret,目前QQ互联的注册需要合法备案的域名,这里为了安全,也不会贴出我自己的appId和appsecret。

1、实现Api与ServiceProvider

定义接口和用户信息实体

/*** autor:liman* createtime:2021/7/12* comment: QQ登录的接口*/
public interface QQLoginInterface {QQUserInfo getQQUserInfo() ;}/*** autor:liman* createtime:2021/7/12* comment: QQ登录返回的用户信息(相关字段参照于QQ互联平台提供)*/
@Data
public class QQUserInfo {/***   返回码*/private String ret;/*** 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。*/private String msg;/****/private String openId;/*** 不知道什么东西,文档上没写,但是实际api返回里有。*/private String is_lost;/*** 省(直辖市)*/private String province;/*** 市(直辖市区)*/private String city;/*** 出生年月*/private String year;/***  用户在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的100×100的头像,但40×40像素则是一定会有。*/private String figureurl_qq_2;/***     性别。 如果获取不到则默认返回”男”*/private String gender;/***  标识用户是否为黄钻用户(0:不是;1:是)。*/private String is_yellow_vip;/***    标识用户是否为黄钻用户(0:不是;1:是)*/private String vip;/***   黄钻等级*/private String yellow_vip_level;/***  黄钻等级*/private String level;/*** 标识是否为年费黄钻用户(0:不是; 1:是)*/private String is_yellow_year_vip;}

2、继承实现AbstractOAuth2ApiBinding

/*** autor:liman* createtime:2021/7/12* comment:* AbstractOAuth2ApiBinding中有两个关键的属性* 1、accessToken用于存储前几步下来获取的访问令牌* 2、restTemplate 一个发起HTTP请求的工具类* 同时由于每一个用户的信息不同,因此这个不可能是单例的,而是每次从QQ获取用户信息,都会实例化一个QQLoginImpl*/
@Slf4j
public class QQLoginImpl extends AbstractOAuth2ApiBinding implements QQLoginInterface {//根据token获取用户OPENID的urlprivate static final String URL_GET_OPENID= "https://graph.qq.com/oauth2.0/me?access_token=%s";//根据openId获取用户信息的urlprivate 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 QQLoginImpl(String accessToken,String appId){super(accessToken,TokenStrategy.ACCESS_TOKEN_PARAMETER);//将token作为参数放到url后面this.appId = appId;//构造函数中,去获取openIdString url = String.format(URL_GET_OPENID,accessToken);String result = getRestTemplate().getForObject(url,String.class);//获取openId的结果log.info("从QQ获取的openId结果为:{}",result);//截取获取openidthis.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");}/**从QQ获取用户信息*/@Overridepublic QQUserInfo getQQUserInfo(){try {String url = String.format(URL_GET_USERINFO, appId, openId);String result = getRestTemplate().getForObject(url, String.class);log.info("读取到的用户信息为:{}", result);QQUserInfo qqUserInfo = objectMapper.readValue(result, QQUserInfo.class);qqUserInfo.setOpenId(openId);return qqUserInfo;}catch (Exception e){log.error("获取用户信息出现异常,异常信息为:{}",e);return null;}}
}

从QQ获取用户信息,根据QQ互联平台提供的接口文档中我们可知,需要三个参数:1、token(这个通过OAuth协议前5步流程获取),2、appId(这个需要在QQ互联上申请注册,让QQ互联知道访问QQ服务器的是哪一个应用),3、openId(这个根据token去QQ互联上获取即可)。其中token的获取,父类自动帮助我们处理了,我们不需要单独处理。

3、构建ServeiceProvider

这一步的类需要继承至抽象类AbstractOAuth2ServiceProvider,同时指定我们QQ登录接口的泛型,这个类在构造方法中会实例化OAuth2Template,这个类完成了我们去QQ获取token和获取授权码的所有操作。

/*** autor:liman* createtime:2021/7/12* comment:QQ服务提供者*/
@Slf4j
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQLoginInterface> {private String appId;/*** 获取授权码的url*/private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";/*** 根据授权码获取token的url*/private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";/*** Create a new {@link OAuth2ServiceProvider}.** @param oauth2Operations the OAuth2Operations template for conducting the OAuth 2 flow with the provider.*/public QQServiceProvider(String appId, String appSecret) {/*** 第一个参数:appId,QQ互联平台注册之后被分配的* 第二个参数:appSecret,QQ互联平台注册之后被分配的* 第三个参数:去QQ获取授权码的url,万年不变的,可以写成固定值* 第四个参数:去QQ获取token的url,也是万年不变的*/super(new OAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));this.appId = appId;}/**返回一个我们的api实例*/@Overridepublic QQLoginInterface getApi(String accessToken) {// QQLoginImpl每次返回的时候都得重新实例化一个(不能通过@Component注解加入,原因之前已经介绍过了)return new QQLoginImpl(accessToken, appId);}
}

2、获取到用户信息之后的处理

通过第一小节的内容,其实已经可获取到用户信息了,但是针对用户信息如何处理,还需要我们自己开发相关代码,并做好配置

1、构建ApiAdapter

在之前的整体代码构造图片中,可以知道ConnectionFactory中需要一个适配器和ServiceProvider,其中ServiceProvider在上一小结中已经实现。

/*** autor:liman* createtime:2021/7/14* comment:QQAdapter需要适配QQ获取的用户信息*/
public class QQAdapter implements ApiAdapter<QQLoginInterface> {//判断第三方服务商的服务器(QQ) 是否是通的,这里直接固定返回true@Overridepublic boolean test(QQLoginInterface api) {return true;}//创建一个connection需要的数据项//从api中获取用户信息,然后将信息存入到ConnectionValues中,ConnectionValues就是spring-social定义的标准的第三方用户信息数据结构。@Overridepublic void setConnectionValues(QQLoginInterface api, ConnectionValues values) {QQUserInfo userInfo = api.getQQUserInfo();//昵称values.setDisplayName(userInfo.getNickname());values.setImageUrl(userInfo.getFigureurl_qq_1());//头像values.setProfileUrl(null);//个人主页(QQ没有,这里直接设置为null)values.setProviderUserId(userInfo.getOpenId());//用户在服务商的openId(第三方用户id)}/*** 用于解绑和绑定 后续会总结* @param api* @return*/@Overridepublic UserProfile fetchUserProfile(QQLoginInterface api) {return null;}/*** 某些社交网站上,可以更新微博的操作,这里不需要* @param api* @param message*/@Overridepublic void updateStatus(QQLoginInterface api, String message) {//do nothing}
}

2、ConnectionFactory的构建

构建OAuth协议的ConnectionFactory,需要指定相关泛型,根据之前提供的代码构建图中,可以知道其需要两个组件,一个是QQAdapter,一个是QQServiceProvider,这两个只需要在构造函数中实例化即可。Connection由ConnectionFactory自动产生,不需要我们操作。

/*** autor:liman* createtime:2021/7/14* comment:*/
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQLoginInterface> {/*** Create a {@link OAuth2ConnectionFactory}.** @param providerId      the provider id e.g. "facebook"* @param serviceProvider the ServiceProvider model for conducting the authorization flow and obtaining a native service API instance.* @param apiAdapter      the ApiAdapter for mapping the provider-specific service API model to the uniform {@link Connection} interface.** 有三个参数:* 第一个为服务提供商id,自己配置的* 第二个为我们构建的serviceProvider* 第三个为我们构建的apiAdapter**/public QQConnectionFactory(String providerId, String appId,String appSecret) {super(providerId, new QQServiceProvider(appId,appSecret), new QQAdapter());}
}

3、配置UsersConnectionRepository

JdbcUsersConnectionsRepository这个类spring-social为我们提供好了,我们只需要配置即可。

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {@Autowiredprivate DataSource dataSource;@Autowiredprivate SecurityProperties securityProperties;@Overridepublic UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {//第一个参数是数据源(这里省略了数据库的配置)//第二个参数是一个根据第三方查找指定的ConnectionFactory,这个参数会被传递进来,这个是根据条件自动查找相关的ConnectionFactory ,如果是微信登录,则会查找微信的ConnectionFactory//第三个是加解密的配置,这里配置的是不加密JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());repository.setTablePrefix("self_");//指定操作表的前缀return repository;}
}

JdbcUsersConnectionsRepository操作的就是我们数据库中的一张表,这个表我们需要手动创建,但是在spring-social相关包中提供了SQL,就在JdbcUsersConnectionsRepository源码对应的包下

拷贝相关SQL,在数据库中执行即可,如果针对表名加前缀,可以在上述代码中通过setTablePrefix进行配置,但是表名是不能改的。

这个表其中有三个属性至关重要——userId,providerId,providerUserId,这三个字段分别表示:在本系统中的用户id,服务提供商的Id,用户在第三方系统中的用户id(openId)。这三个字段联合作为主键。这几个字段可以定位到当前系统中的用户与第三方关联用户的基础信息,refreshToken是当前用户的令牌。

4、提供一个SocialUserDetailsService用于用户信息的转换

/*** autor:liman* createtime:2021/7/8* comment:SocialUserDetailsService的子类,用于转换用户信息*/
@Component
@Slf4j
public class MyUserDetailService implements SocialUserDetailsService {@Autowiredprivate PasswordEncoder passwordEncoder;/*** 这里的参数是spring-social根据openId查出来的userId* @param userId* @return* @throws UsernameNotFoundException*/@Overridepublic SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {log.info("社交登录用户id:{}",userId);return new SocialUser(userId,passwordEncoder.encode("123456"),true,true,true,true,AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));}
}

3、一些需要的配置与整合

appid和appsecret

老操作,通过Properties类注入

public class SocialLoginProperties {private QQSocialLoginProperties qq = new QQSocialLoginProperties();public QQSocialLoginProperties getQq() {return qq;}public void setQq(QQSocialLoginProperties qq) {this.qq = qq;}
}/*** autor:liman* createtime:2021/7/14* comment:社交登录的相关配置 SocialProperties由spring-social提供,其中有appid 和 appsecret*/
public class QQSocialLoginProperties extends SocialProperties {public String provideId = "QQ";//自定义服务提供商的标示public String getProvideId() {return provideId;}public void setProvideId(String provideId) {this.provideId = provideId;}
}

在配置ConnectionFactory的时候,需要用到appid和appsecret

/*** autor:liman* createtime:2021/7/14* comment:*/
@Configuration
@ConditionalOnProperty(prefix="self.security.core.social.qq",name = "app-id")//配置文件中配置了app-id项,则这个配置才生效
public class QQAutoConfig extends SocialAutoConfigurerAdapter {@Autowiredprivate SecurityProperties securityProperties;/*** 配置QQ的连接工厂* @return*/@Overrideprotected ConnectionFactory<?> createConnectionFactory() {QQSocialLoginProperties qqSocialLoginProperties = securityProperties.getSocial().getQq();return new QQConnectionFactory(qqSocialLoginProperties.getProvideId(),qqSocialLoginProperties.getAppId(),qqSocialLoginProperties.getAppSecret());}
}

最后一步,将SocialAuthenticationFilter加入到spring-security认证的过滤器链上。

1、在SocialConfig类中构造一个SpringSocialConfigurer,这个类中的config方法会自动为我们实例化一个SocialAuthenticationFilter

@Bean
public SpringSocialConfigurer selfSocialSecurityConfig(){SpringSocialConfigurer selfSpringSocialConfig = new SpringSocialConfigurer();return selfSpringSocialConfig;
}

之后在我们的springsecurity的核心配置类中加入上述Bean即可。

http.and().apply(selfSocialSecurityConfig);//引入社交登录的配置

4、一个简单的小结

到目前为止,已经完成了社交登录所需要的全部组件,但是还是会有些问题,先在这里梳理一下整个的代码流程

先上图

这张图就是spring-social在进行第三方登录的时候涉及到的主要的接口和实现类,以及他们的调用顺序。其实这个流程与我们之前的自定义手机验证码登录流程差异不大。

一个过滤器拦截某一个特定的请求,在过滤器中将需要身份认证的信息封装一个Authentication中,然后将这个Authentication交给AuthenticationManager管理,AuthenticationManager会根据具体的Authentication类型找到对应的AuthenticationProvider进行具体的认证,只进行具体的认证过程中,AuthenticationProvider会调用我们具体的UserDetailsService类,如果校验通过,则会将认证成功的信息再次放到Authentication中。这其实就是spring-security最关键,最核心的认证流程。

只是在spring-social第三方登录的时候,用到了一些特殊的东西,当第三方登录请求进入到SocialAuthenticationFilter中,这个过滤器会调用SocialAuthenticationService,这个SocialAuthenticationService会帮我们完成OAuth2的第三方认证。在SocialAuthenticationService认证的过程中,会去调用我们自己写的一些组件(这些组件在图中用橘色标明了)。

几个重要的问题(有源码的解读)

以上一顿操作猛如虎,但是……依旧不能实现QQ登录存在以下几个问题

1、QQ登录的路径问题

我们可以在指定页面上添加一个QQ登录的入口

<h3>社交登录</h3>
<a href="/auth/qq">QQ登录</a>

需要说明的是,这个配置的href属性是有讲究的。我们点击之后,QQ会回调一个url,这个url其实就是在这个href后面加上了授权码。(具体需要调试才能发现,由于不好截图,这里只能用文字描述)

以上面的href=/auth/qq为例,其中前半段其实是SocialAuthenticationFilter中指定的拦截url,后半段是我们配置的providerId

我们可以在SocialAuthenticationFilter中的源码看到如下内容

在构造函数中指定了针对这个默认的url的拦截。href中/auth之后的/qq,其实就是我们在构建ConnectionFactory中传入的providerId,表示服务提供商的Id,因此如果在申请QQ互联平台时,配置回调url的时候,要慎重。

SocialAuthenticationFilter中核心的认证方式如下,其实就是找到指定的SocialAuthenticationService,将需要认证的参数传递进去即可。

//以下代码位于:
//org.springframework.social.security.SocialAuthenticationFilter#attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (detectRejection(request)) {if (logger.isDebugEnabled()) {logger.debug("A rejection was detected. Failing authentication.");}throw new SocialAuthenticationException("Authentication failed because user rejected authorization.");}Authentication auth = null;Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();String authProviderId = getRequestedProviderId(request);if (!authProviders.isEmpty() && authProviderId != null && authProviders.contains(authProviderId)) {SocialAuthenticationService<?> authService = authServiceLocator.getAuthenticationService(authProviderId);auth = attemptAuthService(authService, request, response);if (auth == null) {throw new AuthenticationServiceException("authentication failed");}}return auth;
}

还需要留意一下SocialAuthenticationFilter中定义的失败的处理器就是org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler这个失败处理器会将我们的请求重定向到一个默认的失败路径上去,这个默认的失败路径就是在SocialAuthenticationFilter中定义的DEFAULT_FAILURE_URL,上图中都可以看到。

如果要修改SocialAuthenticationFilter中的默认认证路径,需要我们自定义一个SpringSocialConfigurer。我们翻看SpringSocialConfigurer中的代码,发现在其构造函数的时候,其加入了SocialAuthenticationFilter过滤器

但是在加入SocialAuthenticationFilter过滤器之前,其用一个方法postProcess处理了一下

这个方法是protected类型的,因此我们可以通过继承SpringSocialConfigurer进行复写,这个方法就是设置了SocialAuthenticationFilter的拦截url。

自定义SpringSocialConfigurer中的postProcess方法

/*** autor:liman* createtime:2021/7/15* comment:自定义的springsocial配置类*/
public class SelfSpringSocialConfig extends SpringSocialConfigurer {private String processFilterUrl;public SelfSpringSocialConfig(String processFilterUrl) {this.processFilterUrl = processFilterUrl;}@Overrideprotected <T> T postProcess(T object) {SocialAuthenticationFilter socialAuthenticationFilter = (SocialAuthenticationFilter) super.postProcess(object);socialAuthenticationFilter.setFilterProcessesUrl(processFilterUrl);return (T) socialAuthenticationFilter;}
}

将我们自定义的SpringSocialConfigurer交给容器托管,并注入给相关security的配置

/*** autor:liman* createtime:2021/7/14* comment: 社交登录的配置类*/
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {@Autowiredprivate DataSource dataSource;@Autowiredprivate SecurityProperties securityProperties;@Overridepublic UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {//第二个参数是一个,这个参数会被传递进来,这个是根据条件自动查找相关的ConnectionFactory ,如果是微信登录,则查找微信的ConnectionFactory//第三个是加解密JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());repository.setTablePrefix("imooc_");return repository;}//直接在SocialConfig中定义我们自己实现的SpringSocialConfigurer@Beanpublic SpringSocialConfigurer selfSocialSecurityConfig(){String processFilterUrl = securityProperties.getSocial().getProcessFilterUrl();SelfSpringSocialConfig selfSpringSocialConfig = new SelfSpringSocialConfig(processFilterUrl);return selfSpringSocialConfig;}
}

2、内部引发signin的跳转

走到了这里,革命成功大半。但是……我们在引发第三方跳转的时候,在走OAuth流程的时候,会引发一个/signin的跳转,这个可以在OAuth2AuthenticationService的源码中找到答案。在上一小节中介绍跳转href的时候,我们说过,如果我们配置的href是"/auth/qq"。在我们点击QQ登录之后,QQ服务器会配置一个redirect的地址,这个地址就是在href后面加上了授权码(如果我们配置的href是"/auth/qq",则redirect的值为:“域名/auth/qq?code=XXXXX”,在用户点击确认授权之后,服务器会跳转到redirect指定的值,而这个值其实依旧是我们原先的href路径,只是在后面加了一个参数。

说了这么多,就是一句话,在点击QQ登录链接请求之前和之后,我们的应用需要处理两次SocialAuthenticationFilter的目标url。

//以下代码位于:
//org.springframework.social.security.provider.OAuth2AuthenticationService#getAuthToken中
public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {String code = request.getParameter("code");if (!StringUtils.hasText(code)) {//如果code没有值(点击QQ登录的时候)OAuth2Parameters params =  new OAuth2Parameters();params.setRedirectUri(buildReturnToUrl(request));setScope(request, params);params.add("state", generateState(connectionFactory, request));addCustomParameters(params);//做一个重定向,将用户重定向到QQ的网站throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));} else if (StringUtils.hasText(code)) {//如果code有值(从QQ服务器带着授权码调回到我们本身应用的时候)try {String returnToUrl = buildReturnToUrl(request);//根据code去第三方服务器(QQ)获取令牌(这个就是最关键的一步)AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);// TODO avoid API call if possible (auth using token would be fine)Connection<S> connection = getConnectionFactory().createConnection(accessGrant);return new SocialAuthenticationToken(connection, null);} catch (RestClientException e) {logger.debug("failed to exchange for access", e);return null;}} else {return null;}
}

由于QQ返回的token不一定按照spring的默认数据结构实现,因此在根据授权码去获取令牌的时候,会出现异常,这个异常就引发了SocialAuthenticationFilter失败的跳转,这里就引出了如何自定义获取令牌的问题。

3、如果自定义获取token

可以继续深入到exchangeForAccess方法中,我们可以就看到spring-social中默认获取access_token的实现。这个方法就在OAuth2Template类中

//以下代码位于:
//org.springframework.social.oauth2.OAuth2Template#exchangeForAccess
public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap<String, String> additionalParameters) {MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();if (useParametersForClientAuthentication) {//如果useParametersForClientAuthentication为true,才会送给第三方服务client_id和client_secretparams.set("client_id", clientId);params.set("client_secret", clientSecret);}params.set("code", authorizationCode);params.set("redirect_uri", redirectUri);params.set("grant_type", "authorization_code");if (additionalParameters != null) {params.putAll(additionalParameters);}return postForAccessGrant(accessTokenUrl, params);
}//以下代码位于:
//org.springframework.social.oauth2.OAuth2Template#postForAccessGrant
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {//简单的通过RestTemplate请求我们指定的获取token的url,然后第三方服务器返回的是一个JSON的数据,但是有时候第三方服务器并不会按照json返回数据。//这里是直接将json转成一个Mapreturn extractAccessGrant(getRestTemplate().postForObject(accessTokenUrl, parameters, Map.class));
}//以下代码位于:
//org.springframework.social.oauth2.OAuth2Template#extractAccessGrant
private AccessGrant extractAccessGrant(Map<String, Object> result) {//可以很明显的看到,这个是根据restTemplate获取的json转换成的map,从中获取指定的参数构建AccessGrant(这个AccessGrant就是spring为我们封装的令牌)return createAccessGrant((String) result.get("access_token"), (String) result.get("scope"), (String) result.get("refresh_token"), getIntegerValue(result, "expires_in"), result);
}//org.springframework.social.oauth2.OAuth2Template#createAccessGrant
protected AccessGrant createAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn, Map<String, Object> response) {return new AccessGrant(accessToken, scope, refreshToken, expiresIn);
}

如果获取token失败,则会引发SocialAuthenticationFilter默认的失败url的处理。因此如果要考虑全面一点,则需要让我们新写一个OAuth2Template,使得其可以处理非json返回的数据。同时QQ互联平台的接口文档中也说明了,其返回的token格式如下,并不是所谓的json,因此我们需要自定义处理获取token接口的返回数据。

同时,对于client_id和client_secret是必输,因此我们需要将useParametersForClientAuthentication属性设置为true。

絮叨了这么多,就是为了引出我们自定义的QQOAuth2Template的代码

/*** autor:liman* createtime:2021/7/17* comment:自定义的OAuth2Template,使得其可以处理第三方服务器返回的非json数据。*/
@Slf4j
public class QQOAuth2Template extends OAuth2Template {public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {super(clientId, clientSecret, authorizeUrl, accessTokenUrl);setUseParametersForClientAuthentication(true);}/*** 往RestTemplate中加入新的HTTP消息处理器* @return*/@Overrideprotected RestTemplate createRestTemplate() {RestTemplate restTemplate = super.createRestTemplate();restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));return super.createRestTemplate();}/*** 针对不同的返回参数,这里可以定制化处理(这里是按照QQ的标准)* @param accessTokenUrl* @param parameters* @return*/@Overrideprotected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);log.info("从QQ获取token,的返回字符串为:{}",responseStr);String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr,"&");String accessToken = StringUtils.substringAfterLast(items[0],"=");Long expiresIn = Long.valueOf(StringUtils.substringAfterLast(items[1],"="));String refreshToken = StringUtils.substringAfterLast(items[2],"=");//构建AccessGrantreturn new AccessGrant(accessToken,null,refreshToken,expiresIn);}
}

到这里,登录时OK了,但是,依旧还有但是,到这里还会引发一个signup的跳转。到这里我们相当于走到了AuthenticationProvider。

总结

本篇篇幅较长(好吧我说实话,确实很长),因为涉及内容较多,从源码层层深入剖析问题,并最终引出解决方案,这本身就是一个很耗费时长的过程,如果耐心看完相信会有所收获,继续说一点最后的问题,到这里依旧没有完成QQ登录的完整流程,spring-social会自动给我们跳转到注册页面。这个问题就是QQ第三方登录引发的注册问题,这个会在下一篇博客中总结,毕竟,这篇博客篇幅已经够长了。

有点乱,还是附上源码地址吧——参考其中的spring-security开头的项目。

spring-security学习(七)——QQ登录(上篇)相关推荐

  1. spring security 学习三-rememberMe

    spring security 学习三-rememberMe 功能:登录时的"记住我"功能 原理: rememberMeAuthenticationFilter在security过 ...

  2. spring security 学习二

    spring security 学习二 doc:https://docs.spring.io/spring-security/site/docs/ 基于表单的认证(个性化认证流程): 一.自定义登录页 ...

  3. spring security 学习一

    spring security 学习一 1.配置基本的springboot web项目,加入security5依赖,启动项目 浏览器访问,即可出现一个默认的登录页面 2.什么都没有配置 登录页面哪里来 ...

  4. Spring Security学习总结

    2019独角兽企业重金招聘Python工程师标准>>> 1.Spring Security介绍  一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权 ...

  5. 5.Spring Security 短信验证码登录

    Spring Security 短信验证码登录 在 Spring Security 添加图形验证码一节中,我们已经实现了基于 Spring Boot + Spring Security 的账号密码登录 ...

  6. Spring Security默认的用户登录表单 页面源代码

    Spring Security默认的用户登录表单 页面源代码 <html><head><title>Login Page</title></hea ...

  7. Spring Security学习(二)

    以下配置基于表单登录配置 自定义配置登录页面 @Override protected void configure(HttpSecurity http) throws Exception {http. ...

  8. 关于Spring Security框架 关于单点登录sso

    1.Spring Security的作用 Spring Security主要解决了认证和授权相关的问题. 认证(Authenticate):验证用户身份,即登录. 授权(Authorize):允许用户 ...

  9. Spring security 学习 (自助者,天助之!)

    自己努力,何必要强颜欢笑的求助别人呢?  手心向下不求人! Spring security学习有进展哦: 哈哈! 1.页面都是动态生产的吧! 2.设置权限:  a:pom.xml配置jar包 b:cr ...

  10. Spring Security 短信验证码登录(5)

    在Spring Security添加图形验证码中,我们已经实现了基于Spring Boot + Spring Security的账号密码登录,并集成了图形验证码功能.时下另一种非常常见的网站登录方式为 ...

最新文章

  1. DDoS攻击可能损害企业品牌的四种方式
  2. 30 整数中1出现的次数(从1到n整数中1出现的次数)这题很难要多看*
  3. struts2工作原理
  4. 小哥哥,WebRTC 了解一下
  5. mybatis传set参数
  6. 关于EF使用脏读(连接会话开始执行设置隔离级别)
  7. 30人的产研团队如何高效协同?
  8. Flowable 数据库表结构 ACT_RU_VARIABLE
  9. Django讲课笔记06:搭建项目开发环境
  10. 通过编写串口助手工具学习MFC过程——(三)Unicode字符集的宽字符和多字节字符转换...
  11. 软件测试必学之python+unittest+requests+HTMLRunner编写接口自动化测试集
  12. 【5分钟 Paper】Deep Reinforcement Learning with Double Q-learning
  13. 《Linux性能及调优指南》 Linux进程管理
  14. android studio for android learning (十六) support-annotations简介
  15. 雷蛇灵刃 15 黑苹果 Hackintosh
  16. 多实例学习PCNN在关系抽取中的应用
  17. Package winbind is not configured yet.
  18. 双击ie浏览器没反应打不开的解决方法
  19. python量化策略——改进的美林时钟介绍(0)
  20. bert:pre-training of deep bidirectional transformers for language understanding

热门文章

  1. 爬虫逆向之字体反爬(二)、镀金的天空-字体反爬-2
  2. 审计日志在分布式系统中的应用
  3. TwinCAT NC轴控制第三方伺服报错4655原因
  4. 基础平台项目之设计方案
  5. WEB安全的学习总结与心得(一)
  6. C#无法将顶级控件添加到控件
  7. RabbitMQ的七种工作模式-RPC模式(六)
  8. 脑子不灵活适合学计算机吗,开学就初三了,初一初二什么也不学但是老师都说我脑子灵活不知道初三能学好吗...
  9. The Merge 过后,没有以太坊 2.0,只有共识层
  10. 流行音乐界有一个人叫Michael Jackson!