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

前言

在上一章Spring-Security源码分析三-Spring-Social社交登录过程中,我们已经实现了使用 SpringSocial+ Security的QQ社交登录。本章我们将实现微信的社交登录。(微信和QQ登录的大体流程相同,但存在一些细节上的差异,下面我们来简单实现一下)

准备工作

  1. 熟悉OAuth2.0协议标准,微信登录是基于OAuth2.0中的authorization_code模式的授权登录;

  2. 微信开放平台申请网站应用开发,获取 appid和 appsecret

  3. 熟读网站应用微信登录开发指南

  4. 参考Spring-Security源码分析三-Spring-Social社交登录过程的准备工作

为了方便大家测试,博主在某宝租用了一个月的appid和appSecret

appid wxfd6965ab1fc6adb2
appsecret 66bb4566de776ac699ec1dbed0cc3dd1

目录结构

参考

  1. api 定义api绑定的公共接口

  2. config 微信的一些配置信息

  3. connect与服务提供商建立连接所需的一些类。

定义返回用户信息接口

  1. public interface Weixin {

  2.    WeixinUserInfo getUserInfo(String openId);

  3. }

这里我们看到相对于QQ的 getUserInfo微信多了一个参数 openId。这是因为微信文档中在OAuth2.0的认证流程示意图第五步时,微信的 openid 同 access_token一起返回。而 SpringSocial获取 access_token的类 AccessGrant.java中没有 openid。因此我们自己需要扩展一下 SpringSocial获取令牌的类( AccessGrant.java);

处理微信返回的access_token类(添加openid)

  1. @Data

  2. public class WeixinAccessGrant extends AccessGrant{

  3.    private String openId;

  4.    public WeixinAccessGrant() {

  5.        super("");

  6.    }

  7.    public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {

  8.        super(accessToken, scope, refreshToken, expiresIn);

  9.    }

  10. }

实现返回用户信息接口

  1. public class WeiXinImpl extends AbstractOAuth2ApiBinding implements Weixin {

  2.    /**

  3.     * 获取用户信息的url

  4.     */

  5.    private static final String WEIXIN_URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";

  6.    private ObjectMapper objectMapper = new ObjectMapper();

  7.    public WeiXinImpl(String accessToken) {

  8.        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);

  9.    }

  10.    /**

  11.     * 获取用户信息

  12.     *

  13.     * @param openId

  14.     * @return

  15.     */

  16.    @Override

  17.    public WeixinUserInfo getUserInfo(String openId) {

  18.        String url = WEIXIN_URL_GET_USER_INFO + openId;

  19.        String result = getRestTemplate().getForObject(url, String.class);

  20.        if(StringUtils.contains(result, "errcode")) {

  21.            return null;

  22.        }

  23.        WeixinUserInfo userInfo = null;

  24.        try{

  25.            userInfo = objectMapper.readValue(result,WeixinUserInfo.class);

  26.        }catch (Exception e){

  27.            e.printStackTrace();

  28.        }

  29.        return userInfo;

  30.    }

  31.    /**

  32.     * 使用utf-8 替换默认的ISO-8859-1编码

  33.     * @return

  34.     */

  35.    @Override

  36.    protected List<HttpMessageConverter<?>> getMessageConverters() {

  37.        List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();

  38.        messageConverters.remove(0);

  39.        messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));

  40.        return messageConverters;

  41.    }

  42. }

与 QQ获取用户信息相比, 微信的实现类中少了一步通过 access_token获取 openid的请求。 openid由自己定义的扩展类 WeixinAccessGrant中获取;

