专栏首页java干货Spring Security源码分析三:Spring Social实现QQ社交登录

Spring Security源码分析三:Spring Social实现QQ社交登录

社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ、人人网、开心网、新浪微博、搜狐微博、腾讯微博、淘宝、豆瓣、MSN、Google等社会化媒体账号登录该网站。

OAuth2.0的认证流程示意图

  1. 请求第三方应用
  2. 第三方应用将用户请求导向服务提供商
  3. 用户同意授权
  4. 服务提供商返回code
  5. client根据code去服务提供商换取令牌
  6. 返回令牌
  7. 获取用户信息

在标准的OAuth2协议中,1-6步都是固定,只有最后一步,不通的服务提供商返回的用户信息是不同的。Spring Social已经为我们封装好了1-6步。

使用Spring Social

准备工作

在申请个人开发者,获得appId和appKey或者使用 贡献出来的

配置本地host 添加

数据库执行以下sql

项目端口设置为端口

引入Spring Social 模块

| 模块 | 描述 | | ———— | ———— | | spring-social-core | 提供社交连接框架和OAuth 客户端支持 | | spring-social-config | 提供Java 配置 | | spring-social-security | 社交安全的一些支持 | | spring-social-web | 管理web应用程序的连接 |

!--spring-social 相关-->
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-config</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-web</artifactId>
		</dependency>

目录结构

  1. ‘api’ 定义api绑定的公共接口
  2. ‘config’ qq的一些配置信息
  3. ‘connect’与服务提供商建立连接所需的一些类。

定义返回用户信息接口

public interface QQ {
    /**
     * 获取用户信息
     * @return
     */
    QQUserInfo getUserInfo();
}

实现返回用户信息接口

@Slf4j
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {

    //http://wiki.connect.qq.com/openapi%E8%B0%83%E7%94%A8%E8%AF%B4%E6%98%8E_oauth2-0
    private static final String QQ_URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
    //http://wiki.connect.qq.com/get_user_info(access_token由父类提供)
    private static final String QQ_URL_GET_USER_INFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
    /**
     * appId 配置文件读取
     */
    private String appId;
    /**
     * openId 请求QQ_URL_GET_OPENID返回
     */
    private String openId;
    /**
     * 工具类
     */
    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 构造方法获取openId
     */
    public QQImpl(String accessToken, String appId) {
        //access_token作为查询参数来携带。
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);

        this.appId = appId;

        String url = String.format(QQ_URL_GET_OPENID, accessToken);
        String result = getRestTemplate().getForObject(url, String.class);

        log.info("【QQImpl】 QQ_URL_GET_OPENID={} result={}", QQ_URL_GET_OPENID, result);

        this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
    }

    @Override
    public QQUserInfo getUserInfo() {
        String url = String.format(QQ_URL_GET_USER_INFO, appId, openId);
        String result = getRestTemplate().getForObject(url, String.class);

        log.info("【QQImpl】 QQ_URL_GET_USER_INFO={} result={}", QQ_URL_GET_USER_INFO, result);

        QQUserInfo userInfo = null;
        try {
            userInfo = objectMapper.readValue(result, QQUserInfo.class);
            userInfo.setOpenId(openId);
            return userInfo;
        } catch (Exception e) {
            throw new RuntimeException("获取用户信息失败", e);
        }
    }
}

QQOAuth2Template处理qq返回的令牌信息

@Slf4j
public class QQOAuth2Template extends OAuth2Template {
    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true);
    }

    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
        String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);

        log.info("【QQOAuth2Template】获取accessToke的响应:responseStr={}" + responseStr);

        String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
        //http://wiki.connect.qq.com/使用authorization_code获取access_token
        //access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
        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);
    }


    /**
     * 坑,日志debug模式才打印出来 处理qq返回的text/html 类型数据
     *
     * @return
     */
    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }
}

QQServiceProvider连接服务提供商

