From 46b3a5d007210b5790a05cbfe8ce3581a808d14a Mon Sep 17 00:00:00 2001 From: "LAPTOP-SB56SG4Q\\86185" Date: Mon, 28 Feb 2022 16:24:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=BC=80=E6=BA=90=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=9D=90=E6=96=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- JSD-7565需求确认书.docx | Bin 0 -> 40146 bytes README.md | 5 +- plugin.xml | 18 + .../com/fr/plugin/nfsq/sso/DesECBUtil.java | 64 +++ .../java/com/fr/plugin/nfsq/sso/HttpUtil.java | 479 ++++++++++++++++ .../java/com/fr/plugin/nfsq/sso/Params.java | 40 ++ .../com/fr/plugin/nfsq/sso/SsoFilter.java | 537 ++++++++++++++++++ .../fr/plugin/nfsq/sso/SsoHttpHandler.java | 103 ++++ .../nfsq/sso/SsoRequestHandlerBridge.java | 19 + .../nfsq/sso/SsoRequestURLAliasBridge.java | 18 + 10 files changed, 1282 insertions(+), 1 deletion(-) create mode 100644 JSD-7565需求确认书.docx create mode 100644 plugin.xml create mode 100644 src/main/java/com/fr/plugin/nfsq/sso/DesECBUtil.java create mode 100644 src/main/java/com/fr/plugin/nfsq/sso/HttpUtil.java create mode 100644 src/main/java/com/fr/plugin/nfsq/sso/Params.java create mode 100644 src/main/java/com/fr/plugin/nfsq/sso/SsoFilter.java create mode 100644 src/main/java/com/fr/plugin/nfsq/sso/SsoHttpHandler.java create mode 100644 src/main/java/com/fr/plugin/nfsq/sso/SsoRequestHandlerBridge.java create mode 100644 src/main/java/com/fr/plugin/nfsq/sso/SsoRequestURLAliasBridge.java diff --git a/JSD-7565需求确认书.docx b/JSD-7565需求确认书.docx new file mode 100644 index 0000000000000000000000000000000000000000..1b2e2937efeb31d7d6dba03fd562d77084950a2d GIT binary patch literal 40146 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 AMgRZ+ literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 344f3a2..df53d49 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # open-JSD-7565 -JSD-7565 第三方token交换fine_auth_token \ No newline at end of file +JSD-7565 第三方token交换fine_auth_token\ +免责说明:该源码为第三方爱好者提供,不保证源码和方案的可靠性,也不提供任何形式的源码教学指导和协助!\ +仅作为开发者学习参考使用!禁止用于任何商业用途!\ +为保护开发者隐私,开发者信息已隐去!若原开发者希望公开自己的信息,可联系hugh处理。 \ No newline at end of file diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..4890a75 --- /dev/null +++ b/plugin.xml @@ -0,0 +1,18 @@ + + + com.fr.plugin.nfsq.sso + + yes + 1.19 + 10.0 + 2018-07-31 + fr.open + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/fr/plugin/nfsq/sso/DesECBUtil.java b/src/main/java/com/fr/plugin/nfsq/sso/DesECBUtil.java new file mode 100644 index 0000000..34afd35 --- /dev/null +++ b/src/main/java/com/fr/plugin/nfsq/sso/DesECBUtil.java @@ -0,0 +1,64 @@ +package com.fr.plugin.nfsq.sso; + +import com.fr.third.org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; + +/** + * @author fr.open + * @date 2019/1/18 + */ +public class DesECBUtil { + /** + * 加密数据 + * + * @param encryptString + * @param encryptKey + * @return + * @throws Exception + */ + public static String encryptDES(String encryptString, String encryptKey) throws Exception { + Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(getKey(encryptKey), "DES")); + byte[] encryptedData = cipher.doFinal(encryptString.getBytes("UTF-8")); + return Base64.encodeBase64String(encryptedData); + } + + /** + * key 不足8位补位 + * + * @param + */ + public static byte[] getKey(String keyRule) { + Key key = null; + byte[] keyByte = keyRule.getBytes(); + // 创建一个空的八位数组,默认情况下为0 + byte[] byteTemp = new byte[8]; + // 将用户指定的规则转换成八位数组 + for (int i = 0; i < byteTemp.length && i < keyByte.length; i++) { + byteTemp[i] = keyByte[i]; + } + key = new SecretKeySpec(byteTemp, "DES"); + return key.getEncoded(); + } + + /*** + * 解密数据 + * @param decryptString + * @param decryptKey + * @return + * @throws Exception + */ + + public static String decryptDES(String decryptString, String decryptKey) throws Exception { + byte[] sourceBytes = Base64.decodeBase64(decryptString); + Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(getKey(decryptKey), "DES")); + byte[] decoded = cipher.doFinal(sourceBytes); + return new String(decoded, "UTF-8"); + + } +} + diff --git a/src/main/java/com/fr/plugin/nfsq/sso/HttpUtil.java b/src/main/java/com/fr/plugin/nfsq/sso/HttpUtil.java new file mode 100644 index 0000000..0b3f04f --- /dev/null +++ b/src/main/java/com/fr/plugin/nfsq/sso/HttpUtil.java @@ -0,0 +1,479 @@ +package com.fr.plugin.nfsq.sso; + +import com.fr.json.JSONObject; +import com.fr.log.FineLoggerFactory; +import com.fr.stable.StringUtils; +import com.fr.third.org.apache.http.HttpResponse; +import com.fr.third.org.apache.http.HttpStatus; +import com.fr.third.org.apache.http.NameValuePair; +import com.fr.third.org.apache.http.client.HttpClient; +import com.fr.third.org.apache.http.client.entity.UrlEncodedFormEntity; +import com.fr.third.org.apache.http.client.methods.HttpPost; +import com.fr.third.org.apache.http.client.methods.HttpPut; +import com.fr.third.org.apache.http.config.Registry; +import com.fr.third.org.apache.http.config.RegistryBuilder; +import com.fr.third.org.apache.http.conn.socket.ConnectionSocketFactory; +import com.fr.third.org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import com.fr.third.org.apache.http.conn.socket.PlainConnectionSocketFactory; +import com.fr.third.org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import com.fr.third.org.apache.http.conn.ssl.SSLContexts; +import com.fr.third.org.apache.http.conn.ssl.TrustStrategy; +import com.fr.third.org.apache.http.entity.StringEntity; +import com.fr.third.org.apache.http.impl.client.CloseableHttpClient; +import com.fr.third.org.apache.http.impl.client.HttpClientBuilder; +import com.fr.third.org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import com.fr.third.org.apache.http.message.BasicNameValuePair; +import com.fr.third.org.apache.http.util.EntityUtils; + +import javax.net.ssl.*; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author fr.open + * @date 2019/4/2 + */ +public class HttpUtil { + + private static HostnameVerifier hv = new HostnameVerifier() { + @Override + public boolean verify(String urlHostName, SSLSession session) { + System.out.println("Warning: URL Host: " + urlHostName + " vs. " + + session.getPeerHost()); + return true; + } + }; + + /** + * 发送get请求 + * + * @param url + * @param param + * @param header + * @return + * @throws IOException + */ + public static String sendGet(String url, Map param, Map header, String charset) { + String result = ""; + BufferedReader in = null; + String urlNameString = url; + try { + if (param != null && !param.isEmpty()) { + urlNameString += "?"; + urlNameString += param.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue().toString()) + .collect(Collectors.joining("&")); + } + + URL realUrl = new URL(urlNameString); + // 打开和URL之间的连接 + HttpURLConnection connection; + if (url.startsWith("https")) { + trustAllHttpsCertificates(); + HttpsURLConnection.setDefaultHostnameVerifier(hv); + connection = (HttpURLConnection) realUrl.openConnection(); + } else { + connection = (HttpURLConnection) realUrl.openConnection(); + } + //设置超时时间 + connection.setDoInput(true); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(15000); + // 设置通用的请求属性 + if (header != null) { + Iterator> it = header.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + System.out.println(entry.getKey() + ":::" + entry.getValue()); + connection.setRequestProperty(entry.getKey(), entry.getValue().toString()); + } + } + 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(); + if(connection.getResponseCode() == 200){ + // 定义 BufferedReader输入流来读取URL的响应,设置utf8防止中文乱码 + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), charset == null ? "utf-8" : charset)); + String line; + while ((line = in.readLine()) != null) { + result += line; + } + if (in != null) { + in.close(); + } + }else { + in = new BufferedReader(new InputStreamReader(connection.getErrorStream(), charset == null ? "utf-8" : charset)); + String line; + while ((line = in.readLine()) != null) { + result += line; + } + if (in != null) { + in.close(); + } + FineLoggerFactory.getLogger().error("Http post form code is {},message is {}",connection.getResponseCode(),result); + return StringUtils.EMPTY; + } + + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e, "get url error ,url is:{},error is {}", urlNameString, e.getMessage()); + } + return result; + } + + public static String sendPost(String url, Map header, JSONObject body) { + PrintWriter out = null; + BufferedReader in = null; + String result = null; + String res = null; + try { + String urlNameString = url; + + URL realUrl = new URL(urlNameString); + // 打开和URL之间的连接 + HttpURLConnection conn; + if (url.startsWith("https")) { + trustAllHttpsCertificates(); + HttpsURLConnection.setDefaultHostnameVerifier(hv); + conn = (HttpURLConnection) realUrl.openConnection(); + } else { + conn = (HttpURLConnection) 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)"); + conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); + if (header != null) { + header.forEach((k, v) -> { + conn.setRequestProperty(k, String.valueOf(v)); + }); + } + // 发送POST请求必须设置如下两行 + conn.setDoOutput(true); + conn.setDoInput(true); + //获取请求头 + + // 获取URLConnection对象对应的输出流 + out = new PrintWriter(conn.getOutputStream()); + // 发送请求参数 + if (body != null) { + FineLoggerFactory.getLogger().error("content data: {}", body.toString()); + FineLoggerFactory.getLogger().error("content cover data: {}", new String(body.toString().getBytes("UTF-8"), "UTF-8")); + out.print(new String(body.toString().getBytes("UTF-8"), "UTF-8")); + } + // flush输出流的缓冲 + out.flush(); + // 定义BufferedReader输入流来读取URL的响应 + in = new BufferedReader( + new InputStreamReader(conn.getInputStream())); + String line; + while ((line = in.readLine()) != null) { + result += line; + } + res = result; + if (res.startsWith("null")) { + res = res.replace("null", ""); + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + //使用finally块来关闭输出流、输入流 + finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (IOException e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + } + return res; + } + + + public static String doJSONPost(String url, Map header, JSONObject json, Map param, String chartset) { + HttpClient client = getHttpsClient(); + /*if (url.startsWith("https")) { + SSLContext sslcontext = createIgnoreVerifySSL(); + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + HttpClients.custom().setConnectionManager(connManager); + client = HttpClients.custom().setConnectionManager(connManager).build(); + }*/ + if (param != null && !param.isEmpty()) { + url += "?"; + url += param.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + } + HttpPost post = new HttpPost(url); + post.setHeader("accept", "*/*"); + post.setHeader("connection", "Keep-Alive"); + post.setHeader("Content-Type", "application/json"); + if (header != null) { + header.forEach((k, v) -> { + post.setHeader(k, v.toString()); + }); + } + try { + StringEntity s = new StringEntity(json.toString(), chartset == null ? "UTF-8" : chartset); + s.setContentEncoding("UTF-8"); + s.setContentType("application/json; charset=UTF-8");//发送json数据需要设置contentType + post.setEntity(s); + HttpResponse res = client.execute(post); + if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + String result = EntityUtils.toString(res.getEntity());// 返回json格式: + return result; + } else { + FineLoggerFactory.getLogger().error("Http post form code is {},message is {}", res.getStatusLine().getStatusCode(), EntityUtils.toString(res.getEntity())); + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return null; + } + + public static String doJSONPut(String url, Map header, JSONObject json, Map param, String chartset) { + HttpClient client = getHttpsClient(); + /*if (url.startsWith("https")) { + SSLContext sslcontext = createIgnoreVerifySSL(); + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + HttpClients.custom().setConnectionManager(connManager); + client = HttpClients.custom().setConnectionManager(connManager).build(); + }*/ + if (param != null && !param.isEmpty()) { + url += "?"; + url += param.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + } + HttpPut post = new HttpPut(url); + post.setHeader("accept", "*/*"); + post.setHeader("connection", "Keep-Alive"); + post.setHeader("Content-Type", "application/json"); + if (header != null) { + header.forEach((k, v) -> { + post.setHeader(k, v.toString()); + }); + } + try { + StringEntity s = new StringEntity(json.toString(), chartset == null ? "UTF-8" : chartset); + s.setContentEncoding("UTF-8"); + s.setContentType("application/json; charset=UTF-8");//发送json数据需要设置contentType + post.setEntity(s); + HttpResponse res = client.execute(post); + if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + String result = EntityUtils.toString(res.getEntity());// 返回json格式: + return result; + } else { + FineLoggerFactory.getLogger().error("Http post form code is {},message is {}", res.getStatusLine().getStatusCode(), EntityUtils.toString(res.getEntity())); + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return null; + } + + + public static String doFormPost(String url,Map header, Map map, String chartset) { + //声明返回结果 + String result = ""; + UrlEncodedFormEntity entity = null; + HttpResponse httpResponse = null; + HttpClient httpClient = null; + try { + // 创建连接 + httpClient = getHttpsClient(); + ; + /*if (url.startsWith("https")) { + SSLContext sslcontext = createIgnoreVerifySSL(); + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + HttpClients.custom().setConnectionManager(connManager); + httpClient = HttpClients.custom().setConnectionManager(connManager).build(); + }*/ + // 设置请求头和报文 + HttpPost httpPost = new HttpPost(url); + if (header != null) { + header.forEach((k, v) -> { + httpPost.setHeader(k, v.toString()); + }); + } + //设置参数 + List list = new ArrayList(); + Iterator iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry elem = (Map.Entry) iterator.next(); + list.add(new BasicNameValuePair(elem.getKey(), elem.getValue())); + } + entity = new UrlEncodedFormEntity(list, chartset == null ? "UTF-8" : chartset); + httpPost.setEntity(entity); + //执行发送,获取相应结果 + httpResponse = httpClient.execute(httpPost); + if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + result = EntityUtils.toString(httpResponse.getEntity()); + } else { + FineLoggerFactory.getLogger().error("Http post form code is {},message is {}", httpResponse.getStatusLine().getStatusCode(), EntityUtils.toString(httpResponse.getEntity())); + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return result; + + } + + private static void trustAllHttpsCertificates() throws Exception { + TrustManager[] trustAllCerts = new TrustManager[1]; + TrustManager tm = new miTM(); + trustAllCerts[0] = tm; + SSLContext sc = SSLContext.getInstance("SSL", "SunJSSE"); + sc.init(null, trustAllCerts, null); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } + + + /** + * encode url by UTF-8 + * + * @param url url before encoding + * @return url after encoding + */ + public static String encodeUrl(String url) { + String eurl = url; + try { + eurl = URLEncoder.encode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + } + return eurl; + } + + private static class miTM implements TrustManager, + X509TrustManager { + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + + public boolean isServerTrusted( + java.security.cert.X509Certificate[] certs) { + return true; + } + + public boolean isClientTrusted( + java.security.cert.X509Certificate[] certs) { + return true; + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] certs, String authType) + throws CertificateException { + return; + } + + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] certs, String authType) + throws CertificateException { + return; + } + } + + public static SSLContext createIgnoreVerifySSL() { + try { + SSLContext sc = SSLContext.getInstance("TLSv1.2"); + + // 实现一个X509TrustManager接口,用于绕过验证,不用修改里面的方法 + X509TrustManager trustManager = new X509TrustManager() { + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) throws CertificateException { + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) throws CertificateException { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + sc.init(null, new TrustManager[]{trustManager}, null); + return sc; + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return null; + } + + private static CloseableHttpClient getHttpsClient() { + RegistryBuilder registryBuilder = RegistryBuilder.create(); + ConnectionSocketFactory plainSF = new PlainConnectionSocketFactory(); + registryBuilder.register("http", plainSF); + // 指定信任密钥存储对象和连接套接字工厂 + try { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + // 信任任何链接 + TrustStrategy anyTrustStrategy = new TrustStrategy() { + + @Override + public boolean isTrusted(java.security.cert.X509Certificate[] arg0, String arg1) throws CertificateException { + // TODO Auto-generated method stub + return true; + } + }; + SSLContext sslContext = SSLContexts.custom().useTLS().loadTrustMaterial(trustStore, anyTrustStrategy).build(); + LayeredConnectionSocketFactory sslSF = new SSLConnectionSocketFactory(sslContext, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + registryBuilder.register("https", sslSF); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } catch (KeyManagementException e) { + throw new RuntimeException(e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + Registry registry = registryBuilder.build(); + // 设置连接管理器 + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(registry); + // 构建客户端 + return HttpClientBuilder.create().setConnectionManager(connManager).build(); + + } +} diff --git a/src/main/java/com/fr/plugin/nfsq/sso/Params.java b/src/main/java/com/fr/plugin/nfsq/sso/Params.java new file mode 100644 index 0000000..d38cf13 --- /dev/null +++ b/src/main/java/com/fr/plugin/nfsq/sso/Params.java @@ -0,0 +1,40 @@ +package com.fr.plugin.nfsq.sso; + +/** + * @Author fr.open + * @Date 2021/5/10 + * @Description + **/ +public class Params { + + private String username; + + private String accessToken; + + private String refreshToken; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + +} diff --git a/src/main/java/com/fr/plugin/nfsq/sso/SsoFilter.java b/src/main/java/com/fr/plugin/nfsq/sso/SsoFilter.java new file mode 100644 index 0000000..554d33e --- /dev/null +++ b/src/main/java/com/fr/plugin/nfsq/sso/SsoFilter.java @@ -0,0 +1,537 @@ +package com.fr.plugin.nfsq.sso; + +import com.fr.base.TemplateUtils; +import com.fr.data.NetworkHelper; +import com.fr.decision.authority.data.User; +import com.fr.decision.fun.impl.AbstractGlobalRequestFilterProvider; +import com.fr.decision.mobile.terminal.TerminalHandler; +import com.fr.decision.webservice.bean.authentication.LoginRequestInfoBean; +import com.fr.decision.webservice.exception.user.UserNotExistException; +import com.fr.decision.webservice.utils.DecisionServiceConstants; +import com.fr.decision.webservice.utils.DecisionStatusService; +import com.fr.decision.webservice.v10.login.LoginService; +import com.fr.decision.webservice.v10.login.TokenResource; +import com.fr.decision.webservice.v10.user.UserService; +import com.fr.general.http.HttpRequest; +import com.fr.general.http.HttpToolbox; +import com.fr.io.utils.ResourceIOUtils; +import com.fr.json.JSONObject; +import com.fr.log.FineLoggerFactory; +import com.fr.plugin.transform.FunctionRecorder; +import com.fr.stable.StringUtils; +import com.fr.stable.web.Device; +import com.fr.web.utils.WebUtils; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.time.Instant; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @Author fr.open + * @Date 2020/9/10 + * @Description + **/ +@FunctionRecorder +public class SsoFilter extends AbstractGlobalRequestFilterProvider { + + private final String CODE_KEY = "90bb6046"; + + @Override + public String filterName() { + return "nongfu"; + } + + @Override + public String[] urlPatterns() { + return new String[]{"/*"}; + } + + private static final String[] NOT_FILTER = { + "/decision/file", + "/decision/resources", + "/system", + "/materials.min.js.map", + "/remote", + "/login", + "/login/config", + "/getFineToken" + }; + + private String apiAuthorize; + + private String apiAuthorizeResponseType; + + private String apiClientId; + + private String apiGetUser; + + private String apiRefreshToken; + + private String state; + + public SsoFilter() { + InputStream in = ResourceIOUtils.read("/resources/xplatform.properties"); + Properties properties = new Properties(); + try { + properties.load(in); + } catch (IOException e) { + FineLoggerFactory.getLogger().error(e.getMessage(),e); + } + this.apiAuthorize = properties.getProperty("api.authorize"); + this.apiClientId = properties.getProperty("api.authorize.client_id"); + this.apiAuthorizeResponseType = properties.getProperty("api.authorize.response_type"); + this.apiGetUser = properties.getProperty("api.get-user"); + this.apiRefreshToken = properties.getProperty("api.refresh-token"); + this.state = properties.getProperty("api.authorize.state"); + } + + + @Override + public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) { + String thisUrl = req.getRequestURL().toString(); + FineLoggerFactory.getLogger().info("This request [{}] login status is [{}]", thisUrl, isLogin(req)); + if (req.getRequestURI().endsWith("auth")) { + executeGetAuth(req, res); + return; + } + String code = req.getParameter("sign"); + if (StringUtils.isNotBlank(code)) { + loginFromCode(code, req, res); + filter(req, res, filterChain); + return; + } + if (Stream.of(NOT_FILTER).anyMatch(thisUrl::contains) || isLogin(req) || isMobileDevice(req)) { + filter(req, res, filterChain); + return; + } + String prefix = null; + try { + prefix = TemplateUtils.render("${fineServletURL}"); + if (req.getRequestURI().endsWith(prefix + "/login")) { + if (handlerWeChat(req, res)) { + return; + } + filter(req, res, filterChain); + return; + } + + }catch (IllegalAccessException e){ + redirectAuth(req, res); + return; + }catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + Params params = null; + try { + params = getUsername(req); + } catch (IllegalAccessException e) { + redirectAuth(req, res); + return; + } + FineLoggerFactory.getLogger().info("Getted username is [{}]", params.getUsername()); + if (StringUtils.isNotBlank(params.getUsername())) { + Cookie at = new Cookie("access_token", params.getAccessToken()); + at.setDomain("xxxx"); + at.setMaxAge(7200); + Cookie rt = new Cookie("refresh_token", params.getRefreshToken()); + rt.setDomain("xxxx"); + rt.setMaxAge(7200); + res.addCookie(at); + res.addCookie(rt); + loginFromToken(req, res, params.getUsername()); + try { + res.sendRedirect(getRedirectUriWithCachedParams(req)); + } catch (IOException e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return; + } + redirectAuth(req, res); + } + + private void redirectAuth(HttpServletRequest req, HttpServletResponse res) { + try { + res.sendRedirect(getAuthorizeUrl(req,res)); + } catch (IOException e) { + FineLoggerFactory.getLogger().error(e.getMessage(),e); + } + } + + private boolean handlerWeChat(HttpServletRequest request, HttpServletResponse res) throws IOException, IllegalAccessException { + // 有帆软的登录信息 + if (isLogin(request)) { + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> 已登录"); + String token = getAccessToken(request); + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> token value is [{}]", token); + // 没有获取到access_token + if (StringUtils.isBlank(token)) { + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> token value is empty, 不做处理"); + return false; + } + // 获取到了access_token, 比对帆软已登录的用户名和access_token对应的用户名 + String username = getUsername(request, token).getUsername(); + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> token 对应的用户名[{}]", username); + String username1 = LoginService.getInstance().getUserNameFromRequestCookie(request); + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> 已登录的用户名[{}]", username1); + if (Objects.equals(username1, username)) { + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> 两个用户名相同, 不做处理"); + return false; + } + // 不一样的话使用access_token对应的用户名重新登录 + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> 两个用户名不相同, 使用[{}]重新登录", username); + loginFromToken(request, res, username); + return false; + } + + // 没有登录信息 + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> 未登录"); + String token = getAccessToken(request); + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> token value is [{}]", token); + if (StringUtils.isNotBlank(token)) { + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> token value is not empty, 处理自动登录逻辑"); + Params params = getUsername(request, token); + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> 获取到的用户名[{}]", params.getUsername()); + if (StringUtils.isNotBlank(params.getUsername()) && exist(params.getUsername())) { + Cookie at = new Cookie("access_token", params.getAccessToken()); + at.setDomain("yst.com.cn"); + Cookie rt = new Cookie("refresh_token", params.getRefreshToken()); + rt.setDomain("yst.com.cn"); + loginFromToken(request, res, params.getUsername()); + res.sendRedirect(getRedirectUriWithCachedParams(request)); + return true; + } + } + FineLoggerFactory.getLogger().info("/decision请求特殊处理 >>> token value is empty, 跳转到sso登录页"); + res.sendRedirect(getAuthorizeUrl(request, res)); + return true; + } + + private String getAuthorizeUrl(HttpServletRequest request, HttpServletResponse res) throws UnsupportedEncodingException { + String urlPattern = "%s?response_type=%s&client_id=%s&redirect_uri=%s&_=%s&state=%s"; + String state = cacheParams(request); + res.addCookie(new Cookie("FINE_REDIRECT_PARAM",state)); + if (StringUtils.isNotBlank(state)) { + urlPattern += "&target=" + state; + } + String url = String.format(urlPattern, + apiAuthorize, + apiAuthorizeResponseType, + apiClientId, + URLEncoder.encode(request.getRequestURL().toString(), "utf-8"), + Instant.now().toEpochMilli(), + this.state + ); + FineLoggerFactory.getLogger().info("授权登录页面[{}]", url); + return url; + } + + private String cacheParams(HttpServletRequest request) { + Enumeration names = request.getParameterNames(); + Map value = new HashMap<>(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + value.put(name, request.getParameter(name)); + } + if (value.isEmpty()) { + return StringUtils.EMPTY; + } + String key = UUID.randomUUID().toString(); + try { + DecisionStatusService.originUrlStatusService().put(key, value); + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return key; + } + + /** + * 登录成功后将缓存中存储的url参数拼接入对应地址 + * + * @param request + * @return + */ + + + private String getRedirectUriWithCachedParams(HttpServletRequest request) { + String redirectUri = request.getRequestURL().toString(); + String key = request.getParameter("target"); + if(StringUtils.isBlank(key)){ + key = getCookieValue(request, "FINE_REDIRECT_PARAM"); + } + if(StringUtils.isBlank(key)){ + return redirectUri; + } + Map value = new HashMap<>(); + try { + value = DecisionStatusService.originUrlStatusService().get(key); + if (com.fr.stable.StringUtils.isNotBlank(key) && !value.isEmpty()) { + DecisionStatusService.originUrlStatusService().delete(key); + Map finalValue = value; + redirectUri += "?" + value.keySet().stream().map(k -> String.format("%s=%s", k, finalValue.get(k))).collect(Collectors.joining("&")); + } + } catch (Exception e) { + e.printStackTrace(); + } + return redirectUri; + } + + + private boolean exist(String username) { + User user = null; + try { + user = UserService.getInstance().getUserByUserName(username); + } catch (Exception e) { + e.printStackTrace(); + } + return user != null; + } + + private Params getUsername(HttpServletRequest request) throws IllegalAccessException { + String token = getAccessToken(request); + if(StringUtils.isBlank(token)){ + FineLoggerFactory.getLogger().info("token is null"); + return new Params(); + } + return getUsername(request, token); + } + + private Params getUsername(HttpServletRequest request, String accessToken) throws IllegalAccessException { + String url = apiGetUser + "?access_token=" + accessToken; + FineLoggerFactory.getLogger().info("Get user api address is [{}]", url); + try { + String res = HttpToolbox.executeAndParse(HttpRequest.custom().url(url) + .get() + .build()); + FineLoggerFactory.getLogger().info("获取用户信息接口返回内容 ==> {}", res); + JSONObject body = new JSONObject(res); + if (body.getBoolean("success") && body.has("data")) { + body = body.getJSONObject("data"); + if (body.has("account")) { + Params params = new Params(); + params.setRefreshToken(getRefreshToken(request)); + params.setUsername(body.getString("account")); + params.setAccessToken(accessToken); + return params; + } + } + if (body.has("message") && "token不合法".equals(body.getString("message"))) { + FineLoggerFactory.getLogger().info("Access Token [{}] 不合法, 使用Refresh Token[{}]重新获取", accessToken, getRefreshToken(request)); + accessToken = getAccessTokenWithRefreshToken(request); + return getUsername(request, accessToken); + } + throw new IllegalAccessException(); + }catch (IllegalAccessException e){ + throw new IllegalAccessException(); + }catch (Exception e) { + FineLoggerFactory.getLogger().error("获取用户名失败", e); + throw new RuntimeException(e); + } + } + + private String getAccessTokenWithRefreshToken(HttpServletRequest request) throws Exception { + String url = apiRefreshToken + String.format("?refresh_token=%s&client_id=%s&grant_type=refresh_token", getRefreshToken(request), apiClientId); + FineLoggerFactory.getLogger().info("Refresh Token api address is [{}]", url); + String res = HttpToolbox.executeAndParse(HttpRequest.custom().url(url) + .get() + .build()); + FineLoggerFactory.getLogger().info("刷新token接口返回内容 ==> {}", res); + JSONObject body = new JSONObject(res); + if (body.getBoolean("success") && body.has("data")) { + body = body.getJSONObject("data"); + if (body.has("access_token")) { + return body.getString("access_token"); + } + } + throw new IllegalAccessException(); + } + + private String getRefreshToken(HttpServletRequest request) { + return getValueFromRequest(request, "refresh_token"); + } + + private String getAccessToken(HttpServletRequest request) { + return getValueFromRequest(request, "access_token"); + } + + private String getValueFromRequest(HttpServletRequest request, String key) { + try { + String value = request.getParameter(key); + if (StringUtils.isEmpty(value)) { + value = getCookieValue(request, key); + } + if (StringUtils.isEmpty(value)) { + String params = request.getParameter("app"); + params = params.substring(params.indexOf("#") + 1); + Map values = Pattern.compile("&").splitAsStream(params).map(e -> e.split("=")).collect(Collectors.toMap(e -> e[0], e -> e[1])); + value = values.get(key); + } + return value; + } catch (Exception e) { + FineLoggerFactory.getLogger().error("Failed to get \"{}\" value from this request, cause by: ", key, e.getMessage()); + FineLoggerFactory.getLogger().error("", e); + } + return ""; + } + + private String getCookieValue(HttpServletRequest request, String key) { + Cookie[] cookies = request.getCookies(); + for (Cookie c : cookies) { + if (StringUtils.equals(c.getName(), key)) { + return c.getValue(); + } + } + return StringUtils.EMPTY; + } + + private void loginFromCode(String code, HttpServletRequest req, HttpServletResponse res) { + FineLoggerFactory.getLogger().info("ssoFilter >>> inside login code param is {}", code); + try { + code = DesECBUtil.decryptDES(code, CODE_KEY); + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + FineLoggerFactory.getLogger().info("ssoFilter >>> inside login code decode is {}", code); + String[] arr = code.split("_"); + loginFromToken(req, res, arr[0]); + } + + private void executeGetAuth(HttpServletRequest req, HttpServletResponse res) { + String uri = WebUtils.getHTTPRequestParameter(req, "redirect_uri"); + String currentUsername = null; + try { + User user= null; + try { + user = UserService.getInstance().getUserByRequest(req); + }catch (Exception e){ + + } + if(user == null){ + user = UserService.getInstance().getUserByRequestCookie(req); + } + currentUsername = user.getUserName(); + String encryptDES = DesECBUtil.encryptDES(String.format("%s_%d", currentUsername, Instant.now().toEpochMilli()), CODE_KEY); + encryptDES = URLEncoder.encode(encryptDES, "UTF-8"); + uri = uri.indexOf("?") == -1 ? uri + "?sign=" + encryptDES : uri + "&sign=" + encryptDES; + res.sendRedirect(uri); + FineLoggerFactory.getLogger().info("ssoFilter >>> inside redirect url is {}", uri); + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + } + + private boolean notExistUser(String username) { + User user = null; + try { + user = UserService.getInstance().getUserByUserName(username); + if (user == null) { + return true; + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return false; + } + + private boolean hasCookie(HttpServletRequest req) { + if (req.getCookies() == null) { + return false; + } + return Stream.of(req.getCookies()).anyMatch(e -> "IAM_SESSION".equalsIgnoreCase(e.getName())); + } + + public LoginRequestInfoBean getLoginInfo(HttpServletRequest req) { + try { + BufferedReader br = req.getReader(); + String str = ""; + String listString = ""; + while ((str = br.readLine()) != null) { + listString += str; + } + JSONObject jsonObject = new JSONObject(listString); + LoginRequestInfoBean info = jsonObject.mapTo(LoginRequestInfoBean.class); + //info.setPassword(TransmissionTool.decrypt(info.isEncrypted(),info.getPassword())); + return info; + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return null; + + } + + private boolean loginFromToken(HttpServletRequest req, HttpServletResponse res, String username) { + try { + if (StringUtils.isNotEmpty(username)) { + FineLoggerFactory.getLogger().info("current username:" + username); + User user = UserService.getInstance().getUserByUserName(username); + FineLoggerFactory.getLogger().info("get user:" + user); + if (user == null) { + throw new UserNotExistException(); + } + String token = LoginService.getInstance().login(req, res, username); + FineLoggerFactory.getLogger().info("get login token:" + token); + req.setAttribute(DecisionServiceConstants.FINE_AUTH_TOKEN_NAME, token); + FineLoggerFactory.getLogger().info("username:" + username + "login success"); + return true; + } else { + FineLoggerFactory.getLogger().warn("username is null!"); + return false; + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return false; + } + + private void filter(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) { + try { + filterChain.doFilter(req, res); + } catch (IOException e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } catch (ServletException e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + } + + private boolean isLogin(HttpServletRequest request) { + String oldToken = TokenResource.COOKIE.getToken(request); + return oldToken != null && checkTokenValid(request, (String) oldToken); + } + + private boolean checkTokenValid(HttpServletRequest req, String token) { + try { + Device device = NetworkHelper.getDevice(req); + LoginService.getInstance().loginStatusValid(token, TerminalHandler.getTerminal(req, device)); + return true; + } catch (Exception ignore) { + } + return false; + } + + public boolean isMobileDevice(HttpServletRequest request) { + String requestHeader = request.getHeader("user-agent"); + String[] deviceArray = new String[]{"android", "iphone", "ios", "windows phone"}; + if (requestHeader == null) { + return false; + } + requestHeader = requestHeader.toLowerCase(); + for (int i = 0; i < deviceArray.length; i++) { + if (requestHeader.contains(deviceArray[i]) && request.getRequestURI().endsWith("/login")) { + FineLoggerFactory.getLogger().info("current request:{} is mobile request!", request.getRequestURI()); + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/fr/plugin/nfsq/sso/SsoHttpHandler.java b/src/main/java/com/fr/plugin/nfsq/sso/SsoHttpHandler.java new file mode 100644 index 0000000..4ee2691 --- /dev/null +++ b/src/main/java/com/fr/plugin/nfsq/sso/SsoHttpHandler.java @@ -0,0 +1,103 @@ +package com.fr.plugin.nfsq.sso; + +import com.fr.decision.authority.data.User; +import com.fr.decision.fun.impl.BaseHttpHandler; +import com.fr.decision.webservice.v10.login.LoginService; +import com.fr.decision.webservice.v10.user.UserService; +import com.fr.general.PropertiesUtils; +import com.fr.json.JSONObject; +import com.fr.log.FineLoggerFactory; +import com.fr.record.analyzer.EnableMetrics; +import com.fr.stable.StringUtils; +import com.fr.third.springframework.web.bind.annotation.RequestMethod; +import com.fr.web.utils.WebUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author fr.open + * @since 2020/08/28 + */ +@EnableMetrics +public class SsoHttpHandler extends BaseHttpHandler { + + private String apiUser = ""; + + public SsoHttpHandler() { + apiUser = PropertiesUtils.getProperties("xplatform").getProperty("api.get-user"); + } + + @Override + public RequestMethod getMethod() { + return RequestMethod.GET; + } + + @Override + public String getPath() { + return "/getFineToken"; + } + + @Override + public boolean isPublic() { + return true; + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (StringUtils.isBlank(apiUser)) { + sendError(response, "apiUser config is null"); + return; + } + String token = request.getParameter("access_token"); + if (StringUtils.isBlank(token)) { + sendError(response, "token is null"); + return; + } + String userName = getUsername(token); + if (StringUtils.isBlank(userName)) { + sendError(response, "get user is null"); + return; + } + User user = UserService.getInstance().getUserByUserName(userName); + FineLoggerFactory.getLogger().info("get user:" + user); + if (user == null) { + sendError(response, "user not exist"); + } + String fineToken = LoginService.getInstance().login(request, response, userName); + JSONObject jsonObject = new JSONObject("{\"codeDesc\":\"success\",\"success\":true,\"codeNum\":0}"); + jsonObject.put("value", JSONObject.create().put("fine_oath_token", fineToken)); + response.setContentType("application/json;charset=UTF-8"); + WebUtils.printAsJSON(response, jsonObject); + } + + private String getUsername(String accessToken) { + String url = apiUser + "?access_token=" + accessToken; + FineLoggerFactory.getLogger().info("Get user api address is [{}]", url); + try { + String res = HttpUtil.sendGet(url, null, null, null); + FineLoggerFactory.getLogger().info("获取用户信息接口返回内容 ==> {}", res); + JSONObject body = new JSONObject(res); + if (body.getBoolean("success") && body.has("data")) { + body = body.getJSONObject("data"); + if (body.has("account")) { + return body.getString("account"); + } + } + throw new IllegalAccessException(); + } catch (Exception e) { + FineLoggerFactory.getLogger().error("获取用户名失败", e); + throw new RuntimeException(e); + } + } + + protected void sendError(HttpServletResponse response, String errorCode) { + JSONObject jsonObject = new JSONObject("{\"codeDesc\":\"" + errorCode + "\",\"success\":false,\"codeNum\":70}"); + try { + response.setContentType("application/json;charset=UTF-8"); + WebUtils.printAsJSON(response, jsonObject); + } catch (Exception e) { + FineLoggerFactory.getLogger().error("输出响应错误失败", e); + } + } +} diff --git a/src/main/java/com/fr/plugin/nfsq/sso/SsoRequestHandlerBridge.java b/src/main/java/com/fr/plugin/nfsq/sso/SsoRequestHandlerBridge.java new file mode 100644 index 0000000..2173b60 --- /dev/null +++ b/src/main/java/com/fr/plugin/nfsq/sso/SsoRequestHandlerBridge.java @@ -0,0 +1,19 @@ +package com.fr.plugin.nfsq.sso; + +import com.fr.decision.fun.HttpHandler; +import com.fr.decision.fun.impl.AbstractHttpHandlerProvider; +import com.fr.plugin.transform.FunctionRecorder; + +/** + * @author fr.open + * @since 2020/08/28 + */ +@FunctionRecorder +public class SsoRequestHandlerBridge extends AbstractHttpHandlerProvider { + @Override + public HttpHandler[] registerHandlers() { + return new HttpHandler[]{ + new SsoHttpHandler(), + }; + } +} diff --git a/src/main/java/com/fr/plugin/nfsq/sso/SsoRequestURLAliasBridge.java b/src/main/java/com/fr/plugin/nfsq/sso/SsoRequestURLAliasBridge.java new file mode 100644 index 0000000..78b9b71 --- /dev/null +++ b/src/main/java/com/fr/plugin/nfsq/sso/SsoRequestURLAliasBridge.java @@ -0,0 +1,18 @@ +package com.fr.plugin.nfsq.sso; + +import com.fr.decision.fun.impl.AbstractURLAliasProvider; +import com.fr.decision.webservice.url.alias.URLAlias; +import com.fr.decision.webservice.url.alias.URLAliasFactory; + +/** + * @author fr.open + * @since 2020/08/28 + */ +public class SsoRequestURLAliasBridge extends AbstractURLAliasProvider { + @Override + public URLAlias[] registerAlias() { + return new URLAlias[]{ + URLAliasFactory.createPluginAlias("/getFineToken", "/getFineToken", true), + }; + } +}