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: wxfb72c85ee5329311secret: e6eba215f6df135d023e42d69b17f4e0redirect_uri: /api-uaa/oauth/wechat/callbackopenVisitUrl: http://www.super.comqrCode: https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE&connect_redirect=1#wechat_redirectwebAccessTokenHttpsOAuth: 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、补码方式PKCS5PaddingCipher 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);//CBCcipher.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、补码方式PKCS5PaddingCipher 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);//CBCcipher.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);// 创建httpPostreturn sendHttpPost(httpPost);}/*** 发送 post请求* @param httpUrl 地址* @param params 参数(格式:key1=value1&key2=value2)*/public static String sendHttpPost(String httpUrl, String params) {HttpPost httpPost = new HttpPost(httpUrl);// 创建httpPosttry {//设置参数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() {@Overridepublic void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}@Overridepublic void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}@Overridepublic X509Certificate[] getAcceptedIssuers() {return null;}} };sslContext.init(null, tm, new java.security.SecureRandom());// 从上面对象中得到SSLSocketFactorySSLSocketFactory 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;@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic 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的错误
错误代码:@Overridepublic 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;}*/@Overridepublic 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") StringopenId, @ApiParam(required = true, name = "state", value = "state") Stringstate, 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默认是过滤掉以_开头的参数的,suString 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);}}

解决网页微信扫码登录报40163相关推荐

  1. 微信开放平台开发——网页微信扫码登录(OAuth2.0)

    1.OAuth2.0 OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用. 允许用户提供 ...

  2. 通过微信扫码登录剖析 oauth2 认证授权技术

    本文目录 前言 趣味解读oauth2 oauth2精髓 oauth2核心概念 结合微信登录深刻理解oauht2 本文小结 前言 相信很多小伙伴在学习 JAVA 的过程中或多或少接触或者开发过类似于 x ...

  3. vue3、vue2 实现网站微信扫码登录

    其实前端实现没什么难点,重点都是在后端,这里我记录了一下前端实现扫码登录的做法 准备工作 | 微信开放文档  做之前我们先去看一下大致流程 记得要看一下参数 都讲的很清楚了 实现网页微信扫码登录有两种 ...

  4. 「微信群合影2.5.0」- 微信网页版账号不能登录解决办法,扫码登录

    「微信群合影qunheying.com」- 一键生成微信全家福 「 微信群合影 2.5.0 」版本更新: 支持微信网页版不能登录账号生成群合影, 通过扫码登录获取 在一键生成全家福的过程中,有一些用户 ...

  5. vue实现网页端企业微信扫码登录功能(前端部分)

     时至今日,企业微信在企业日常工作中的使用越来越频繁也越来越重要,不少企业已使用企业微信进行着日常的工作安排管理.在这种背景下,各类系统和企业微信对接的需求也不断增加,今天要说的就是一个比较常见的需求 ...

  6. 微信扫码登录网页实现原理

    扫码登录操作过程 浏览器输入:https://wx.qq.com/?lang=zh_CN 手机登录微信,利用"扫一扫"功能扫描网页上的二维码 手机扫描成功后,提示"登录网 ...

  7. (转)微信扫码登录网页实现原理

    扫码登录操作过程 浏览器输入:https://wx.qq.com/?lang=zh_CN 手机登录微信,利用"扫一扫"功能扫描网页上的二维码 手机扫描成功后,提示"登录网 ...

  8. 基于Spring Boot实现电脑端网页微信扫码授权登录方式一(附带完整源码)

    简介 电脑端微信网页扫码授权登录有2种方式: 第一种:基于微信公众号,单独获取登录二维码扫码,然后扫码登录,程序控制跳转逻辑,例如CSDN: 第二种:基于微信开放平台,跳转到微信二维码页面进行扫码登录 ...

  9. 网页端企业微信扫码登录及其cookie问题

    这个问题折磨了我6个小时,太痛苦了,特此记录! 1:企业微信扫码登录 1:环境 前端 vue  后端spring-gateWay+springboot 2:实现过程 1:前端生成二维码,回调地址直接写 ...

  10. 关于网页版微信扫码登录以及获取用户信息

    由于我只研究了1天时间,可能有些地方认知错误 1:网页版微信扫码登录的流程 ① 点击扫码登录按钮 ② qrAuthorize(访问微信接口,如果微信接口判断有权限生成二维码的话,跳转到二维码页面.) ...

最新文章

  1. 一起谈.NET技术,ASP.NET Eval如何进行数据绑定
  2. 三层交换机实现VLAN互通实例
  3. c:数据结构-线性表
  4. php $表达式,Notepad++
  5. bs4抓起大众点评的用户评论
  6. 1976年,提出公钥密码体制概念的学者
  7. 牛客网_PAT乙级_1025插入与归并(25)
  8. ARM Linux启动过程分析
  9. C什么k什么_K线图基础知识丨什么是K线散兵坑形态?K线散兵坑形态的操作与案例详解...
  10. 如何提高NLP模型鲁棒性和泛化能力?对抗训练论文串讲
  11. 安全攻防之BadUsb攻击之CS上线
  12. 密码学-密钥管理与分发
  13. 网易互娱2017实习生招聘在线笔试第一场-3划线
  14. 钉钉勋章在哪看?钉钉勋章查看方法
  15. 火影忍者379在线观看
  16. xmlHttp.send(null)与xmlHttp.send…
  17. SEO的工作内容是什么?
  18. vscode实现边写边查
  19. 又有人因为买考研资料被骗!4种常见骗局曝光,第三种最可恶!
  20. Android Studio 4.1 发布

热门文章

  1. 数据结构二叉树学习1-前序序列创建二叉树
  2. pands 画图 调整大小_用宏命令对word里的图片尺寸大小进行批量修改的方法
  3. 新手教程直连路由的配置
  4. 交换机端口详细配置Trunk
  5. 现浇板用弹性计算方法_弹性楼板的计算和选择
  6. 戴尔Latitude5285笔记本触摸板失灵的原因
  7. 如何从shutterstock下载无水印图片
  8. 面向对象程序设计原则——依赖倒置原则(DIP)
  9. 国内优秀开源镜像站汇总
  10. myeclipse取消快捷键