微信授权登录是一个非常常见的场景,利用微信授权登录,我们可以很容易获取用户的一些信息,通过用户对公众号的唯一openid从而建立数据库绑定用户身份;
一、开发前准备
1、需要申请一个公众号(https://mp.weixin.qq.com/),拿到AppID和AppSecret;
2、进入公众号开发者中心页配置授权回调域名。具体位置:接口权限-网页服务-网页账号-网页授权获取用户基本信息-修改。注意,这里仅需填写全域名(例:www.qq.com),勿加 http:// 等协议头及具体的地址;
3、常用微信开发者工具,开发人员,可申请公众平台测试账号,直接体验和测试公众平台所有高级接口;可使用官方提供的web开发者工具直接在浏览器中进行调试;
二、微信公众平台(测试号)配置接口信息,接入微信公众号
开发者工具->公众平台测试账号->接口配置信息
具体实现,新增Controller类
@Controller
@RequestMapping("/wx")
@Api(value = "MainController", description = "微收发主模块")
public class MainController extends BaseController {
private String TOKEN = "wsf";
/**
* 接收并校验四个请求参数
*/
@RequestMapping(value = "/chk", method = RequestMethod.GET)
public void checkName(@RequestParam(name = "signature") String signature,
@RequestParam(name = "timestamp") String timestamp,
@RequestParam(name = "nonce") String nonce,
@RequestParam(name = "echostr") String echostr, HttpServletResponse response) throws IOException {
System.out.println("-----------------------开始校验------------------------");
//排序
String sortString = sort(TOKEN, timestamp, nonce);
//加密
String myString = sha1(sortString);
//校验
if (myString != null && myString != "" && myString.equals(signature)) {
System.out.println("签名校验通过");
//如果检验成功原样返回echostr,微信服务器接收到此输出,才会确认检验完成。
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(echostr.getBytes());
out.flush();
out.close();
} else {
System.out.println("签名校验失败");
}
}
/**
* 排序方法
*/
public String sort(String token, String timestamp, String nonce) {
String[] strArray = {token, timestamp, nonce};
Arrays.sort(strArray);
StringBuilder sb = new StringBuilder();
for (String str : strArray) {
sb.append(str);
}
return sb.toString();
}
/**
* 将字符串进行sha1加密
*
* @param str 需要加密的字符串
* @return 加密后的内容
*/
public String sha1(String str) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(str.getBytes());
byte messageDigest[] = digest.digest();
// Create Hex String
StringBuffer hexString = new StringBuffer();
// 字节数组转换为 十六进制 数
for (int i = 0; i < messageDigest.length; i++) {
String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
}
注意:SpringBoot项目中,一般会另外配置json解析器,使用@responsebody注解并且返回值类型为String时,返回的string字符串会带有双引号,导致微信接口验证不通过;可以通过返回HttpServletResponse 对象来解决;
三、通过网页授权机制获取用户信息
主要有3步:
1)引导用户进入授权页面同意授权,获取code;
2)通过code换取网页授权access_token;
3)拉取用户信息(需scope为 snsapi_userinfo);
- snsapi_base:只能获取到用户openid。好处是静默认证,无需用户手动点击认证按钮,感觉上像是直接进入网站一样。
- snsapi_userinfo:可以获取到openid、昵称、头像、所在地等信息。需要用户手动点击认证按钮。
1、Vue微信授权插件
wechatAuth.js
class wechatAuth {
constructor(config) {
let defaultConfig = {
appid: '',
responseType: 'code',
redirect_uri: '',
error_uri: '',
scope: 'snsapi_base ',
getCodeCallback: () => { },
}
this.config = Object.assign(defaultConfig, config)
}
//调取微信获取code接口
getCode() {
let authPageBaseUri = 'https://open.weixin.qq.com/connect/oauth2/authorize';
let authParams = `?appid=${this.config.appid}&redirect_uri=${this.config.redirectUri}&response_type=${this.config.responseType}&scope=${this.config.scope}#wechat_redirect`;
window.location.href = authPageBaseUri + authParams;
}
next(next) {
return (to, code) => {
if (code) {
window.sessionStorage.setItem('wxcode', code);
to ? next(to) : next();
} else {
to && next(to);
}
}
}
getCodeCallback(next, code) {
return this.config.getCodeCallback(this.next(next), code);
}
}
export default wechatAuth;
index.js
import wechatAuth from './wechatAuth';
import url from 'url'
import querystring from 'querystring';
export default {
install(Vue, options) {
let router = options.router;
let wechatPlugin = new wechatAuth(options)
if (!router) return false;
//绑定到路由上
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.wechatAuth)) {// 判断是否需要授权
let query = querystring.parse(url.parse(window.location.href).query);
let code = query.code;
if (window.sessionStorage.getItem('wxcode')) {// 判断是否已经有授权
next();
} else if (code) {
wechatPlugin.getCodeCallback(next, code);
} else { //去获取code
wechatPlugin.getCode();
}
} else {
next();
}
});
}
}
vueWechatAuth.js
//入口文件
let vueWechatAuth = require('./component/index')
module.exports = vueWechatAuth
2、Vue微信授权插件初始化
在router.js文件,进行相关操作,并将code传递给后端;
// 微信授权插件初始化
Vue.use(wechatPlugin, {
router, // 路由实例对象
appid: 'wxad2cb5ca6f48a285', // 您的微信appid
responseType: 'code', // 返回类型,请填写code
scope: 'snsapi_userinfo', // 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息)
redirectUri: 'http://pcd.ngrok.i668.top', //微信回调地址
getCodeCallback(next, code) {
// 用户同意授权后回调方法
// code:用户同意授权后,获得code值
// code说明: code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
// next: 无论access_token是否获取成功,一定要调用该方法
// next说明:next方法接收两个参数
// 参数1(必填,切换到的路由地址,空字符串为当前路由,指定切换对象 next('/') 或者 next({ path: '/' })
// 参数2为通过code值请求后端获取access_token的结果,true或者false,默认为false
Api.post('/mobile/wx/auth', {
code1: code,
state1: ''
}, response => {
let data = response.data;
if (data.isSucc) {
if (data.obj != null){
localStorage.setItem(
"userInfoData",
JSON.stringify(data.obj)
);
localStorage.setItem("accessToken", data.obj.ticket);
next('/main', code); // 获取access_toeken成功,转至主页
}else{
next('', code); // 获取access_toeken成功
}
} else {
next('/login'); // 获取access_token失败
}
});
}
})
3、Java后端,通过code换取网页授权access_token;
@RequestMapping(value = "/auth", method = RequestMethod.POST)
@ResponseBody
public ResponseResult wechatLogin(@RequestBody PostRequest request) throws Exception {
ResponseResult result = new ResponseResult();
if (request == null || request.body == null)
return result;
String code1 = request.body.getString("code1");
String state1 = request.body.getString("state1");
// 1. 用户同意授权,获取code
logger.info("收到微信重定向跳转.");
logger.info("用户同意授权,获取code1:{} , state1:{}", code1, state1);
// 2. 通过code换取网页授权access_token
if (code1 != null || !(code1.equals(""))) {
String APPID = WX_APPID;
String SECRET = WX_APPSECRET;
String CODE = code1;
String WebAccessToken = "";
String openId = "";
String nickName, sex, openid = "";
String REDIRECT_URI = "http://pcd.ngrok.i668.top";
String SCOPE = "snsapi_userinfo";
/* String getCodeUrl = UserInfoUtil.getCode(APPID, REDIRECT_URI, SCOPE);
logger.info("第一步:用户授权, get Code URL:{}", getCodeUrl);*/
// 替换字符串,获得请求access token URL
String tokenUrl = UserInfoUtil.getWebAccess(APPID, SECRET, CODE);
logger.info("第二步:get Access Token URL:{}", tokenUrl);
// 通过https方式请求获得web_access_token
String response = HttpsUtil.httpsRequestToString(tokenUrl, "GET", null);
JSONObject jsonObject = JSON.parseObject(response);
logger.info("请求到的Access Token:{}", jsonObject.toJSONString());
if (null != jsonObject) {
try {
WebAccessToken = jsonObject.getString("access_token");
openId = jsonObject.getString("openid");
logger.info("获取access_token成功!");
logger.info("WebAccessToken:{} , openId:{}", WebAccessToken, openId);
// 3. 使用获取到的 Access_token 和 openid 拉取用户信息
String userMessageUrl = UserInfoUtil.getUserMessage(WebAccessToken, openId);
logger.info("第三步:获取用户信息的URL:{}", userMessageUrl);
// 通过https方式请求获得用户信息响应
String userMessageResponse = HttpsUtil.httpsRequestToString(userMessageUrl, "GET", null);
JSONObject userMessageJsonObject = JSON.parseObject(userMessageResponse);
logger.info("用户信息:{}", userMessageJsonObject.toJSONString());
if (userMessageJsonObject != null) {
if ("40001".equals(userMessageJsonObject.getString("errcode"))){
result.setIsSucc(Boolean.FALSE);
result.setCode(ResultCode.ACCESS_TOKEN__INVALID_CODE);
result.setMsg(ResultCode.ACCESS_TOKEN__INVALID_MSG);
return result;
}
//用户昵称
nickName = userMessageJsonObject.getString("nickname");
//用户性别
sex = userMessageJsonObject.getString("sex");
sex = (sex.equals("1")) ? "男" : "女";
//用户唯一标识
openid = userMessageJsonObject.getString("openid");
logger.info("用户昵称:{}", nickName);
logger.info("用户性别:{}", sex);
logger.info("OpenId:{}", openid);
//根据OpenId获取用户信息
UserVO userVO = wxUserService.findUserByOpenID(openid);
if (userVO != null){
UserInfo userInfo = createTokenForUserInfo(userVO.getName(),userVO.getPassword(),false);
result.setObj(userInfo);
}
}
} catch (JSONException e) {
logger.error("获取Web Access Token失败");
result.setIsSucc(Boolean.FALSE);
result.setCode(ResultCode.JSSON_TRANSFER_ERROR_CODE);
result.setMsg(ResultCode.JSSON_TRANSFER_ERROR_MSG);
}
}
}
result.setIsSucc(Boolean.TRUE);
result.setCode(ResultCode.SUCCESS_CODE);
result.setMsg(ResultCode.SUCCESS_MSG);
return result;
}
HttpsUtil.java
package top.i668.mobile.util;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.net.URL;
public class HttpsUtil {
/**
* 以https方式发送请求并将请求响应内容以String方式返回
*
* @param path 请求路径
* @param method 请求方法
* @param body 请求数据体
* @return 请求响应内容转换成字符串信息
*/
public static String httpsRequestToString(String path, String method, String body) {
if (path == null || method == null) {
return null;
}
String response = null;
InputStream inputStream = null;
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
HttpsURLConnection conn = null;
try {
// 创建SSLConrext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = {new JEEWeiXinX509TrustManager()};
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述对象中的到SSLSocketFactory
SSLSocketFactory ssf = sslContext.getSocketFactory();
System.out.println(path);
URL url = new URL(path);
conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
//设置请求方式(git|post)
conn.setRequestMethod(method);
//有数据提交时
if (null != body) {
OutputStream outputStream = conn.getOutputStream();
outputStream.write(body.getBytes("UTF-8"));
outputStream.close();
}
// 将返回的输入流转换成字符串
inputStream = conn.getInputStream();
inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
response = buffer.toString();
} catch (Exception e) {
} finally {
if (conn != null) {
conn.disconnect();
}
try {
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
} catch (IOException execption) {
}
}
return response;
}
}
UserInfoUtil.java
// 2.获取Web_access_tokenhttps的请求地址
public static String Web_access_tokenhttps = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
// 替换字符串
public static String getWebAccess(String APPID, String SECRET, String CODE) {
return String.format(Web_access_tokenhttps, APPID, SECRET, CODE);
}
// 3.拉取用户信息的请求地址
public static String User_Message = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN";
// 替换字符串
public static String getUserMessage(String access_token, String openid) {
return String.format(User_Message, access_token, openid);
}