1、问题描述:
大家在做微信扫码登录时,可能会遇到40163的错误,具体报错如下:
{“errcode”:40163,“errmsg”:"code been used}。网上对于这个问题的说法千差万别,但都没有一个好的解决方法。经过仔细分析,最终找到了问题所在(如果急于排错,可直接跳转到下面第6步)
2、开发准备:
要完成微信扫码登录功能,要先注册相关账号,拿到应用的appId和secret,配置好外网可以访问的网址(授权回调地址)。注意授权回调地址,不能简单使用用回调方法(例如: /api-uaa/oauth/wechat/callback),而是需要带上http或者https协议,完整回调地址应该类似于:
http://www.super.com/api-uaa/oauth/weChat/callback(注意:授权回调域非回调地址,授权回调域:类似:www.super.com,回调地址:http://www.super.com/api-uaa/oauth/weChat/callback)。所有都配置好了就需要编写相关方法了。
下面附上基本的代码,有需要者只需要根据自己项目需要修改appId和secret以及回调地址等即可:
(1)配置appId、secret等参数:
wx:
appId: wxfb72c85ee5329311
secret: e6eba215f6df135d023e42d69b17f4e0
redirect_uri: /api-uaa/oauth/wechat/callback
openVisitUrl: http://www.super.com
qrCode: https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE&connect_redirect=1#wechat_redirect
webAccessTokenHttpsOAuth: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
(2)编写相关工具类:
1)AES加密解密
/**
* @Description: AES加密解密
* @Date: 2021-03-10
* @Author: Jonathan.WQ
* @Version V1.0
* @Modified By: Jonathan.WQ
*/
public class AesUtil {
private AesUtil() {
}
/**
* 秘钥
*/
public static final String PASSWORD_SECRET_KEY = "EasyRailEveryday";
/**
* 初始向量
*/
public static final String INITIAL_VECTOR = "EasyRailEasyRail";
/**
* 加密
*
* @param content 需要加密的内容
* @param password 加密密码
* @param keySize 密钥长度16,24,32(密码长度为24和32时需要将local_policy.jar/US_export_policy.jar两个jar包放到JRE目录%jre%/lib/security下)
* @return
*/
public static byte[] encrypt(String content, String password, int keySize) {
try {
//密钥长度不够用0补齐。
SecretKeySpec key = new SecretKeySpec(ZeroPadding(password.getBytes(Base64Util.DEFAULT_CHARSET), keySize), "AES");
//定义加密算法AES、算法模式ECB、补码方式PKCS5Padding
//Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
//定义加密算法AES 算法模式CBC、补码方式PKCS5Padding
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
//CBC模式模式下初始向量 不足16位用0补齐
IvParameterSpec iv = new IvParameterSpec(ZeroPadding(INITIAL_VECTOR.getBytes(Base64Util.DEFAULT_CHARSET), 16));
byte[] byteContent = content.getBytes();
//初始化加密
//ECB
//cipher.init(Cipher.ENCRYPT_MODE, key);
//CBC
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] result = cipher.doFinal(byteContent);
return result;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 解密
*
* @param content 待解密内容
* @param password 解密密钥
* @param keySize 密钥长度16,24,32(密码长度为24和32时需要将local_policy.jar/US_export_policy.jar两个jar包放到JRE目录%jre%/lib/security下)
* @return
*/
public static String decrypt(byte[] content, String password, int keySize) {
try {
//密钥长度不够用0补齐。
SecretKeySpec key = new SecretKeySpec(ZeroPadding(password.getBytes(), keySize), "AES");
//定义加密算法AES、算法模式ECB、补码方式PKCS5Padding
//Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
//定义加密算法AES 算法模式CBC、补码方式PKCS5Padding
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
//CBC模式模式下初始向量 不足16位用0补齐
IvParameterSpec iv = new IvParameterSpec(ZeroPadding(INITIAL_VECTOR.getBytes(Base64Util.DEFAULT_CHARSET), 16));
// 初始化解密
//ECB
//cipher.init(Cipher.DECRYPT_MODE, key);
//CBC
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] result = cipher.doFinal(content);
return new String(result, Base64Util.DEFAULT_CHARSET);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将二进制转换成16进制
*
* @param buf
* @return
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* 将16进制转换为二进制
*
* @param hexStr
* @return
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1) {
return null;
}
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
/**
* 字符达不到指定长度补0
*
* @param in 字符数组
* @param blockSize 长度
* @return
*/
public static byte[] ZeroPadding(byte[] in, Integer blockSize) {
Integer copyLen = in.length;
if (copyLen > blockSize) {
copyLen = blockSize;
}
byte[] out = new byte[blockSize];
System.arraycopy(in, 0, out, 0, copyLen);
return out;
}
}
2)Http请求工具类
/**
* @Description: httpClient 工具类</p>
* @Date: 2021-03-10
* @Author: Jonathan.WQ
* @Version V1.0
* @Modified By:
*/
@Slf4j
public class HttpUtils {
private HttpUtils(){}
/**
* 默认参数设置
* setConnectTimeout:设置连接超时时间,单位毫秒。
* setConnectionRequestTimeout:设置从connect Manager获取Connection 超时时间,单位毫秒。
* setSocketTimeout:请求获取数据的超时时间,单位毫秒。访问一个接口,多少时间内无法返回数据,就直接放弃此次调用。 暂时定义15分钟
*/
private static RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(600000).setConnectTimeout(600000).setConnectionRequestTimeout(600000).build();
/**
* 静态内部类---作用:单例产生类的实例
* @author Administrator
*
*/
private static class LazyHolder {
private static final HttpUtils INSTANCE = new HttpUtils();
}
public static HttpUtils getInstance(){
return LazyHolder.INSTANCE;
}
/**
* 发送 post请求
* @param httpUrl 地址
*/
public static String sendHttpPost(String httpUrl) {
HttpPost httpPost = new HttpPost(httpUrl);// 创建httpPost
return sendHttpPost(httpPost);
}
/**
* 发送 post请求
* @param httpUrl 地址
* @param params 参数(格式:key1=value1&key2=value2)
*/
public static String sendHttpPost(String httpUrl, String params) {
HttpPost httpPost = new HttpPost(httpUrl);// 创建httpPost
try {
//设置参数
StringEntity stringEntity = new StringEntity(params, "UTF-8");
stringEntity.setContentType("application/x-www-form-urlencoded");
httpPost.setEntity(stringEntity);
} catch (Exception e) {
e.printStackTrace();
}
return sendHttpPost(httpPost);
}
/**
* 发送 post请求
* @param httpUrl 地址
* @param maps 参数
*/
public static String sendHttpPost(String httpUrl, Map<String, String> maps) {
HttpPost httpPost = new HttpPost(httpUrl);// 创建httpPost
// 创建参数队列
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
for (String key : maps.keySet()) {
nameValuePairs.add(new BasicNameValuePair(key, maps.get(key)));
}
try {
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs, "UTF-8"));
} catch (Exception e) {
e.printStackTrace();
}
return sendHttpPost(httpPost);
}
/**
* 发送Post请求
* @param httpPost
* @return
*/
private static String sendHttpPost(HttpPost httpPost) {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
HttpEntity entity = null;
String responseContent = null;
try {
// 创建默认的httpClient实例
httpClient = HttpClients.createDefault();
httpPost.setConfig(requestConfig);
// 执行请求
long execStart = System.currentTimeMillis();
response = httpClient.execute(httpPost);
long execEnd = System.currentTimeMillis();
System.out.println("=================执行post请求耗时:"+(execEnd-execStart)+"ms");
long getStart = System.currentTimeMillis();
entity = response.getEntity();
responseContent = EntityUtils.toString(entity, "UTF-8");
long getEnd = System.currentTimeMillis();
System.out.println("=================获取响应结果耗时:"+(getEnd-getStart)+"ms");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 关闭连接,释放资源
if (response != null) {
response.close();
}
if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return responseContent;
}
/**
* 发送 get请求
* @param httpUrl
*/
public static String sendHttpGet(String httpUrl) {
HttpGet httpGet = new HttpGet(httpUrl);// 创建get请求
return sendHttpGet(httpGet);
}
/**
* 发送 get请求Https
* @param httpUrl
*/
public static String sendHttpsGet(String httpUrl) {
HttpGet httpGet = new HttpGet(httpUrl);// 创建get请求
return sendHttpsGet(httpGet);
}
/**
* 发送Get请求
* @param httpGet
* @return
*/
private static String sendHttpGet(HttpGet httpGet) {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
HttpEntity entity = null;
String responseContent = null;
try {
// 创建默认的httpClient实例.
httpClient = HttpClients.createDefault();
httpGet.setConfig(requestConfig);
// 执行请求
response = httpClient.execute(httpGet);
entity = response.getEntity();
responseContent = EntityUtils.toString(entity, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 关闭连接,释放资源
if (response != null) {
response.close();
}
if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return responseContent;
}
/**
* 发送Get请求Https
* @param httpGet
* @return
*/
private static String sendHttpsGet(HttpGet httpGet) {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
HttpEntity entity = null;
String responseContent = null;
try {
// 创建默认的httpClient实例.
PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.load(new URL(httpGet.getURI().toString()));
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(publicSuffixMatcher);
httpClient = HttpClients.custom().setSSLHostnameVerifier(hostnameVerifier).build();
httpGet.setConfig(requestConfig);
// 执行请求
response = httpClient.execute(httpGet);
entity = response.getEntity();
responseContent = EntityUtils.toString(entity, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 关闭连接,释放资源
if (response != null) {
response.close();
}
if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return responseContent;
}
/**
* 发送post请求
*
* @param url
* @param params
* 没有参数则传入null
* @return
* @throws IOException
*/
public static String post(String url, Map<String, String> params) throws IOException {
// 创建http客户对象
CloseableHttpClient client = HttpClients.createDefault();
// 定义一个访问url后返回的结果对象
CloseableHttpResponse response = null;
// 创建HttpGet对象,如不携带参数可以直接传入url创建对象
HttpPost post = new HttpPost(url);
// 从结果对象中获取的内容
String content = null;
// 设置请求头,为浏览器访问
post.setHeader("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36");
// 设置表单项,对于的是一个添加方法,添加需要的属性
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
if (params != null && params.size() > 0) {
for (String key : params.keySet()) {
nvps.add(new BasicNameValuePair(key, params.get(key)));
}
}
// 设置表单项
post.setEntity(new UrlEncodedFormEntity(nvps, "utf-8"));
try {
// 访问这个url,并携带参数,获取结果对象
response = client.execute(post);
// 从结果对象中获取返回的内容
content = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭连接
if (response != null) {
response.close();
}
client.close();
}
return content;
}
/**
* get方式调用接口
*
* @param url
* @param params
* 没有参数则传入null
* @return
* @throws URISyntaxException
* @throws IOException
*/
public static String get(String url, Map<String, String> params) throws URISyntaxException, IOException {
// 创建http客户对象
CloseableHttpClient client = HttpClients.createDefault();
// 定义一个访问url后返回的结果对象
CloseableHttpResponse response = null;
// 从结果对象中获取的内容
String content = null;
// GET方法如果要携带参数先创建URIBuilder对象,然后设置参数,如果不携带可以忽略这步骤
URIBuilder builder = new URIBuilder(url);
if (params != null && params.size() > 0) {
for (String key : params.keySet()) {
builder.setParameter(key, params.get(key));
}
}
// 创建HttpGet对象,如不携带参数可以直接传入url创建对象
HttpGet get = new HttpGet(builder.build());
try {
// 访问这个url,并携带参数,获取结果对象
response = client.execute(get);
// 从结果对象中获取返回的内容
content = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
// 关闭连接
} finally {
if (response != null) {
response.close();
}
client.close();
}
return content;
}
/**
* 向指定URL发送GET方法的请求
*
* @param url
* 发送请求的URL
* @param param
* 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return URL 所代表远程资源的响应结果
*/
@SuppressWarnings("unused")
public static String sendGet(String url, String param) {
String result = "";
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
URL realUrl = new URL(urlNameString);
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置通用的请求属性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立实际的连接
connection.connect();
//设置相应请求时间
connection.setConnectTimeout(30000);
//设置读取超时时间
connection.setReadTimeout(30000);
// 获取所有响应头字段
Map<String, List<String>> map = connection.getHeaderFields();
// 遍历所有的响应头字段
/*for (String key : map.keySet()) {
//System.out.println(key + "--->" + map.get(key));
}*/
//System.out.println("响应时间--->" + map.get(null));
// 定义 BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(
connection.getInputStream(),"utf-8"));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println(e);
return "发送GET请求出现异常!";
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
/**
* 向指定 URL 发送POST方法的请求
*
* @param url
* 发送请求的 URL
* @param param
* 请求参数
* @return 所代表远程资源的响应结果
*/
public static String sendPost(String url, Map<String, String> param) {
PrintWriter out = null;
BufferedReader in = null;
String result = "";
try {
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
// 设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
// 设置相应请求时间
conn.setConnectTimeout(30000);
// 设置读取超时时间
conn.setReadTimeout(30000);
// 获取URLConnection对象对应的输出流
out = new PrintWriter(conn.getOutputStream());
// 发送请求参数
if (param != null && param.size() > 0) {
String paramStr = "";
for (String key : param.keySet()) {
paramStr += "&" + key + "=" + param.get(key);
}
paramStr = paramStr.substring(1);
out.print(paramStr);
}
// flush输出流的缓冲
out.flush();
// 定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println(e);
return "发送 POST 请求出现异常!";
}
// 使用finally块来关闭输出流、输入流
finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
return result;
}
/**
* 发送https请求
*
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return JSONObject(通过JSONObject.get ( key)的方式获取json对象的属性值)
*/
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
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);
// 当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();
inputStream = null;
conn.disconnect();
jsonObject = JSONUtil.parseObj(buffer.toString());
} catch (ConnectException ce) {
log.error("连接超时:{}", ce);
} catch (Exception e) {
log.error("https请求异常:{}", e);
}
return jsonObject;
}
public static String getSha1(String str) {
if (str == null || str.length() == 0) {
return null;
}
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes("UTF-8"));
byte[] md = mdTemp.digest();
int j = md.length;
char buf[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (Exception e) {
return null;
}
}
/**
* 发送https请求
*
* @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对象,并使用我们指定的信任管理器初始化
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
TrustManager[] tm = { new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
} };
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上面对象中得到SSLSocketFactory
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(path);
conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(get|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;
}
}
3)通过微信扫码回调的code获取AccessToken对象封装
import lombok.Data;
/**
* @Description: 通过code获取access_token</p>
* @Date: 2021-03-10
* @Author: Jonathan.WQ
* @Version V1.0
* @Modified By:
*/
@Data
public class AccessToken {
/**
* 接口调用凭证
*/
private String access_token;
/**
* access_token接口调用凭证超时时间,单位(秒)
*/
private Integer expires_in;
/**
* 用户刷新access_token
*/
private String refresh_token;
/**
* 授权用户唯一标识
*/
private String openid;
/**
* 用户授权的作用域,使用逗号(,)分隔
*/
private String scope;
/**
* 当且仅当该网站应用已获得该用户的userinfo授权时,才会出现该字段。
*/
private String unionid;
}
4)微信用户对象封装
/**
* @Description: 微信用户对象
* @Date: 2021-03-10
* @Author: Jonathan.WQ
* @Version V1.0
* @Modified By:
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("member_wechat")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class MemberWechat extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableField("open_id")
private String openId;//微信的openid
@TableField("mini_open_id")
private String miniOpenId;//小程序的openId
@TableField("union_id")
private String unionId;//用户在微信的唯一标识
@TableField("member_id")
private String memberId;//会员ID
@TableField("groupid")
private Integer groupid;//用户所在的分组ID(兼容旧的用户分组接口)
@TableField("province")
private String province;//用户所在省份
@TableField("headimgurl")
private String headimgurl;//用户头像
@TableField("nickname")
private String nickname;//用户的昵称
@TableField("language")
private String language;//用户的语言,简体中文为zh_CN
@TableField("sex")
private Integer sex;//性别
@TableField("subscribe_time")
private Date subscribeTime;//用户关注时间
@TableField("subscribe")
private Integer subscribe;//用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息
@TableField("country")
private String country;//用户所在国家
@TableField("city")
private String city;//用户所在城市
@TableField("create_user")
private String createUser;//
@TableField("create_time")
private Date createTime;//
@TableField("update_user")
private String updateUser;//
@TableField("update_time")
private Date updateTime;//
@TableField("data_status")
private Integer dataStatus;//
@TableField("version")
private Integer version;//
@TableField(exist = false)
private Integer errcode;
@TableField(exist = false)
private String errmsg;
public MemberWechat() {
}
}
5)获取微信扫码的二维码:
@ApiOperation("获取微信二维码")
@ResponseBody
@RequestMapping("/api-uaa/oauth/wechat/wxLogin")
public CommonResult toLogin(HttpServletRequest request, @RequestParam(value = "redirectUrl", required = false) String redirectUrl) {
if (StringUtils.isEmpty(redirectUrl)) {//redirectUrl为扫码成功之后需要跳转的页面地址
return new CommonResult().validateFailed("redirectUrl参数不能为空");
}
//缓存redirectURL地址
redisTemplate.set("PROJECT:MEMBERWECHAT:REDIRECTURL", redirectUrl);
String url = weChatService.getWeChatLoginUrl();
return new CommonResult().success(url);
}
备注:CommonResult类很简单,就提供三个属性:data(数据)、msg(消息)、code(状态码),关于状态码大家可以根据自身项目需要与前端沟通好预设好就行(例如:20000成功,20001失败,20004无权限,20003认证失败)。
6)WeChatService
特别注意:报40163的错误就是在这里生成链接的时候
/**
* @Description: (用一句话描述该文件的作用)
* @Date: 2020-11-23
* @Author: WQ
* @Version V1.0
* @Modified By:
*/
@Service
public class WeChatServiceImpl implements WeChatService {
@Value(("${wx.qrCode}"))
private String url;
@Value("${wx.appId}")
private String appId;
@Value("${wx.redirect_uri}")
private String redirectUri;
@Value("${wx.openVisitUrl}")
private String openVisitUrl;
@Value("${wx.webAccessTokenHttpsOAuth}")
private String webAccessTokenHttpsOAuth;
@Value("${wx.secret}")
private String appSecret;
@Autowired
private RedisTemplate redisTemplate;
@Override
public String getWeChatLoginUrl() {
String content = CommonConstant.PWD_MD5 + DateUtils.format(Calendar.getInstance().getTime(), "yyyyMMdd");
byte[] encrypt = AesUtil.encrypt(content, AesUtil.PASSWORD_SECRET_KEY, 16);
String parseByte2HexStr = AesUtil.parseByte2HexStr(encrypt);
String wxLoginUrl = url;
wxLoginUrl = wxLoginUrl.replaceAll("APPID", appId);
try {
wxLoginUrl = wxLoginUrl.replaceAll("REDIRECT_URI", URLEncoder.encode(
openVisitUrl + redirectUri, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
wxLoginUrl = wxLoginUrl.replaceAll("SCOPE", "snsapi_login");
wxLoginUrl = wxLoginUrl.replace("STATE", parseByte2HexStr); //加密state进行验证 回调地址当天有效 防止恶意攻击
return wxLoginUrl;
}
/**
*40163错误就出现在这里,code不能重复使用。如果直接读取配置文 件的链接并替换相关占位符参数,使用原来的webAccessTokenHttpsOAuth接收,会导致code不能及时被替换成新获得的code,用生成的链接请求微信获取AccessToken时就会报40163的错误
错误代码:
@Override
public AccessToken getAccessToken(String code) {
webAccessTokenHttpsOAuth = webAccessTokenHttpsOAuth.replaceAll("APPID", appId);
webAccessTokenHttpsOAuth = webAccessTokenHttpsOAuth.replaceAll("SECRET", appSecret);
webAccessTokenHttpsOAuth = webAccessTokenUrl.replaceAll("CODE", code);
String responseContent = HttpUtils.sendHttpGet(webAccessTokenHttpsOAuth);
if (responseContent == null || responseContent == "") {
return null;
}
JSONObject parseObject = JSONObject.parseObject(responseContent);
AccessToken accessToken = JSONObject.toJavaObject(parseObject, AccessToken.class);
return accessToken;
}
*/
@Override
public AccessToken getAccessToken(String code) {
String webAccessTokenUrl = webAccessTokenHttpsOAuth;
webAccessTokenUrl = webAccessTokenUrl.replaceAll("APPID", appId);
webAccessTokenUrl = webAccessTokenUrl.replaceAll("SECRET", appSecret);
webAccessTokenUrl = webAccessTokenUrl.replaceAll("CODE", code);
String responseContent = HttpUtils.sendHttpGet(webAccessTokenUrl);
if (responseContent == null || responseContent == "") {
return null;
}
JSONObject parseObject = JSONObject.parseObject(responseContent);
AccessToken accessToken = JSONObject.toJavaObject(parseObject, AccessToken.class);
return accessToken;
}
}
7)扫码授权成功之后的回调方法
/**
* 回调地址处理(上面方法的备份)
*
* @param code 授权回调码
* @param state 状态参数(防止跨站伪造攻击)
* @return
*/
@GetMapping( "/api-uaa/oauth/wechat/callback")
public ModelAndView callback(String code, String state) {
String redirectUrl = String.valueOf(redisRepository.get("PROJECT:MEMBERWECHAT:REDIRECTURL"));
ModelAndView modelAndView = new ModelAndView();
try {
if (code != null && state != null) {
// 验证state为了用于防止跨站请求伪造攻击
String decrypt = AesUtil.decrypt(AesUtil.parseHexStr2Byte(state), AesUtil.PASSWORD_SECRET_KEY, 16);
if (!decrypt.equals(CommonConstant.PWD_MD5 + DateUtils.format(Calendar.getInstance().getTime(), "yyyyMMdd"))) {
//校验失败跳转
modelAndView.setViewName("redirect:" + redirectUrl);
return modelAndView;
}
AccessToken access = weChatService.getAccessToken(code);
if (access != null ) {
// 把获取到的OPENID和ACCESS_TOKEN写到redis中,用于校验用户授权的微信用户是否存在于我们的系统中,用完即删除
redisRepository.set(SecurityMemberConstants.WEIXIN_TOKEN_CACHE_KEY + ":" + "ACCESS_TOKEN", access.getAccess_token());
redisTemplate.setExpire(SecurityMemberConstants.WEIXIN_TOKEN_CACHE_KEY + ":" + "OPEN_ID", access.getOpenid(), 60 * 60);//一个小时过期
// 拿到openid获取微信用户的基本信息
MemberWechat memberWechat = umsCenterFeignService.selectByOpenId(access.getOpenid());
boolean isExists = memberWechat == null ? false : true;
if (!isExists) {//不存在
// 跳转绑定页面
modelAndView.setViewName("redirect:" + openVisitUrl + "/bind");
} else {
//校验是否已经绑定过了系统用户(之前绑定过,但是解绑了)
if (memberWechat.getMemberId() == null) {
modelAndView.setViewName("redirect:" + openVisitUrl + "/bind");
} else {
// 存在则跳转前端传递的redirectURL,并携带OPENID和state参数
String content = CommonConstant.PWD_MD5 + DateUtils.format(Calendar.getInstance().getTime(), "yyyyMMdd");
byte[] encrypt = AesUtil.encrypt(content, AesUtil.PASSWORD_SECRET_KEY, 16);
String parseByte2HexStr = AesUtil.parseByte2HexStr(encrypt);
if (redirectUrl.contains("?")) {
modelAndView.setViewName("redirect:" + openVisitUrl + redirectUrl + "&openId=" + access.getOpenid() + "&state=" + parseByte2HexStr);
} else {
modelAndView.setViewName("redirect:" + openVisitUrl + redirectUrl + "?openId=" + access.getOpenid() + "&state=" + parseByte2HexStr);
}
}
}
return modelAndView;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
redisRepository.del("PROJECT:MEMBERWECHAT:REDIRECTURL");
}
modelAndView.setViewName("redirect:" + openVisitUrl + redirectUrl);//登录失败跳转
return modelAndView;
}
备注:
MemberWechat memberWechat =umsCenterFeignService.selectByOpenId(access.getOpenid());这个就是拿着openId去自己搭建的系统看是否存在该用户,不存在则添加,根据自身项目需要编写相关逻辑(因为需要跨服务调用,所以才缓存AccessToken和OpenId)
8)如果已绑定系统账号,需要通过获得的openId和state请求后台接口获得token令牌
@ApiOperation(value = "openId获取token")
@PostMapping("/api-uaa/oauth/openId/ums/token")
public void getTokenByOpenId(@ApiParam(required = true, name = "openId", value = "openId") String
openId, @ApiParam(required = true, name = "state", value = "state") String
state, HttpServletRequest request, HttpServletResponse response) throws IOException {
String decrypt = AesUtil.decrypt(AesUtil.parseHexStr2Byte(state), AesUtil.PASSWORD_SECRET_KEY, 16);
if (!decrypt.equals(CommonConstant.PWD_MD5 + DateUtils.format(Calendar.getInstance().getTime(), "yyyyMMdd"))) {
exceptionHandler(response, "非法登录");
}
MemberWechat member = umsCenterFeignService.selectByOpenId(openId);
if (member != null) {
MemberInfo memberInfo = umsCenterFeignService.selectById(member.getMemberId());
OpenIdMemberAuthenticationToken token = new OpenIdMemberAuthenticationToken(openId);
writeToken(request, response, token, "openId错误", member.getMemberId());
} else {
exceptionHandler(response, "openId错误");
}
}
备注:具体的通过Feign跨服务调用的方法就不细写了,这个相对来说比较简单。最后附上writeToken()方法:
private void writeToken(HttpServletRequest request, HttpServletResponse response,
AbstractAuthenticationToken token, String badCredenbtialsMsg, String memberId) throws IOException {
try {
//Nginx默认是过滤掉以_开头的参数的,su
String clientId = request.getHeader("client-id");
String clientSecret = request.getHeader("client-secret");
if (StringUtils.isBlank(clientId)) {
throw new UnapprovedClientAuthenticationException("请求头中无client-id信息");
}
if (StringUtils.isBlank(clientSecret)) {
throw new UnapprovedClientAuthenticationException("请求头中无client-secret信息");
}
Map<String, String> requestParameters = new HashedMap();
requestParameters.put("memberId", memberId);
ClientDetails clientDetails = getClient(clientId, clientSecret, null);
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, clientDetails.getScope(),
"customer");
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
Authentication authentication = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices
.createAccessToken(oAuth2Authentication);
oAuth2Authentication.setAuthenticated(true);
writerObj(response, oAuth2AccessToken);
} catch (BadCredentialsException | InternalAuthenticationServiceException e) {
exceptionHandler(response, badCredenbtialsMsg);
e.printStackTrace();
} catch (Exception e) {
exceptionHandler(response, e);
}
}