public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {

    /**
     * 获取code
     */
    private static final String QQ_URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
    /**
     * 获取access_token 也就是令牌
     */
    private static final String QQ_URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
    private String appId;

    public QQServiceProvider(String appId, String appSecret) {
        super(new QQOAuth2Template(appId, appSecret, QQ_URL_AUTHORIZE, QQ_URL_ACCESS_TOKEN));
        this.appId = appId;
    }

    @Override
    public QQ getApi(String accessToken) {

        return new QQImpl(accessToken, appId);
    }
}

QQConnectionFactory连接服务提供商的工厂类

public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {

    public QQConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
    }
}

QQAdapter 适配spring Social默认的返回信息

public class QQAdapter implements ApiAdapter<QQ> {
    @Override
    public boolean test(QQ api) {
        return true;
    }

    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        QQUserInfo userInfo = api.getUserInfo();

        values.setProviderUserId(userInfo.getOpenId());//openId 唯一标识
        values.setDisplayName(userInfo.getNickname());
        values.setImageUrl(userInfo.getFigureurl_qq_1());
        values.setProfileUrl(null);
    }

    @Override
    public UserProfile fetchUserProfile(QQ api) {
        return null;
    }

    @Override
    public void updateStatus(QQ api, String message) {

    }
}

SocialConfig 社交配置主类

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    /**
     * 社交登录配类
     *
     * @return
     */
    @Bean
    public SpringSocialConfigurer merryyouSocialSecurityConfig() {
        String filterProcessesUrl = SecurityConstants.DEFAULT_SOCIAL_QQ_PROCESS_URL;
        MerryyouSpringSocialConfigurer configurer = new MerryyouSpringSocialConfigurer(filterProcessesUrl);
        return configurer;
    }

    /**
     * 处理注册流程的工具类
     * @param factoryLocator
     * @return
     */
    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator factoryLocator) {
        return new ProviderSignInUtils(factoryLocator, getUsersConnectionRepository(factoryLocator));
    }

}
QQAuthConfig 针对qq返回结果的一些操作
@Configuration
public class QQAuthConfig extends SocialAutoConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private ConnectionSignUp myConnectionSignUp;

    @Override
    protected ConnectionFactory<?> createConnectionFactory() {
        return new QQConnectionFactory(SecurityConstants.DEFAULT_SOCIAL_QQ_PROVIDER_ID, SecurityConstants.DEFAULT_SOCIAL_QQ_APP_ID, SecurityConstants.DEFAULT_SOCIAL_QQ_APP_SECRET);
    }

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
                connectionFactoryLocator, Encryptors.noOpText());
        if (myConnectionSignUp != null) {
            repository.setConnectionSignUp(myConnectionSignUp);
        }
        return repository;
    }
}

MerryyouSpringSocialConfigurer自定义登录和注册连接

public class MerryyouSpringSocialConfigurer extends SpringSocialConfigurer {

    private String filterProcessesUrl;

    public MerryyouSpringSocialConfigurer(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }

    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(filterProcessesUrl);
        filter.setSignupUrl("/register");
        return (T) filter;
    }
}

开启SocialAuthenticationFilter过滤器

@Autowired
    private SpringSocialConfigurer merryyouSpringSocialConfigurer;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()//使用表单登录,不再使用默认httpBasic方式
                .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//如果请求的URL需要认证则跳转的URL
                .loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//处理表单中自定义的登录URL
                .and()
                .apply(merryyouSpringSocialConfigurer)
                .and()
                .authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM,
                SecurityConstants.DEFAULT_REGISTER_URL,
                "/register",
                "/social/info",
                "/**/*.js",
                "/**/*.css",
                "/**/*.jpg",
                "/**/*.png",
                "/**/*.woff2",
                "/code/image")
                .permitAll()//以上的请求都不需要认证
                //.antMatchers("/").access("hasRole('USER')")
                .and()
                .csrf().disable()//关闭csrd拦截
        ;
        //安全模块单独配置
        authorizeConfigProvider.config(http.authorizeRequests());
    }

效果如下:

代码下载