WeixinOAuth2Template处理微信返回的令牌信息

  1. @Slf4j

  2. public class WeixinOAuth2Template extends OAuth2Template {

  3.    private String clientId;

  4.    private String clientSecret;

  5.    private String accessTokenUrl;

  6.    private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";

  7.    public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {

  8.        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);

  9.        setUseParametersForClientAuthentication(true);

  10.        this.clientId = clientId;

  11.        this.clientSecret = clientSecret;

  12.        this.accessTokenUrl = accessTokenUrl;

  13.    }

  14.    /* (non-Javadoc)

  15.     * @see org.springframework.social.oauth2.OAuth2Template#exchangeForAccess(java.lang.String, java.lang.String, org.springframework.util.MultiValueMap)

  16.     */

  17.    @Override

  18.    public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri,

  19.                                         MultiValueMap<String, String> parameters) {

  20.        StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl);

  21.        accessTokenRequestUrl.append("?appid="+clientId);

  22.        accessTokenRequestUrl.append("&secret="+clientSecret);

  23.        accessTokenRequestUrl.append("&code="+authorizationCode);

  24.        accessTokenRequestUrl.append("&grant_type=authorization_code");

  25.        accessTokenRequestUrl.append("&redirect_uri="+redirectUri);

  26.        return getAccessToken(accessTokenRequestUrl);

  27.    }

  28.    public AccessGrant refreshAccess(String refreshToken, MultiValueMap<String, String> additionalParameters) {

  29.        StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL);

  30.        refreshTokenUrl.append("?appid="+clientId);

  31.        refreshTokenUrl.append("&grant_type=refresh_token");

  32.        refreshTokenUrl.append("&refresh_token="+refreshToken);

  33.        return getAccessToken(refreshTokenUrl);

  34.    }

  35.    @SuppressWarnings("unchecked")

  36.    private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {

  37.        log.info("获取access_token, 请求URL: "+accessTokenRequestUrl.toString());

  38.        String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);

  39.        log.info("获取access_token, 响应内容: "+response);

  40.        Map<String, Object> result = null;

  41.        try {

  42.            result = new ObjectMapper().readValue(response, Map.class);

  43.        } catch (Exception e) {

  44.            e.printStackTrace();

  45.        }

  46.        //返回错误码时直接返回空

  47.        if(StringUtils.isNotBlank(MapUtils.getString(result, "errcode"))){

  48.            String errcode = MapUtils.getString(result, "errcode");

  49.            String errmsg = MapUtils.getString(result, "errmsg");

  50.            throw new RuntimeException("获取access token失败, errcode:"+errcode+", errmsg:"+errmsg);

  51.        }

  52.        WeixinAccessGrant accessToken = new WeixinAccessGrant(

  53.                MapUtils.getString(result, "access_token"),

  54.                MapUtils.getString(result, "scope"),

  55.                MapUtils.getString(result, "refresh_token"),

  56.                MapUtils.getLong(result, "expires_in"));

  57.        accessToken.setOpenId(MapUtils.getString(result, "openid"));

  58.        return accessToken;

  59.    }

  60.    /**

  61.     * 构建获取授权码的请求。也就是引导用户跳转到微信的地址。

  62.     */

  63.    public String buildAuthenticateUrl(OAuth2Parameters parameters) {

  64.        String url = super.buildAuthenticateUrl(parameters);

  65.        url = url + "&appid="+clientId+"&scope=snsapi_login";

  66.        return url;

  67.    }

  68.    public String buildAuthorizeUrl(OAuth2Parameters parameters) {

  69.        return buildAuthenticateUrl(parameters);

  70.    }

  71.    /**

  72.     * 微信返回的contentType是html/text,添加相应的HttpMessageConverter来处理。

  73.     */

  74.    protected RestTemplate createRestTemplate() {

  75.        RestTemplate restTemplate = super.createRestTemplate();

  76.        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));

  77.        return restTemplate;

  78.    }

  79. }

与 QQ处理令牌类相比多了三个全局变量并且复写了 exchangeForAccess方法。这是因为 微信在通过 code获取 access_token是传递的参数是 appid和 secret而不是标准的 client_id和 client_secret。

