使用SpringSocial开发微信登录
⒈编写微信用户对应的数据结构
1 package cn.coreqi.social.weixin.entities; 2 3 /** 4 * 微信用户实体类 5 */ 6 public class WeixinUserInfo { 7 /** 8 * 普通用户的标识,对当前开发者账号唯一 9 */ 10 private String openid; 11 /** 12 * 普通用户昵称 13 */ 14 private String nickname; 15 16 /** 17 * 语言 18 */ 19 private String language; 20 21 /** 22 * 普通用户性别,1为男性,2为女性 23 */ 24 private String sex; 25 /** 26 * 普通用户个人资料填写的省份 27 */ 28 private String province; 29 /** 30 * 普通用户个人资料填写的城市 31 */ 32 private String city; 33 /** 34 * 国家,如中国为CN 35 */ 36 private String country; 37 /** 38 * 用户头像,最后一个数值代表正方形头像大小(有0,46,64,96,132数值可选,0代表640*640正方形) 39 */ 40 private String headimgurl; 41 /** 42 * 用户特权信息,json数组,如微信沃卡用户为(chinaunicom) 43 */ 44 private String[] privilege; 45 /** 46 * 用户统一标识,针对一个微信开放平台账号下的应用,同一用户的unionid是唯一的。 47 */ 48 private String unionid; 49 50 51 public String getOpenid() { 52 return openid; 53 } 54 55 public void setOpenid(String openid) { 56 this.openid = openid; 57 } 58 59 public String getNickname() { 60 return nickname; 61 } 62 63 public void setNickname(String nickname) { 64 this.nickname = nickname; 65 } 66 67 public String getLanguage() { 68 return language; 69 } 70 71 public void setLanguage(String language) { 72 this.language = language; 73 } 74 75 public String getSex() { 76 return sex; 77 } 78 79 public void setSex(String sex) { 80 this.sex = sex; 81 } 82 83 public String getProvince() { 84 return province; 85 } 86 87 public void setProvince(String province) { 88 this.province = province; 89 } 90 91 public String getCity() { 92 return city; 93 } 94 95 public void setCity(String city) { 96 this.city = city; 97 } 98 99 public String getCountry() { 100 return country; 101 } 102 103 public void setCountry(String country) { 104 this.country = country; 105 } 106 107 public String getHeadimgurl() { 108 return headimgurl; 109 } 110 111 public void setHeadimgurl(String headimgurl) { 112 this.headimgurl = headimgurl; 113 } 114 115 public String[] getPrivilege() { 116 return privilege; 117 } 118 119 public void setPrivilege(String[] privilege) { 120 this.privilege = privilege; 121 } 122 123 public String getUnionid() { 124 return unionid; 125 } 126 127 public void setUnionid(String unionid) { 128 this.unionid = unionid; 129 } 130 }
⒉编写一个微信API接口用于获取微信用户信息
1 package cn.coreqi.social.weixin.api; 2 3 4 import cn.coreqi.social.weixin.entities.WeixinUserInfo; 5 6 /** 7 * 微信API调用接口 8 */ 9 public interface Weixin { 10 11 /** 12 * 获取微信用户信息 13 * @param openId 14 * @return 15 */ 16 WeixinUserInfo getUserInfo(String openId); 17 }
⒊编写一个微信API接口实现
1 package cn.coreqi.social.weixin.api.impl; 2 3 import cn.coreqi.social.weixin.api.Weixin; 4 import cn.coreqi.social.weixin.entities.WeixinUserInfo; 5 import com.fasterxml.jackson.databind.ObjectMapper; 6 import org.apache.commons.lang.StringUtils; 7 import org.springframework.http.converter.HttpMessageConverter; 8 import org.springframework.http.converter.StringHttpMessageConverter; 9 import org.springframework.social.oauth2.AbstractOAuth2ApiBinding; 10 import org.springframework.social.oauth2.TokenStrategy; 11 12 import java.nio.charset.Charset; 13 import java.util.List; 14 15 /** 16 * 微信API调用模板,scope为Request的SpringBean,根据当前用户的accessToken创建。 17 */ 18 public class WeixinImpl extends AbstractOAuth2ApiBinding implements Weixin { 19 20 /** 21 * 用于序列化Json数据 22 */ 23 private ObjectMapper objectMapper = new ObjectMapper(); 24 25 /** 26 * 获取用户信息url 27 */ 28 private static final String URL_GET_USER_INFO="https://api.weixin.qq.com/sns/userinfo?openid="; 29 30 /** 31 * WeixinImpl构造器 32 * @param accessToken 33 */ 34 public WeixinImpl(String accessToken){ 35 super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER); 36 } 37 38 /** 39 * 默认注册的HttpMessageConverter字符集为ISO-8859-1,而微信返回的是UTF-8,因此必须覆盖原来的方法 40 * @return 41 */ 42 @Override 43 protected List<HttpMessageConverter<?>> getMessageConverters() { 44 List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters(); 45 messageConverters.remove(0); //删除StringHttpMessageConverter 46 messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); 47 return messageConverters; 48 } 49 50 /** 51 * 获取微信用户信息 52 * @param openId 53 * @return 54 */ 55 @Override 56 public WeixinUserInfo getUserInfo(String openId) { 57 String url = URL_GET_USER_INFO + openId; 58 String response = getRestTemplate().getForObject(url,String.class); 59 if(StringUtils.contains(response,"errcode")){ //如果响应中存在错误码则返回null 60 return null; 61 } 62 WeixinUserInfo userInfo = null; 63 try { 64 userInfo = objectMapper.readValue(response,WeixinUserInfo.class); 65 }catch (Exception e){ 66 67 } 68 return userInfo; 69 } 70 }
⒋编写微信access_token类
1 package cn.coreqi.social.weixin.connect; 2 3 import org.springframework.social.oauth2.AccessGrant; 4 5 /** 6 * 对微信access_token信息的封装 7 * 与标准的OAuth2协议不同,微信在获取access_token时会同时返回openId,并没有单独的通过accessToke换取openId的服务 8 * 在此处继承标准AccessGrant(Spring提供的令牌封装类),添加openId字段 9 */ 10 public class WeixinAccessGrant extends AccessGrant { 11 12 private String openId; 13 14 public WeixinAccessGrant() { 15 super(""); 16 } 17 public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) { 18 super(accessToken, scope, refreshToken, expiresIn); 19 } 20 21 public String getOpenId() { 22 return openId; 23 } 24 25 public void setOpenId(String openId) { 26 this.openId = openId; 27 } 28 }
⒌编写微信OAuth2认证流程模板类。
1 package cn.coreqi.social.weixin.connect; 2 3 import java.nio.charset.Charset; 4 import java.util.Map; 5 import org.apache.commons.collections4.MapUtils; 6 import org.apache.commons.lang.StringUtils; 7 import org.slf4j.Logger; 8 import org.slf4j.LoggerFactory; 9 import org.springframework.http.converter.StringHttpMessageConverter; 10 import org.springframework.social.oauth2.AccessGrant; 11 import org.springframework.social.oauth2.OAuth2Parameters; 12 import org.springframework.social.oauth2.OAuth2Template; 13 import org.springframework.util.MultiValueMap; 14 import org.springframework.web.client.RestTemplate; 15 import com.fasterxml.jackson.databind.ObjectMapper; 16 17 /** 18 * 完成微信的OAuth2认证流程的模板类。 19 * 国内厂商实现的OAuth2方式不同, Spring默认提供的OAuth2Template无法完整适配,只能针对每个厂商调整。 20 */ 21 public class WeixinOAuth2Template extends OAuth2Template { 22 23 private Logger logger = LoggerFactory.getLogger(getClass()); 24 25 private String clientId; 26 27 private String clientSecret; 28 29 private String accessTokenUrl; 30 31 private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token"; 32 33 public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) { 34 super(clientId, clientSecret, authorizeUrl, accessTokenUrl); 35 setUseParametersForClientAuthentication(true); //请求中添加client_id和client_secret参数 36 this.clientId = clientId; 37 this.clientSecret = clientSecret; 38 this.accessTokenUrl = accessTokenUrl; 39 } 40 41 /** 42 * 微信变更了OAuth请求参数的名称,我们覆写相应的方法按照微信的文档改为微信请求参数的名字。 43 * @param authorizationCode 44 * @param redirectUri 45 * @param parameters 46 * @return 47 */ 48 @Override 49 public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, 50 MultiValueMap<String, String> parameters) { 51 52 StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl); 53 54 accessTokenRequestUrl.append("?appid="+clientId); 55 accessTokenRequestUrl.append("&secret="+clientSecret); 56 accessTokenRequestUrl.append("&code="+authorizationCode); 57 accessTokenRequestUrl.append("&grant_type=authorization_code"); 58 accessTokenRequestUrl.append("&redirect_uri="+redirectUri); 59 60 return getAccessToken(accessTokenRequestUrl); 61 } 62 63 /** 64 * 微信变更了OAuth请求参数的名称,我们覆写相应的方法按照微信的文档改为微信请求参数的名字。 65 * @param refreshToken 66 * @param additionalParameters 67 * @return 68 */ 69 public AccessGrant refreshAccess(String refreshToken, MultiValueMap<String, String> additionalParameters) { 70 71 StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL); 72 73 refreshTokenUrl.append("?appid="+clientId); 74 refreshTokenUrl.append("&grant_type=refresh_token"); 75 refreshTokenUrl.append("&refresh_token="+refreshToken); 76 77 return getAccessToken(refreshTokenUrl); 78 } 79 80 /** 81 * 获取微信access_token信息 82 * @param accessTokenRequestUrl 83 * @return 84 */ 85 @SuppressWarnings("unchecked") 86 private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) { 87 88 logger.info("获取access_token, 请求URL: "+accessTokenRequestUrl.toString()); 89 90 String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class); 91 92 logger.info("获取access_token, 响应内容: "+response); 93 94 Map<String, Object> result = null; 95 try { 96 result = new ObjectMapper().readValue(response, Map.class); 97 } catch (Exception e) { 98 e.printStackTrace(); 99 } 100 101 //返回错误码时直接返回空 102 if(StringUtils.isNotBlank(MapUtils.getString(result, "errcode"))){ 103 String errcode = MapUtils.getString(result, "errcode"); 104 String errmsg = MapUtils.getString(result, "errmsg"); 105 throw new RuntimeException("获取access token失败, errcode:"+errcode+", errmsg:"+errmsg); 106 } 107 108 WeixinAccessGrant accessToken = new WeixinAccessGrant( 109 MapUtils.getString(result, "access_token"), 110 MapUtils.getString(result, "scope"), 111 MapUtils.getString(result, "refresh_token"), 112 MapUtils.getLong(result, "expires_in")); 113 114 accessToken.setOpenId(MapUtils.getString(result, "openid")); 115 116 return accessToken; 117 } 118 119 /** 120 * 构建获取授权码的请求。也就是引导用户跳转到微信的地址。 121 */ 122 public String buildAuthenticateUrl(OAuth2Parameters parameters) { 123 String url = super.buildAuthenticateUrl(parameters); 124 url = url + "&appid="+clientId+"&scope=snsapi_login"; 125 return url; 126 } 127 128 public String buildAuthorizeUrl(OAuth2Parameters parameters) { 129 return buildAuthenticateUrl(parameters); 130 } 131 132 /** 133 * 微信返回的contentType是html/text,添加相应的HttpMessageConverter来处理。 134 */ 135 protected RestTemplate createRestTemplate() { 136 RestTemplate restTemplate = super.createRestTemplate(); 137 restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); 138 return restTemplate; 139 } 140 141 }
⒍编写微信API适配器,将从微信API拿到的用户数据模型转换为Spring Social的标准用户数据模型。
1 package cn.coreqi.social.weixin.connect; 2 3 import cn.coreqi.social.weixin.api.Weixin; 4 import cn.coreqi.social.weixin.entities.WeixinUserInfo; 5 import org.springframework.social.connect.ApiAdapter; 6 import org.springframework.social.connect.ConnectionValues; 7 import org.springframework.social.connect.UserProfile; 8 9 /** 10 * 微信API适配器,将从微信API拿到的用户数据模型转换为Spring Social的标准用户数据模型。 11 * @author fanqi 12 */ 13 public class WeixinAdapter implements ApiAdapter<Weixin> { 14 15 private String openId; 16 17 public WeixinAdapter() {} 18 19 public WeixinAdapter(String openId){ 20 this.openId = openId; 21 } 22 23 /** 24 * 用来测试当前的API是否可用 25 * @param api 26 * @return 27 */ 28 @Override 29 public boolean test(Weixin api) { 30 return true; 31 } 32 33 /** 34 * 将微信的用户信息映射到ConnectionValues标准的数据化结构上 35 * @param api 36 * @param values 37 */ 38 @Override 39 public void setConnectionValues(Weixin api, ConnectionValues values) { 40 WeixinUserInfo profile = api.getUserInfo(openId); 41 values.setProviderUserId(profile.getOpenid()); 42 values.setDisplayName(profile.getNickname()); 43 values.setImageUrl(profile.getHeadimgurl()); 44 } 45 46 /** 47 * 48 * @param api 49 * @return 50 */ 51 @Override 52 public UserProfile fetchUserProfile(Weixin api) { 53 return null; 54 } 55 56 /** 57 * 58 * @param api 59 * @param message 60 */ 61 @Override 62 public void updateStatus(Weixin api, String message) { 63 //do nothing 64 } 65 66 }
⒎编写微信的OAuth2流程处理器的提供器
1 package cn.coreqi.social.weixin.connect; 2 3 import cn.coreqi.social.weixin.api.Weixin; 4 import cn.coreqi.social.weixin.api.impl.WeixinImpl; 5 import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider; 6 7 /** 8 * 微信的OAuth2流程处理器的提供器,供spring social的connect体系调用 9 */ 10 public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> { 11 12 /** 13 * 微信获取授权码的url 14 */ 15 private static final String URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect"; 16 /** 17 * 微信获取accessToken的url 18 */ 19 private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token"; 20 21 /** 22 * 23 * @param appId 24 * @param appSecret 25 */ 26 public WeixinServiceProvider(String appId, String appSecret) { 27 super(new WeixinOAuth2Template(appId, appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN)); 28 } 29 30 /** 31 * 32 * @param accessToken 33 * @return 34 */ 35 @Override 36 public Weixin getApi(String accessToken) { 37 return new WeixinImpl(accessToken); 38 } 39 40 }
⒏创建微信连接工厂
1 package cn.coreqi.social.weixin.connect; 2 3 import cn.coreqi.social.weixin.api.Weixin; 4 import org.springframework.social.connect.ApiAdapter; 5 import org.springframework.social.connect.Connection; 6 import org.springframework.social.connect.ConnectionData; 7 import org.springframework.social.connect.support.OAuth2Connection; 8 import org.springframework.social.connect.support.OAuth2ConnectionFactory; 9 import org.springframework.social.oauth2.AccessGrant; 10 import org.springframework.social.oauth2.OAuth2ServiceProvider; 11 12 /** 13 * 微信连接工厂 14 */ 15 public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> { 16 17 /** 18 * 19 * @param providerId 20 * @param appId 21 * @param appSecret 22 */ 23 public WeixinConnectionFactory(String providerId, String appId, String appSecret) { 24 super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter()); 25 } 26 27 /** 28 * 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取 29 * @param accessGrant 30 * @return 31 */ 32 @Override 33 protected String extractProviderUserId(AccessGrant accessGrant) { 34 if(accessGrant instanceof WeixinAccessGrant) { 35 return ((WeixinAccessGrant)accessGrant).getOpenId(); 36 } 37 return null; 38 } 39 40 /** 41 * 42 * @param accessGrant 43 * @return 44 */ 45 public Connection<Weixin> createConnection(AccessGrant accessGrant) { 46 return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(), 47 accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant))); 48 } 49 50 /** 51 * 52 * @param data 53 * @return 54 */ 55 public Connection<Weixin> createConnection(ConnectionData data) { 56 return new OAuth2Connection<Weixin>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId())); 57 } 58 59 /** 60 * 61 * @param providerUserId 62 * @return 63 */ 64 private ApiAdapter<Weixin> getApiAdapter(String providerUserId) { 65 return new WeixinAdapter(providerUserId); 66 } 67 68 /** 69 * 70 * @return 71 */ 72 private OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() { 73 return (OAuth2ServiceProvider<Weixin>) getServiceProvider(); 74 } 75 76 77 }
⒐创建微信登陆配置类
1 package cn.coreqi.social.weixin.config; 2 3 import cn.coreqi.social.weixin.connect.WeixinConnectionFactory; 4 import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter; 5 import org.springframework.context.annotation.Configuration; 6 import org.springframework.social.connect.ConnectionFactory; 7 8 /** 9 * 微信登录配置 10 */ 11 @Configuration 12 public class WeixinAutoConfiguration extends SocialAutoConfigurerAdapter { 13 14 @Override 15 protected ConnectionFactory<?> createConnectionFactory() { 16 String providerId = "weixin"; //第三方id,用来决定发起第三方登录的url,默认是weixin 17 String appId = ""; 18 String appSecret = ""; 19 return new WeixinConnectionFactory(providerId, appId, 20 appSecret); 21 } 22 23 }