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);
}
}