WeixinServiceProvider连接服务提供商

  1. public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {

  2.    /**

  3.     * 微信获取授权码的url

  4.     */

  5.    private static final String WEIXIN_URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";

  6.    /**

  7.     * 微信获取accessToken的url(微信在获取accessToken时也已经返回openId)

  8.     */

  9.    private static final String WEIXIN_URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";

  10.    public WeixinServiceProvider(String appId, String appSecret) {

  11.        super(new WeixinOAuth2Template(appId, appSecret, WEIXIN_URL_AUTHORIZE, WEIXIN_URL_ACCESS_TOKEN));

  12.    }

  13.    @Override

  14.    public Weixin getApi(String accessToken) {

  15.        return new WeiXinImpl(accessToken);

  16.    }

  17. }

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

  1. public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> {

  2.    /**

  3.     * @param appId

  4.     * @param appSecret

  5.     */

  6.    public WeixinConnectionFactory(String providerId, String appId, String appSecret) {

  7.        super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter());

  8.    }

  9.    /**

  10.     * 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取

  11.     */

  12.    @Override

  13.    protected String extractProviderUserId(AccessGrant accessGrant) {

  14.        if(accessGrant instanceof WeixinAccessGrant) {

  15.            return ((WeixinAccessGrant)accessGrant).getOpenId();

  16.        }

  17.        return null;

  18.    }

  19.    /* (non-Javadoc)

  20.     * @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant)

  21.     */

  22.    public Connection<Weixin> createConnection(AccessGrant accessGrant) {

  23.        return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),

  24.                accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));

  25.    }

  26.    /* (non-Javadoc)

  27.     * @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.connect.ConnectionData)

  28.     */

  29.    public Connection<Weixin> createConnection(ConnectionData data) {

  30.        return new OAuth2Connection<Weixin>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));

  31.    }

  32.    private ApiAdapter<Weixin> getApiAdapter(String providerUserId) {

  33.        return new WeixinAdapter(providerUserId);

  34.    }

  35.    private OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() {

  36.        return (OAuth2ServiceProvider<Weixin>) getServiceProvider();

  37.    }

  38. }

WeixinAdapter将微信api返回的数据模型适配Spring Social的标准模型

  1. public class WeixinAdapter implements ApiAdapter<Weixin> {

  2.    private String openId;

  3.    public WeixinAdapter() {

  4.    }

  5.    public WeixinAdapter(String openId) {

  6.        this.openId = openId;

  7.    }

  8.    @Override

  9.    public boolean test(Weixin api) {

  10.        return true;

  11.    }

  12.    @Override

  13.    public void setConnectionValues(Weixin api, ConnectionValues values) {

  14.        WeixinUserInfo userInfo = api.getUserInfo(openId);

  15.        values.setProviderUserId(userInfo.getOpenid());

  16.        values.setDisplayName(userInfo.getNickname());

  17.        values.setImageUrl(userInfo.getHeadimgurl());

  18.    }

  19.    @Override

  20.    public UserProfile fetchUserProfile(Weixin api) {

  21.        return null;

  22.    }

  23.    @Override

  24.    public void updateStatus(Weixin api, String message) {

  25.    }

  26. }

WeixinAuthConfig创建工厂和设置数据源

  1. @Configuration

  2. public class WeixinAuthConfig extends SocialAutoConfigurerAdapter {

  3.    @Autowired

  4.    private DataSource dataSource;

  5.    @Autowired

  6.    private ConnectionSignUp myConnectionSignUp;

  7.    @Override

  8.    protected ConnectionFactory<?> createConnectionFactory() {

  9.        return new WeixinConnectionFactory(DEFAULT_SOCIAL_WEIXIN_PROVIDER_ID, SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_ID,

  10.                SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_SECRET);

  11.    }

  12.    @Override

  13.    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {

  14.        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,

  15.                connectionFactoryLocator, Encryptors.noOpText());

  16.        if (myConnectionSignUp != null) {

  17.            repository.setConnectionSignUp(myConnectionSignUp);

  18.        }

  19.        return repository;

  20.    }

  21.    /**

  22.     * /connect/weixin POST请求,绑定微信返回connect/weixinConnected视图

  23.     * /connect/weixin DELETE请求,解绑返回connect/weixinConnect视图

  24.     * @return

  25.     */

  26.    @Bean({"connect/weixinConnect", "connect/weixinConnected"})

  27.    @ConditionalOnMissingBean(name = "weixinConnectedView")

  28.    public View weixinConnectedView() {

  29.        return new SocialConnectView();

  30.    }

  31. }

社交登录配置类

由于社交登录都是通过 SocialAuthenticationFilter过滤器拦截的,如果 上一章 已经配置过,则本章不需要配置。

效果如下:

代码下载

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

推荐系列:

https://mp.weixin.qq.com/s?__biz=MzU0MDEwMjgwNA==&mid=2247484233&idx=1&sn=1e84ffd8c9169db56a0d48ccb31bc842&chksm=fb3f1ab2cc4893a4263799c466d73ee67971ce9deb22a91b8ae8e968621679de3bce83a2c558&mpshare=1&scene=24&srcid=0119R1KE5Q7t4Ym1RERJzexH#rd

