Spring Security 源码分析(四):Spring Social实现微信社交登录
社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ、人人网、开心网、新浪微博、搜狐微博、腾讯微博、淘宝、豆瓣、MSN、Google等社会化媒体账号登录该网站。
前言
在上一章Spring-Security源码分析三-Spring-Social社交登录过程中,我们已经实现了使用 SpringSocial+ Security的QQ社交登录。本章我们将实现微信的社交登录。(微信和QQ登录的大体流程相同,但存在一些细节上的差异,下面我们来简单实现一下)
准备工作
熟悉OAuth2.0协议标准,微信登录是基于OAuth2.0中的authorization_code模式的授权登录;
微信开放平台申请网站应用开发,获取
appid和appsecret熟读网站应用微信登录开发指南
参考Spring-Security源码分析三-Spring-Social社交登录过程的准备工作
为了方便大家测试,博主在某宝租用了一个月的appid和appSecret
| appid | wxfd6965ab1fc6adb2 |
|---|---|
| appsecret | 66bb4566de776ac699ec1dbed0cc3dd1 |
目录结构
参考
api定义api绑定的公共接口config微信的一些配置信息connect与服务提供商建立连接所需的一些类。
定义返回用户信息接口
public interface Weixin {WeixinUserInfo getUserInfo(String openId);}
这里我们看到相对于QQ的 getUserInfo微信多了一个参数 openId。这是因为微信文档中在OAuth2.0的认证流程示意图第五步时,微信的 openid 同 access_token一起返回。而 SpringSocial获取 access_token的类 AccessGrant.java中没有 openid。因此我们自己需要扩展一下 SpringSocial获取令牌的类( AccessGrant.java);
处理微信返回的access_token类(添加openid)
@Datapublic class WeixinAccessGrant extends AccessGrant{private String openId;public WeixinAccessGrant() {super("");}public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {super(accessToken, scope, refreshToken, expiresIn);}}
实现返回用户信息接口
public class WeiXinImpl extends AbstractOAuth2ApiBinding implements Weixin {/*** 获取用户信息的url*/private static final String WEIXIN_URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";private ObjectMapper objectMapper = new ObjectMapper();public WeiXinImpl(String accessToken) {super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);}/*** 获取用户信息** @param openId* @return*/@Overridepublic WeixinUserInfo getUserInfo(String openId) {String url = WEIXIN_URL_GET_USER_INFO + openId;String result = getRestTemplate().getForObject(url, String.class);if(StringUtils.contains(result, "errcode")) {return null;}WeixinUserInfo userInfo = null;try{userInfo = objectMapper.readValue(result,WeixinUserInfo.class);}catch (Exception e){e.printStackTrace();}return userInfo;}/*** 使用utf-8 替换默认的ISO-8859-1编码* @return*/@Overrideprotected List<HttpMessageConverter<?>> getMessageConverters() {List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();messageConverters.remove(0);messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));return messageConverters;}}
与 QQ获取用户信息相比, 微信的实现类中少了一步通过 access_token获取 openid的请求。 openid由自己定义的扩展类 WeixinAccessGrant中获取;
WeixinOAuth2Template处理微信返回的令牌信息
@Slf4jpublic class WeixinOAuth2Template extends OAuth2Template {private String clientId;private String clientSecret;private String accessTokenUrl;private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {super(clientId, clientSecret, authorizeUrl, accessTokenUrl);setUseParametersForClientAuthentication(true);this.clientId = clientId;this.clientSecret = clientSecret;this.accessTokenUrl = accessTokenUrl;}/* (non-Javadoc)* @see org.springframework.social.oauth2.OAuth2Template#exchangeForAccess(java.lang.String, java.lang.String, org.springframework.util.MultiValueMap)*/@Overridepublic AccessGrant exchangeForAccess(String authorizationCode, String redirectUri,MultiValueMap<String, String> parameters) {StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl);accessTokenRequestUrl.append("?appid="+clientId);accessTokenRequestUrl.append("&secret="+clientSecret);accessTokenRequestUrl.append("&code="+authorizationCode);accessTokenRequestUrl.append("&grant_type=authorization_code");accessTokenRequestUrl.append("&redirect_uri="+redirectUri);return getAccessToken(accessTokenRequestUrl);}public AccessGrant refreshAccess(String refreshToken, MultiValueMap<String, String> additionalParameters) {StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL);refreshTokenUrl.append("?appid="+clientId);refreshTokenUrl.append("&grant_type=refresh_token");refreshTokenUrl.append("&refresh_token="+refreshToken);return getAccessToken(refreshTokenUrl);}@SuppressWarnings("unchecked")private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {log.info("获取access_token, 请求URL: "+accessTokenRequestUrl.toString());String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);log.info("获取access_token, 响应内容: "+response);Map<String, Object> result = null;try {result = new ObjectMapper().readValue(response, Map.class);} catch (Exception e) {e.printStackTrace();}//返回错误码时直接返回空if(StringUtils.isNotBlank(MapUtils.getString(result, "errcode"))){String errcode = MapUtils.getString(result, "errcode");String errmsg = MapUtils.getString(result, "errmsg");throw new RuntimeException("获取access token失败, errcode:"+errcode+", errmsg:"+errmsg);}WeixinAccessGrant accessToken = new WeixinAccessGrant(MapUtils.getString(result, "access_token"),MapUtils.getString(result, "scope"),MapUtils.getString(result, "refresh_token"),MapUtils.getLong(result, "expires_in"));accessToken.setOpenId(MapUtils.getString(result, "openid"));return accessToken;}/*** 构建获取授权码的请求。也就是引导用户跳转到微信的地址。*/public String buildAuthenticateUrl(OAuth2Parameters parameters) {String url = super.buildAuthenticateUrl(parameters);url = url + "&appid="+clientId+"&scope=snsapi_login";return url;}public String buildAuthorizeUrl(OAuth2Parameters parameters) {return buildAuthenticateUrl(parameters);}/*** 微信返回的contentType是html/text,添加相应的HttpMessageConverter来处理。*/protected RestTemplate createRestTemplate() {RestTemplate restTemplate = super.createRestTemplate();restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));return restTemplate;}}
与 QQ处理令牌类相比多了三个全局变量并且复写了 exchangeForAccess方法。这是因为 微信在通过 code获取 access_token是传递的参数是 appid和 secret而不是标准的 client_id和 client_secret。
WeixinServiceProvider连接服务提供商
public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {/*** 微信获取授权码的url*/private static final String WEIXIN_URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";/*** 微信获取accessToken的url(微信在获取accessToken时也已经返回openId)*/private static final String WEIXIN_URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";public WeixinServiceProvider(String appId, String appSecret) {super(new WeixinOAuth2Template(appId, appSecret, WEIXIN_URL_AUTHORIZE, WEIXIN_URL_ACCESS_TOKEN));}@Overridepublic Weixin getApi(String accessToken) {return new WeiXinImpl(accessToken);}}
WeixinConnectionFactory连接服务提供商的工厂类
public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> {/*** @param appId* @param appSecret*/public WeixinConnectionFactory(String providerId, String appId, String appSecret) {super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter());}/*** 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取*/@Overrideprotected String extractProviderUserId(AccessGrant accessGrant) {if(accessGrant instanceof WeixinAccessGrant) {return ((WeixinAccessGrant)accessGrant).getOpenId();}return null;}/* (non-Javadoc)* @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant)*/public Connection<Weixin> createConnection(AccessGrant accessGrant) {return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));}/* (non-Javadoc)* @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.connect.ConnectionData)*/public Connection<Weixin> createConnection(ConnectionData data) {return new OAuth2Connection<Weixin>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));}private ApiAdapter<Weixin> getApiAdapter(String providerUserId) {return new WeixinAdapter(providerUserId);}private OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() {return (OAuth2ServiceProvider<Weixin>) getServiceProvider();}}
WeixinAdapter将微信api返回的数据模型适配Spring Social的标准模型
public class WeixinAdapter implements ApiAdapter<Weixin> {private String openId;public WeixinAdapter() {}public WeixinAdapter(String openId) {this.openId = openId;}@Overridepublic boolean test(Weixin api) {return true;}@Overridepublic void setConnectionValues(Weixin api, ConnectionValues values) {WeixinUserInfo userInfo = api.getUserInfo(openId);values.setProviderUserId(userInfo.getOpenid());values.setDisplayName(userInfo.getNickname());values.setImageUrl(userInfo.getHeadimgurl());}@Overridepublic UserProfile fetchUserProfile(Weixin api) {return null;}@Overridepublic void updateStatus(Weixin api, String message) {}}
WeixinAuthConfig创建工厂和设置数据源
@Configurationpublic class WeixinAuthConfig extends SocialAutoConfigurerAdapter {@Autowiredprivate DataSource dataSource;@Autowiredprivate ConnectionSignUp myConnectionSignUp;@Overrideprotected ConnectionFactory<?> createConnectionFactory() {return new WeixinConnectionFactory(DEFAULT_SOCIAL_WEIXIN_PROVIDER_ID, SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_ID,SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_SECRET);}@Overridepublic UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,connectionFactoryLocator, Encryptors.noOpText());if (myConnectionSignUp != null) {repository.setConnectionSignUp(myConnectionSignUp);}return repository;}/*** /connect/weixin POST请求,绑定微信返回connect/weixinConnected视图* /connect/weixin DELETE请求,解绑返回connect/weixinConnect视图* @return*/@Bean({"connect/weixinConnect", "connect/weixinConnected"})@ConditionalOnMissingBean(name = "weixinConnectedView")public View weixinConnectedView() {return new SocialConnectView();}}
社交登录配置类
由于社交登录都是通过 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实现微信社交登录的更多相关文章
- Spring Security 源码分析 --- WebSecurity
概述 spring security 源码分析系列文章. 源码分析 我们想一下,我们使用 ss 框架的步骤是怎么样的. @Configuration @EnableWebSecurity @Enabl ...
- spring security源码分析之core包
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring I ...
- spring security源码分析心得
看了半天的文档及源码,终于理出了spring-security的一些总体思路,spring security主要分认证(authentication)和授权(authority). 1.认证authe ...
- spring security源码分析之web包分析
Spring 是一个非常流行和成功的 Java 应用开发框架.Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案.一般来说,Web 应用的安全性包括 ...
- spring security源码分析之一springSecurityFilterChain
1. spring和spring security的集成,配置web.xml如下: <context-param> <param-name>contextConfigLocat ...
- 精尽Spring MVC源码分析 - HandlerMapping 组件(四)之 AbstractUrlHandlerMapping
该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...
- 精尽Spring MVC源码分析 - HandlerAdapter 组件(四)之 HandlerMethodReturnValueHandler
该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...
- Spring Ioc源码分析系列--容器实例化Bean的四种方法
Spring Ioc源码分析系列--实例化Bean的几种方法 前言 前面的文章Spring Ioc源码分析系列--Bean实例化过程(二)在讲解到bean真正通过那些方式实例化出来的时候,并没有继续分 ...
- Spring mvc源码分析系列--Servlet的前世今生
Spring mvc源码分析系列--Servlet的前世今生 概述 上一篇文章Spring mvc源码分析系列--前言挖了坑,但是由于最近需求繁忙,一直没有时间填坑.今天暂且来填一个小坑,这篇文章我们 ...
随机推荐
- Cocos2d-swift V3.x 中的update方法
在cocos2d V3.x中update方法如果实现,则会被自动调用;不用向早期的版本那样要显式schedule. 但是你还是要显式schedule其他方法或blocks使用node的schedule ...
- Android BLE与终端通信(二)——Android Bluetooth基础科普以及搜索蓝牙设备显示列表
Android BLE与终端通信(二)--Android Bluetooth基础搜索蓝牙设备显示列表 摘要 第一篇算是个热身,这一片开始来写些硬菜了,这篇就是实际和蓝牙打交道了,所以要用到真机调试哟, ...
- openresty+websocket+redis simple chat
openresty 很早就支持websocket了,但是早期的版本cosocket是单工的,处理起来比较麻烦参见邮件列表讨论 websocket chat,后来的版本cosocket是双全工的,就可以 ...
- javascript中的eval函数
eval()只有一个参数,如果传入的参数不是字符串,则直接返回这个参数.否则会将字符串当成js代码进行编译,如果编译失败则抛出语法错误(SyntaxError)异常.如果编译成功则开始执行这段代码,并 ...
- sql——查询出表中不为空或为空字段的总值数
查询所给的表中值为空的总数 判断字段是否为空的sql语句 SELECT sex FROM id where sex is not NULL SELECT COUNT(*) t FROM id wher ...
- c语言,文件操作总结
C语言文件操作 一.标准文件的读写 1.文件的打开 fopen() 文件的打开操作表示将给用户指定的文件在内存分配一个FILE结构区,并将该结构的指针返回给用户程序,以后用户程序就可用此FILE指针来 ...
- PyCOn2013大会笔记
DAE的设计 By洪强宁 hongon@douban.com 3个aaS服务都不能模块化灵活组合服务 DAE的起因:代码横向拆分模块化,重用基础设施 最佳实践对新App复用 Scale SA D ...
- Ocelot中文文档-负载均衡
Ocelot能通过可用的下游服务对每个ReRoute进行负载平衡. 这意味着您可以扩展您的下游服务,并且Ocelot可以有效地使用它们. 可用的负载均衡器的类型是: LeastConnection - ...
- [译文]Domain Driven Design Reference(一)—— 前言
本书是Eric Evans对他自己写的<领域驱动设计-软件核心复杂性应对之道>的一本字典式的参考书,可用于快速查找<领域驱动设计>中的诸多概念及其简明解释. DDD到目前为止知 ...
- 使用ASP.NET SignalR实现一个简单的聊天室
前言 距离我写上一篇博客已经又过了一年半载了,时间过得很快,一眨眼,就把人变得沧桑了许多.青春是短暂的,知识是无限的.要用短暂的青春,去学无穷无尽的知识,及时当勉励,岁月不待人.今天写个随笔小结记录一 ...