2021-05-29 15:30  阅读(176)
文章分类:Spring Security 源码分析 文章标签:Spring SecuritySpring Security 源码
©  原文作者: 郑龙飞 原文地址:https://juejin.cn/user/4212984285237870

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

前言

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

准备工作

  1. 熟悉OAuth2.0协议标准,微信登录是基于OAuth2.0中的authorization_code模式的授权登录;
  2. 微信开放平台申请网站应用开发,获取appidappsecret
  3. 熟读网站应用微信登录开发指南
  4. 参考Spring-Security源码分析三-Spring-Social社交登录过程的准备工作

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

appid wxfd6965ab1fc6adb2
appsecret 66bb4566de776ac699ec1dbed0cc3dd1

目录结构

202105291530259151.png https://user-gold-cdn.xitu.io/2018/1/12/160e8a694ca55e2a?w=398&h=449&f=png&s=31567

参考

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

定义返回用户信息接口

    public interface Weixin {
        WeixinUserInfo getUserInfo(String openId);
    }
    复制代码

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

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

    @Data
    public 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
         */
        @Override
        public 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
         */
        @Override
        protected 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处理微信返回的令牌信息

    @Slf4j
    public 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)
         */
        @Override
        public 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是传递的参数是appidsecret而不是标准client_idclient_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));
        }
    
        @Override
        public 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来获取
         */
        @Override
        protected 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;
        }
    
        @Override
        public boolean test(Weixin api) {
            return true;
        }
    
        @Override
        public void setConnectionValues(Weixin api, ConnectionValues values) {
            WeixinUserInfo userInfo = api.getUserInfo(openId);
            values.setProviderUserId(userInfo.getOpenid());
            values.setDisplayName(userInfo.getNickname());
            values.setImageUrl(userInfo.getHeadimgurl());
        }
    
        @Override
        public UserProfile fetchUserProfile(Weixin api) {
            return null;
        }
    
        @Override
        public void updateStatus(Weixin api, String message) {
    
        }
    }
    复制代码

WeixinAuthConfig创建工厂和设置数据源

    @Configuration
    public class WeixinAuthConfig extends SocialAutoConfigurerAdapter {
    
        @Autowired
        private DataSource dataSource;
    
        @Autowired
        private ConnectionSignUp myConnectionSignUp;
    
        @Override
        protected ConnectionFactory<?> createConnectionFactory() {
            return new WeixinConnectionFactory(DEFAULT_SOCIAL_WEIXIN_PROVIDER_ID, SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_ID,
                    SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_SECRET);
        }
    
        @Override
        public 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过滤器拦截的,如果 上一章 已经配置过,则本章不需要配置。

效果如下:

202105291530260612.png https://user-gold-cdn.xitu.io/2018/1/12/160e8a694d2ca6ed?w=1012&h=762&f=gif&s=1009108

代码下载

从我的 github 中下载,github.com/longfeizhen…

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> Spring Security源码分析四:Spring Social实现微信社交登录
上一篇
Spring Security源码分析三:Spring Social实现QQ社交登录
下一篇
Spring Security源码分析五:Spring Security实现短信登录