从我的 github 中下载,https://github.com/longfeizheng/logback

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Spring Security源码分析四:Spring Social实现微信社交登录

    在上一章Spring-Security源码分析三-Spring-Social社交登录过程中,我们已经实现了使用Spring Social+Security的QQ...

    java干货
  • Spring Security源码分析六:Spring Social社交登录源码解析

    OAuth2是一种授权协议,简单理解就是它可以让用户在不将用户名密码交给第三方应用的情况下,第三方应用有权访问用户存在服务提供商上面的数据。

    java干货
  • 应有尽有!终于有人整理出Spring Security配置+安全防护+OAuth2源码学习资料

    本篇文章包含Spring Security Java配置、Spring Security安全防护和源码导读。详细讲解OAuth2实战,并深入地剖析OAuth2核...

    烂猪皮
  • Spring Security技术栈开发企业级认证与授权(十五)解决Spring Social集成QQ登录后的注册问题

    为什么会跳转到/signup上,或者在上面情况下会跳转到/signup上呢?我们一起阅读源代码来查找原因。我们在此把社交登录的流程图贴到这里。

    itlemon
  • Spring Security技术栈开发企业级认证与授权(十)开发记住我功能

    首先我们在浏览器的属性类BrowserProperties中添加一个字段rememberMeSeconds,这个字段用来描述“记住我”的时间期限,具体的配置类代...

    itlemon
  • Spring Security源码分析十六:Spring Security项目实战

    java干货
  • Spring Security-JWT-OAuth2一本通

    字母哥博客
  • web应用安全框架选型:Spring Security与Apache Shiro

    官网:https://projects.spring.io/spring-security/

    字母哥博客
  • Spring Security技术栈开发企业级认证与授权(十四)使用Spring Social集成QQ登录验证方式

    在前一篇文章中介绍到,Spring Social封装了OAuth协议的标准步骤,我们只需要配置第三方应用的认证服务器地址即可,就可以获取到访问令牌Access ...

    itlemon
  • Spring Security源码分析五:Spring Security实现短信登录

    在Spring Security源码分析一:Spring Security认证过程和Spring Security源码分析二:Spring Security授权...

    java干货
  • SpringSecurity之记住我功能的实现

    登录的时候,请求发送给过滤器UsernamePasswordAuthenticationFilter,当该过滤器认证成功后,会调用RememberMeServi...

    Dream城堡
  • Spring Security源码分析十二:Spring Security OAuth2基于JWT实现单点登录

    用户的登录状态是由sso-server认证中心来保存的,登录界面和账号密码的验证也是sso-server认证中心来做的(client1和clien2返回toke...

    java干货
  • Spring Security与Java应用安全保护

    Spring Security是一个强大且高度可定制的安全框架,致力于为Java应用提供身份认证和授权。

    博文视点Broadview
  • 【SpringSecurity系列(一)】初识 Spring Security

    《深入浅出Spring Security》一书已由清华大学出版社正式出版发行,感兴趣的小伙伴戳这里->->>深入浅出Spring Security,一本书学会 ...

    江南一点雨
  • Spring Boot + Spring Security 实现自动登录功能

    很多网站我们在登录的时候都会看到类似的选项,毕竟总让用户输入用户名密码是一件很麻烦的事。

    江南一点雨
  • 挖一个大坑,Spring Security 开搞!

    自从 Spring Boot、Spring Cloud 火起来之后,Spring Security 也跟着沾了一把光!

    江南一点雨
  • Spring Boot 自动登录,安全风险要怎么控制?松哥教你两招

    在上篇文章中,我们提到了 Spring Boot 自动登录存在的一些安全风险,在实际应用中,我们肯定要把这些安全风险降到最低,今天就来和大家聊一聊如何降低安全风...

    江南一点雨
  • 在 SpringBoot 项目中,Spring Security 和 Shiro 该如何选择?

    要知道Shiro和Spring Security该如何选择,首先要看看两者的区别和对比

    好好学java
  • 在微服务项目中,Spring Security 比 Shiro 强在哪?

    虽然目前 Spring Security 一片火热,但是 Shiro 的市场依然存在,今天我就来稍微的说一说这两个框架的,方便大家在实际项目中选择适合自己的安全...

    江南一点雨

扫码关注云+社区

领取腾讯云代金券