微信登陆和QQ登陆大致流程一致,只是有些api不一样,主要是QQ的getUserInfo微信多了一个参数openId。这是因为文档中在OAuth2.0的认证流程示意图第五步时,微信的openid 同access_token一起返回。而Spring Social获取access_token的类AccessGrant.java中没有openid

微信暂时没有申请测试账户,先上代码后续网站备案下来再自行测试

这里也分三个模块进行开发  api  connect config

api

package com.rui.tiger.auth.core.social.wechat.api;

/**
 * 微信用户api接口
 * @author CaiRui
 * @Date 2019-01-12 12:08
 */
public interface WechatApi {
    /**
     *  获取微信用户信息
     * @param openId
     * @return
     */
    WechatUserInfo getUserInfo(String openId);
}
package com.rui.tiger.auth.core.social.wechat.api;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;

import java.nio.charset.Charset;
import java.util.List;

/**
 * @author CaiRui
 * @Date 2019-01-12 12:24
 */
@Slf4j
public class WechatApiImpl extends AbstractOAuth2ApiBinding implements WechatApi {

    /**
     * 获取用户信息的url
     * https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
     * access_token 父类会帮我们拼接
     */
    private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";

    /**
     * @param accessToken
     */
    public WechatApiImpl(String accessToken) {
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
    }

    /**
     * 默认注册的StringHttpMessageConverter字符集为ISO-8859-1,而微信返回的是UTF-8的,所以覆盖了原来的方法。
     */
    protected List<HttpMessageConverter<?>> getMessageConverters() {
        List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
        messageConverters.remove(0);
        messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return messageConverters;
    }

    @Override
    public WechatUserInfo getUserInfo(String openId) {
        String url = URL_GET_USER_INFO + openId;
        String response = getRestTemplate().getForObject(url, String.class);
        if(StringUtils.contains(response, "errcode")) {
            log.info("微信用户信息获取失败:"+response);
            return null;
        }
        WechatUserInfo  profile = null;
        try {
            profile = JSON.parseObject(response, WechatUserInfo.class);
        } catch (Exception e) {
            log.info("微信用户信息json转换异常",e);
        }
        return profile;
    }
}

微信社交用户信息封装

package com.rui.tiger.auth.core.social.wechat.api;

import lombok.Data;

/**
 * 微信用户信息
 * https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316518&token=&lang=zh_CN}
 * @author CaiRui
 * @Date 2019-01-12 12:13
 */
@Data
public class WechatUserInfo {

    /**
     * 普通用户的标识,对当前开发者帐号唯一
     */
    private String openid;
    /**
     * 普通用户昵称
     */
    private String nickname;
    /**
     * 语言
     */
    private String language;
    /**
     * 普通用户性别,1为男性,2为女性
     */
    private String sex;
    /**
     * 普通用户个人资料填写的省份
     */
    private String province;
    /**
     * 普通用户个人资料填写的城市
     */
    private String city;
    /**
     * 国家,如中国为CN
     */
    private String country;
    /**
     * 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
     */
    private String headimgurl;
    /**
     * 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
     */
    private String[] privilege;
    /**
     * 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。
     */
    private String unionid;

}

connect 

微信的返回比标准的多了个openId

package com.rui.tiger.auth.core.social.wechat.connect;

import org.springframework.social.oauth2.AccessGrant;

/**
 * 微信的access_token信息。与标准OAuth2协议不同,微信在获取access_token时会同时返回openId,并没有单独的通过accessToke换取openId的服务
 * 所以在这里继承了标准AccessGrant,添加了openId字段,作为对微信access_token信息的封装。
 * @author CaiRui
 * @Date 2019-01-12 13:08
 */
public class WechatAccessGrant extends AccessGrant {

    private String openId;

    public WechatAccessGrant() {
        super("");
    }

    public WechatAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
        super(accessToken, scope, refreshToken, expiresIn);
    }

    /**
     * @return the openId
     */
    public String getOpenId() {
        return openId;
    }

    /**
     * @param openId the openId to set
     */
    public void setOpenId(String openId) {
        this.openId = openId;
    }
}

api适配器

package com.rui.tiger.auth.core.social.wechat.connect;

import com.rui.tiger.auth.core.social.wechat.api.WechatApi;
import com.rui.tiger.auth.core.social.wechat.api.WechatUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;

/**
 * 微信 api适配器,将微信 api的数据模型转为spring social的标准模型。
 * @author CaiRui
 * @Date 2019-01-12 13:34
 */
