zoukankan      html  css  js  c++  java
  • Spring Security 源码分析(四):Spring Social实现微信社交登录

    社交登录又称作社会化登录(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

    appidwxfd6965ab1fc6adb2
    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

  • 相关阅读:
    Debug技巧
    SOA&微服务&服务网格&高可用
    缓存重点要点一览
    Mysql的变量一览
    计算机基本概念
    SpringMvc中获取Request
    空话大话汇集
    slf4j 作用及logback概述
    TensorFlow实战Google深度学习框架1-4章学习笔记
    Deep Learning.ai学习笔记_第五门课_序列模型
  • 原文地址:https://www.cnblogs.com/softidea/p/8335366.html
Copyright © 2011-2022 走看看