社交登录又称作社会化登录(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的认证流程示意图第五步时,微信的 openidaccess_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是传递的参数是 appidsecret而不是标准的 client_idclient_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. 如何在VS2013中进行Boost单元测试

    对于如何在VS2013中进行Boost单元测试,这方面资料太少.自己也因此走了不少弯路.下文将会阐述一下如何在VS2013中进行Boost单元测试. 在开始Boost单元测试之前,我们需要先安装VS2 ...

  2. Media Player Classic - HC 源代码分析 4:核心类 (CMainFrame)(3)

    ===================================================== Media Player Classic - HC 源代码分析系列文章列表: Media P ...

  3. 个人Source Insight使用设置笔记

    1.打开SourceInsight, 在菜单栏中点击Options-->Document Options 在显示的对话框中,点击Screen Fonts...., 可改变这个项目的字体,我选的是 ...

  4. 深入浅出web服务器与python应用程序之间的联系

    简单来说,Web服务器是在运行在物理服务器上的一个程序,它永久地等待客户端(主要是浏览器,比如Chrome,Firefox等)发送请求.Web 服务器接受 Http Request,返回 Respon ...

  5. 布局display属性(一)--【Flex】

    一.Flex 布局是什么? Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性. 任何一个容器都可以指定为 Flex 布局. .box ...

  6. nginx日志中添加请求的response日志

    换个新公司,做一些新鲜的事情,经过一天的琢磨,终于成功添加response日志 在nginx的日志中添加接口response的日志 由于此功能在nginx内置的功能中没有,需要安装第三方模块ngx_l ...

  7. 运行ant脚本(转载)

    http://blog.csdn.net/linwei_1029/article/details/5809801 运行ANT脚本的步骤 1.右击我的电脑-->属性-->高级-->环境 ...

  8. JAVA 综合面试题

    JAVA 综合面试题 2007-08-12 目录 TOC \o "1-3" \h \z \u Java面试题整理 9 Java面向对象 9 1. super()与this()的区别 ...

  9. 讲解Oracle面试过程中常见的二十个问题

    1.冷备份和热备份的不同点以及各自的优点     解答:热备份针对归档模式的数据库,在数据库仍旧处于工作状态时进行备份.而冷备份指在数据库关闭后,进行备份,适用于所有模式的数据库.热备份的优点在于当备 ...

  10. C4 垃圾回收

    使用C4垃圾回收器可以有效提升对低延迟有要求的企业级Java应用程序的伸缩性. 到目前为止,stop-the-world式的垃圾回收视为影响Java应用程序伸缩性的一大障碍,而伸缩性又是现代企业级Ja ...