public class WechatApiAdapter implements ApiAdapter<WechatApi> {
    private String openId;

    public WechatApiAdapter() {}

    public WechatApiAdapter(String openId){
        this.openId = openId;
    }

    /**
     * @param api
     * @return
     */
    @Override
    public boolean test(WechatApi api) {
        return true;
    }

    /**
     * @param api
     * @param values
     */
    @Override
    public void setConnectionValues(WechatApi api, ConnectionValues values) {
        WechatUserInfo profile = api.getUserInfo(openId);
        values.setProviderUserId(profile.getOpenid());
        values.setDisplayName(profile.getNickname());
        values.setImageUrl(profile.getHeadimgurl());
    }

    /**
     * @param api
     * @return
     */
    @Override
    public UserProfile fetchUserProfile(WechatApi api) {
        return null;
    }

    /**
     * @param api
     * @param message
     */
    @Override
    public void updateStatus(WechatApi api, String message) {
        //do nothing
    }
}
package com.rui.tiger.auth.core.social.wechat.connect;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Parameters;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.Charset;
import java.util.Map;

/**
 * 完成微信的OAuth2认证流程的模板类。国内厂商实现的OAuth2每个都不同,
 * spring默认提供的OAuth2Template适应不了,只能针对每个厂商自己微调。
 * @author CaiRui
 * @Date 2019-01-12 13:23
 */
@Slf4j
public class WechatOAuth2Template extends OAuth2Template {

    private String clientId;

    private String clientSecret;

    private String accessTokenUrl;
    /**
     * 获取token
     * https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
     */
    private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";

    public WechatOAuth2Template(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 = JSON.parseObject(response, Map.class);
        } catch (Exception e) {
            log.error("微信获取token解析json异常",e);
        }

        //返回错误码时直接返回空
        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);
        }

        WechatAccessGrant  accessToken = new WechatAccessGrant(
                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;
    }

    /**
     * 构建获取授权码的请求。也就是引导用户跳转到微信的地址。
     * https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN
     */
    public String buildAuthenticateUrl(OAuth2Parameters parameters) {
        String url = super.buildAuthenticateUrl(parameters);
        url=url.replace("client_id","appid");
        //url = url + "&appid="+clientId+"&scope=snsapi_login";
        url = url + "&scope=snsapi_login";
        log.info("微信获取授权码地址url:"+url);
        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;
    }

}
package com.rui.tiger.auth.core.social.wechat.connect;

import com.rui.tiger.auth.core.social.wechat.api.WechatApi;
import com.rui.tiger.auth.core.social.wechat.api.WechatApiImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;

/**
 *  微信的OAuth2流程处理器的提供器,供spring social的connect体系调用
 * @author CaiRui
 * @Date 2019-01-12 13:40
 */
public class WechatServiceProvider extends AbstractOAuth2ServiceProvider<WechatApi> {

    /**
     * 微信获取授权码的url
     *
     * https://open.weixin.qq.com/connect/qrconnect?
     * appid=APPID&
     * redirect_uri=REDIRECT_URI&
     * response_type=code&
     * scope=SCOPE&
     * state=STATE#wechat_redirect
     */
    private static final String URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
    /**
     * 微信获取accessToken的url
     */
    private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";

    /**
     * @param appId
     * @param appSecret
     */
    public WechatServiceProvider(String appId, String appSecret) {
        super(new WechatOAuth2Template(appId, appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));
    }


    /* (non-Javadoc)
     * @see org.springframework.social.oauth2.AbstractOAuth2ServiceProvider#getApi(java.lang.String)
     */
    @Override
    public WechatApi getApi(String accessToken) {
        return new WechatApiImpl(accessToken);
    }

}
package com.rui.tiger.auth.core.social.wechat.connect;

import com.rui.tiger.auth.core.social.wechat.api.WechatApi;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.support.OAuth2Connection;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2ServiceProvider;

/**
 * @author CaiRui
 * @Date 2019-01-12 13:32
 */
public class WechatConnectionFactory extends OAuth2ConnectionFactory<WechatApi> {