Spring Security 源码分析(四):Spring Social实现微信社交登录的更多相关文章

  1. Spring Security 源码分析 --- WebSecurity

    概述 spring security 源码分析系列文章. 源码分析 我们想一下,我们使用 ss 框架的步骤是怎么样的. @Configuration @EnableWebSecurity @Enabl ...

  2. spring security源码分析之core包

    Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring I ...

  3. spring security源码分析心得

    看了半天的文档及源码,终于理出了spring-security的一些总体思路,spring security主要分认证(authentication)和授权(authority). 1.认证authe ...

  4. spring security源码分析之web包分析

    Spring 是一个非常流行和成功的 Java 应用开发框架.Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案.一般来说,Web 应用的安全性包括 ...

  5. spring security源码分析之一springSecurityFilterChain

    1. spring和spring security的集成,配置web.xml如下: <context-param> <param-name>contextConfigLocat ...

  6. 精尽Spring MVC源码分析 - HandlerMapping 组件(四)之 AbstractUrlHandlerMapping

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  7. 精尽Spring MVC源码分析 - HandlerAdapter 组件(四)之 HandlerMethodReturnValueHandler

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  8. Spring Ioc源码分析系列--容器实例化Bean的四种方法

    Spring Ioc源码分析系列--实例化Bean的几种方法 前言 前面的文章Spring Ioc源码分析系列--Bean实例化过程(二)在讲解到bean真正通过那些方式实例化出来的时候,并没有继续分 ...

  9. Spring mvc源码分析系列--Servlet的前世今生

    Spring mvc源码分析系列--Servlet的前世今生 概述 上一篇文章Spring mvc源码分析系列--前言挖了坑,但是由于最近需求繁忙,一直没有时间填坑.今天暂且来填一个小坑,这篇文章我们 ...

随机推荐

  1. 苹果新的编程语言 Swift 语言进阶(十六)--泛型

    泛型允许你定义一个宽松.可重用的函数或者类型,使用泛型能够避免代码的重复,也能以更清楚和抽象的方式来表达程序的意图. 泛型是Swift语言提供的强大功能之一,Swift提供的许多标准库都使用了泛型来创 ...

  2. ATPCS

    ATPCS是: Arm Thumb procedure Call Standard的缩写.意思是arm thumb子程序调用规范.      C语言函数与C函数之间进行调用是用同一个C函数调用方式进行 ...

  3. ORACLE ERP各模块会计分录

      ORACLE ERP各模块会计分录   第一章 采购模块 一.资产采购(科目来源:库存组织) 1.物料接收 借  材料采购     接收数量*采购单价 贷 应计暂估     接收数量*采购单价 2 ...

  4. C++语言之动态内存分配

    在C语言中,我们熟悉的内存分配与释放的最常用的接口分别是malloc , free .在C++中: 存在着更加方便的动态存储分配: 1.new 和delete 机制,new 它能更可靠控制存储区的分配 ...

  5. C# 如何在PDF文档中创建表格

    表格能够直观的传达数据信息,使信息显得条理化,便于阅读同时也利于管理.那在PDF类型的文档中如何来添加表格并且对表格进行格式化操作呢?使用常规方法直接在PDF中添加表格行不通,那我们可以在借助第三方组 ...

  6. redis简单主从复制

    两台ubuntu 云服务器,分别redis主从服务器,ip地址是:123.207.96.138(主)139.199.167.251(从) 安装redis,在这里我建议给redis设置密码,之前看过一篇 ...

  7. LeetCode刷题之合并排序链表

    合并两个有序链表并返回一个新的列表.新列表应该由连接在一起的节点前两个列表 给定实例:Input: 1->2->4, 1->3->4Output: 1->1->2- ...

  8. maven技术(一)软件安装与配置

    maven技术在研发的过程中,作为资源依赖管理非常出色,例如在Java项目开发过程中,需要各种各样jar包,一般情况下开发者会直接将所用到的jar包放在project的lib目录下,提供自己程序调用. ...

  9. C语言关键字register、extern、static、一些总结,及项目中使用的心得

    首先介绍两个概念: 一.变量的生存周期: 变量从建立到撤销的时间段成变量的生存周期.静态变量,从变量产生到整个程序执行结束.当函数使用变量结束后,变量的存储空间依然存在,变量的值也会随着函数的对其的使 ...

  10. JSF-受管Bean与EL表达式

    受管Bean与EL表达式 1)编写Bean:①有一个不带形参的构造方法 ②getXxx.setXxx ③一般要实现io.Serializable接口 2)声明受管Bean:①bean名称为外界访问其属 ...