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

原文地址:https://www.cnblogs.com/softidea/p/8335366.html