    /**
     * @param appId
     * @param appSecret
     */
    public WechatConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new WechatServiceProvider(appId, appSecret), new WechatApiAdapter());
    }

    /**
     * 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取
     */
    @Override
    protected String extractProviderUserId(AccessGrant accessGrant) {
        if(accessGrant instanceof WechatAccessGrant) {
            return ((WechatAccessGrant)accessGrant).getOpenId();
        }
        return null;
    }

    /* (non-Javadoc)
     * @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant)
     */
    public Connection<WechatApi> createConnection(AccessGrant accessGrant) {
        return new OAuth2Connection<WechatApi>(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<WechatApi> createConnection(ConnectionData data) {
        return new OAuth2Connection<WechatApi>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
    }

    private ApiAdapter<WechatApi> getApiAdapter(String providerUserId) {
        return new WechatApiAdapter(providerUserId);
    }

    private OAuth2ServiceProvider<WechatApi> getOAuth2ServiceProvider() {
        return (OAuth2ServiceProvider<WechatApi>) getServiceProvider();
    }

}

config

package com.rui.tiger.auth.core.social.wechat.config;

import com.rui.tiger.auth.core.properties.SecurityProperties;
import com.rui.tiger.auth.core.properties.WechatProperties;
import com.rui.tiger.auth.core.social.wechat.connect.WechatConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;

/**
 * 微信登陆配置
 * @author CaiRui   extends SocialConfigurerAdapter
 * @Date 2019-01-12 13:57
 */
//@Configuration
//@ConditionalOnProperty(prefix = "tiger.auth.social.wechat", name = "app-id")
public class WechatAutoConfiguration extends SocialConfigurerAdapter {
 /*   @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
        connectionFactoryConfigurer.addConnectionFactory(createConnectionFactory());
    }



    private ConnectionFactory<?> createConnectionFactory() {
        WechatProperties weixinConfig = securityProperties.getSocial().getWechat();
        return new WechatConnectionFactory(weixinConfig.getProviderId(), weixinConfig.getAppId(),
                weixinConfig.getAppSecret());
    }


    // 后补:做到处理注册逻辑的时候发现的一个bug:登录完成后,数据库没有数据,但是再次登录却不用注册了
    // 就怀疑是否是在内存中存储了。结果果然发现这里父类的内存ConnectionRepository覆盖了SocialConfig中配置的jdbcConnectionRepository
    // 这里需要返回null,否则会返回内存的 ConnectionRepository
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        return null;
    }*/

}

配置文件调整

package com.rui.tiger.auth.core.properties;

/**
 * 微信配置文件
 * @author CaiRui
 * @Date 2019-01-12 14:00
 */
public class WechatProperties {

    /**
     * 第三方id,用来决定发起第三方登录的url,默认是 weixin。
     */
    private String providerId = "wechat";

    private String appId;//应用id

    private String appSecret;//应用密匙

    public String getProviderId() {
        return providerId;
    }

    public void setProviderId(String providerId) {
        this.providerId = providerId;
    }

    public String getAppId() {
        return appId;
    }

    public void setAppId(String appId) {
        this.appId = appId;
    }

    public String getAppSecret() {
        return appSecret;
    }

    public void setAppSecret(String appSecret) {
        this.appSecret = appSecret;
    }
}

放到社交配置中

springsecurity小程序登录 spring oauth2 微信登录_java

 添加微信相关配置

social:
        filterProcessesUrl: /auth
        qq:
          app-id: ***
          app-secret: ***
        wechat:
          app-id: ***
          app-secret: ***

前台界面添加微信登陆 tiger-login.html

<html>
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td>图形验证码:</td>
            <td>
                <input type="text" name="imageCode">
                <img src="/captcha/image">
            </td>
        </tr>
        <tr>
            <td colspan="2"><input type="checkbox" name="remember-me" value="true"/>记住我</td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>

<h3>短信登录</h3>
<form action="/authentication/mobile" method="post">
    <table>
        <tr>
            <td>手机号:</td>
            <td><input type="text" name="mobile" value="15026929536"></td>
        </tr>
        <tr>
            <td>短信验证码:</td>
            <td>
                <input type="text" name="smsCode">
                <a href="/captcha/sms?mobile=13012345678">发送验证码</a>
            </td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">登录</button></td>
        </tr>
    </table>
</form>

<!--<h3>社交登录</h3>
<!–不支持get请求  /auth/qq 默认是这个请求 /login/qq –>
<form action="/auth/qq" method="post">
    <button type="submit">QQ登录</button>
        
    <a href="/qqLogin/weixin">微信登录</a>
</form>-->

<h3>社交登录</h3>
<a href="/auth/qq">QQ登录</a>
    
<a href="/auth/wechat">微信登录</a>

</body>
</html>

ok 微信登陆的代码都写完了 ,等待后续测试