根据需求,需要拥有第三方微信登录功能,并获取到用户信息。
网站应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统。
1、注册邮箱账号。
2、根据邮箱账号注册微信开放平台账号,完善开发者资料。
3、申请开发者资质认证、填写相关资料、填写发票、支付认证金额。提交并等待认证结果
1)申请开发者资质认证
2)选定类型
3)填写“认证资料”
4)填写“管理员信息”
5)上传“企业基本信息”材料:
6)进入填写发票及支付费用
4、认证成功后,创建网站应用,填写基本信息、下载网站信息登记表填写并上传扫描件、填写授权回调域等。提交审核等待结果。
1)创建网站应用
2)创建移动应用
5、认证成功后,创建移动应用,至少选择安卓、IOS、WP8其中一种平台
6、创建应用成功后,申请微信登陆,等待审核结果,待审核通过后,可进行微信登陆的开发。
1)第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
2)通过code参数加上AppID和AppSecret等,通过API换取access_token;
3)通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
1)添加依赖
<!-- 添加httpclient支持 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
2)配置文件
## 微信开放平台
wechat:
open:
# APPID
appid: ******
# APPSECRET
appsecret: ******
# 回调地址
redirect_uri: http://de43f7.39nat.com
3)实体类
/**
*
* @author: xxm
* 功能描述: access_token封装基础类
* @date: 2021/2/2 10:41
*/
@Data
public class Token {
private String openid; //授权用户唯一标识
private String accessToken; //接口调用凭证
private Integer ExpiresIn; //access_token接口调用凭证超时时间,单位(秒)
}
/**
*
* @author: xxm
* 功能描述: 微信与网站绑定关系表
* @date: 2021/2/1 17:08
*/
@Data
@Table(name = "UserWeChat")
@NameStyle(Style.normal)
public class UserWeChat implements Serializable {
private static final long serialVersionUID = 8997358443007506192L;
@Id
@GeneratedValue(generator = "JDBC")
private Integer id;
//用户id
private Integer userId;
//微信OpenId
private String openId;
//昵称
private String nickName;
}
/**
*
* @author: xxm
* 功能描述: access_token封装基础类
* @date: 2021/2/2 10:41
*/
@Data
public class Token {
private String openid; //授权用户唯一标识
private String accessToken; //接口调用凭证
private Integer ExpiresIn; //access_token接口调用凭证超时时间,单位(秒)
}
4)微信工具类
/**
*
* @author: xxm
* 功能描述: 微信登录相关接口工具类
* @date: 2021/2/23 16:14
* @param:
* @return:
*/
public class WeChatCommonUtil {
private static Logger log = LoggerFactory.getLogger(WeChatCommonUtil.class);
/**
* @author: xxm
* 功能描述: urlEncodeUTF8工具类
* (用于将扫描二维码后重定向的资源url进行编码)
* @date: 2021/2/22 15:55
* @param:
* @return:
*/
public static String urlEncodeUTF8(String source) {
String result = source;
try {
result = java.net.URLEncoder.encode(source, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
/**
*
* @author: xxm
* 功能描述: 获取openid等信息的方法
* @date: 2021/2/22 17:52
* @param:
* @return:
*/
public static Token getTokenWithOpenid(String appid, String appsecret, String code) {
String findAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
Token token = null;
// 发起GET请求获取凭证
String requestUrl = findAccessTokenUrl.replace("APPID", appid).replace("SECRET", appsecret).replace("CODE", code);
JSONObject jsonObject = JSONObject.fromObject(httpsRequest(requestUrl, "GET", null));
if (null != jsonObject) {
try {
token = new Token();
token.setOpenid(jsonObject.getString("openid"));
token.setAccessToken(jsonObject.getString("access_token"));
token.setExpiresIn(jsonObject.getInt("expires_in"));
} catch (JSONException e) {
token = null;
// 获取token失败
log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
}
}
return token;
}
/**
*
* @author: xxm
* 功能描述: 根据openid获取用户信息的方法
* @date: 2021/2/22 17:52
* @param:
* @return:
*/
public static WechatUserInfo getUserinfo(String access_token, String openid) {
WechatUserInfo wxuse = new WechatUserInfo();
String findUseinfo = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID";
String requestUrl = findUseinfo.replace("ACCESS_TOKEN", access_token).replace("OPENID", openid);
JSONObject jsonObject = JSONObject.fromObject(httpsRequest(requestUrl, "GET", null));
if (null != jsonObject) {
try {
wxuse.setNickname(jsonObject.getString("nickname"));
wxuse.setHeadimgurl(jsonObject.getString("headimgurl"));
wxuse.setUnionid(jsonObject.getString("unionid"));
wxuse.setOpenid(jsonObject.getString("openid"));
} catch (JSONException e) {
e.printStackTrace();
}
}
return wxuse;
}
/**
* 发送https请求
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
* 返回微信服务器响应的信息
*/
public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
conn.disconnect();
return buffer.toString();
} catch (ConnectException ce) {
log.error("连接超时:{}", ce);
} catch (Exception e) {
log.error("https请求异常:{}", e);
}
return null;
}
}
/**
* @author: xxm
* @description:信任管理器
* @date: 2021/2/22 17:41
*/
public class MyX509TrustManager implements X509TrustManager {
// 检查客户端证书
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
// 检查服务器端证书
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
// 返回受信任的X509证书数组
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
前提:应用已经获取相应的网页授权作用域(scope=snsapi_login)
开发:第三方网站引导用户打开链接
https://open.weixin.qq.com/connect/qrconnect?
appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
注意:若提示“该链接无法访问”,请检查参数是否填写错误,如redirect_uri的域名与审核时填写的授权域名不一致或scope不为snsapi_login。
/**
*
* @author: xxm
* 功能描述: 跳转至登录授权页面(页面出现二维码)
* @date: 2021/2/22 17:50
* @param:
* @return:
*/
@RequestMapping("/login")
public String openWeChatLogin() {
// 防止csrf攻击(跨站请求伪造攻击)
String state = UUID.randomUUID().toString().replaceAll("-", "");
String url = "https://open.weixin.qq.com/connect/qrconnect?" +
"appid=" +
env.getProperty("wechat.open.appid").trim() +
"&redirect_uri=" +
WeChatCommonUtil.urlEncodeUTF8(env.getProperty("wechat.open.redirect_uri").trim()+"/wechat/weChatLogin_epf") +
"&response_type=code" +
"&scope=snsapi_login" +
// 由后台自动生成
"&state=" + state +
"#wechat_redirect";
return "redirect:" + url;
}
用户允许授权后,将会重定向到redirect_uri的网址上,并且带上code和state参数
redirect_uri?code=CODE&state=STATE
若用户禁止授权,则不会重定向到我们提供的回调地址中
成功授权后,将获得Code,通过Code可以获取access_token
通过上述方法获取的code获取access_token.
Http Get请求
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
请求后,
返回成功的json串为(样例):
{
“access_token”:“ACCESS_TOKEN”, // 接口调用凭证
“expires_in”:7200, // access_token接口调用凭证超时时间,单位(秒)
“refresh_token”:“REFRESH_TOKEN”, //用户刷新access_token
“openid”:“OPENID”, //授权用户唯一标识
“scope”:“SCOPE”, //用户授权的作用域,使用逗号(,)分隔
“unionid”: “o6_bmasdasdsad6_2sgVt7hMZOPfL” //当且仅当该网站应用已获得该用户的userinfo授权时,才会出现该字段
}
失败返回的样例:
{“errcode”:40029,“errmsg”:“invalid code”}
失败可能原因: 暂时不明
Token tokenWithOpenid = WeChatCommonUtil.getTokenWithOpenid(loginAppid, loginSecrect, code);
前提:
access_token有效且未超时;
微信用户已授权给第三方应用帐号相应接口作用域(scope)。
Http Get请求:
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
返回成功的json结果(样例):
{
“openid”:“OPENID”, //普通用户的标识,对当前开发者帐号唯一
“nickname”:“NICKNAME”, //普通用户昵称
“sex”:1, //普通用户性别,1为男性,2为女性
“province”:“PROVINCE”, //普通用户个人资料填写的省份
“city”:“CITY”, //普通用户个人资料填写的城市
“country”:“COUNTRY”, //国家,如中国为CN
“headimgurl”: “http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0”, //用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
“privilege”:[ //用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
“PRIVILEGE1”,
“PRIVILEGE2”
],
“unionid”: " o6_bmasdasdsad6_2sgVt7hMZOPfL" //用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的
}
失败JSON样例:
{“errcode”:40003,“errmsg”:“invalid openid”}
注意:在用户修改微信头像后,旧的微信头像URL将会失效,因此开发者应该自己在获取用户信息后,将头像图片保存下来,避免微信头像URL失效后的异常情况
最好保存用户unionID信息,以便以后在不同应用中进行用户信息互通。
//通过access_token调用接口
WechatUserInfo wxuse = WeChatCommonUtil.getUserinfo(access_token, openid);
获取到access_token,在通过access_token获取到微信用户信息WechatUserInfo,就可以判断微信账号是否已经与网站账号关联 ,如果关联了,则此处可以直接登录跳转到主页,但在此处,我还是用了Spring Security安全框架,所有需要微信登录需要经过Spring Security的url请求进行验证,才可以登录。
如果微信账号没有关联网站账号,则可以跳转到微信绑定页面,完成微信绑定。
/**
*
* @author: xxm
* 功能描述: 授权成功后
* @date: 2021/2/22 17:50
* @param:
* @return:
*/
@ResponseBody
@RequestMapping("/weChatLogin_epf")
public ModelAndView weChatLogin_epf(HttpServletRequest request, HttpSession session, RedirectAttributes attribute){
ModelAndView model = new ModelAndView();
//获取到code,这个code应该是微信那边定义的
String code = request.getParameter("code");
//第三方网站(即我们自己)自定义的参数,可以存储一些重要信息和防伪信息,因为这个接口是完全暴露的,所以必须要有防伪措施,防止恶意请求来搞事
String state=request.getParameter("state");
//如果这两个字段都为空值,就可以判定为而已请求
if(StringUtils.isBlank(code)||StringUtils.isBlank(state)){
logger.info("非法请求,缺少必要的参数");
model.setViewName("redirect:/login");
attribute.addFlashAttribute("errorInfo", "非法请求,缺少必要的参数!");
attribute.addFlashAttribute("success", false);
return model;
}
logger.info("获取到的code是 :" + code+",state="+state);
//通过code获取access_token
String loginAppid = env.getProperty("wechat.open.appid").trim();
String loginSecrect = env.getProperty("wechat.open.appsecret").trim();
try {
Token tokenWithOpenid = WeChatCommonUtil.getTokenWithOpenid(loginAppid, loginSecrect, code);
if (null != tokenWithOpenid) {
String openid = tokenWithOpenid.getOpenid();
String access_token = tokenWithOpenid.getAccessToken();
//通过access_token调用接口
WechatUserInfo wxuse = WeChatCommonUtil.getUserinfo(access_token, openid);
logger.info("微信用户信息:" + wxuse);
UserWeChatDto uwcDto = new UserWeChatDto();
uwcDto.setOpenId(openid);
List<UserWeChatDto> uwcList = userWeChatClient.getListByParam(uwcDto);
if (null != uwcList && uwcList.size()==1) {
UserWeChatDto userWeChatDto = uwcList.get(0);
//微信账号已经与网站账号关联
//根据用户id查询用户
SysUser currentUser = userControllerClient.getUserById(userWeChatDto.getUserId());
session.setAttribute("username", currentUser.getUsername());
// 微信登录需要经过的url请求
model.setViewName("redirect:/wechat/weChatLogin");
model.addObject("openid",openid);
attribute.addFlashAttribute("success", true);
} else {
//微信账号没有关联网站账号
String url = "redirect:/weChatBind";
model.setViewName(url);
attribute.addFlashAttribute("wechatUserInfo", wxuse);
attribute.addFlashAttribute("nickname", wxuse.getNickname());
attribute.addFlashAttribute("openid", wxuse.getOpenid());
}
} else {
logger.error("获取token失败");
model.setViewName("redirect:/login");
attribute.addFlashAttribute("errorInfo", "获取token失败!");
attribute.addFlashAttribute("success", false);
}
}catch (Exception e) {
e.printStackTrace();
model.setViewName("redirect:/login");
attribute.addFlashAttribute("errorInfo", "微信授权登录失败!");
attribute.addFlashAttribute("success", false);
}finally {
return model;
}
}
由于access_token有效期(目前为2个小时)较短,当access_token超时后,可以使用refresh_token进行刷新,access_token刷新结果有两种:
refresh_token拥有较长的有效期(30天),当refresh_token失效的后,需要用户重新授权。
请求方法:
获取第一步的code后,请求以下链接进行refresh_token:
https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN
参数说明:
成功返回的结果:
{
“access_token”:“ACCESS_TOKEN”, //接口调用凭证
“expires_in”:7200, // access_token接口调用凭证超时时间,单位(秒)
“refresh_token”:“REFRESH_TOKEN”, //用户刷新access_token
“openid”:“OPENID”, //授权用户唯一标识
“scope”:“SCOPE” //用户授权的作用域,使用逗号(,)分隔
}
失败样例:
{“errcode”:40030,“errmsg”:“invalid refresh_token”}
注意:
1、Appsecret 是应用接口使用密钥,泄漏后将可能导致应用数据泄漏、应用的用户数据泄漏等高风险后果;存储在客户端,极有可能被恶意窃取(如反编译获取Appsecret);
2、access_token 为用户授权第三方应用发起接口调用的凭证(相当于用户登录态),存储在客户端,可能出现恶意获取access_token 后导致的用户数据泄漏、用户微信相关接口功能被恶意发起等行为;
3、refresh_token 为用户授权第三方应用的长效凭证,仅用于刷新access_token,但泄漏后相当于access_token 泄漏,风险同上。
建议将secret、用户数据(如access_token)放在App云端服务器,由云端中转接口调用请求。
请自己测试