diff --git a/JSD-8928-需求文档V1.docx b/JSD-8928-需求文档V1.docx new file mode 100644 index 0000000..27277a0 Binary files /dev/null and b/JSD-8928-需求文档V1.docx differ diff --git a/README.md b/README.md index 9514bdb..05b8526 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # open-JSD-8928 -JSD-8928 ADFS( saml模式 )单点 \ No newline at end of file +JSD-8928 ADFS( saml模式 )单点\ +免责说明:该源码为第三方爱好者提供,不保证源码和方案的可靠性,也不提供任何形式的源码教学指导和协助!\ +仅作为开发者学习参考使用!禁止用于任何商业用途!\ +为保护开发者隐私,开发者信息已隐去!若原开发者希望公开自己的信息,可联系hugh处理。 \ No newline at end of file diff --git a/doc/JSD-8928配置使用文档.docx b/doc/JSD-8928配置使用文档.docx new file mode 100644 index 0000000..7b9ae36 Binary files /dev/null and b/doc/JSD-8928配置使用文档.docx differ diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..9f2c91a --- /dev/null +++ b/plugin.xml @@ -0,0 +1,21 @@ + + + com.fr.plugin.xxxx.tpyy.adfs + + yes + 1.2 + 10.0 + 2019-03-10 + fr.open + + + [2021-11-10]【1.2】增加授权
+ [2021-11-08]【1.1】增加授权
+ [2021-10-29]【1.0】初始化插件。
+
+ + + + + +
diff --git a/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/Constants.java b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/Constants.java new file mode 100644 index 0000000..afa0c5e --- /dev/null +++ b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/Constants.java @@ -0,0 +1,9 @@ +package com.fr.plugin.xxxx.tpyy.adfs; + +/** + * @author fr.open + * @date 2020/11/11 + */ +public class Constants { + public static final String PLUGIN_ID = "com.fr.plugin.xxxx.tpyy.adfs"; +} diff --git a/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/CustomLogInOutEventProvider.java b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/CustomLogInOutEventProvider.java new file mode 100644 index 0000000..42cd703 --- /dev/null +++ b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/CustomLogInOutEventProvider.java @@ -0,0 +1,19 @@ +package com.fr.plugin.xxxx.tpyy.adfs; + +import com.fr.decision.fun.impl.AbstractLogInOutEventProvider; +import com.fr.decision.webservice.login.LogInOutResultInfo; +import com.fr.general.PropertiesUtils; +import com.fr.log.FineLoggerFactory; + +/** + * @author fr.open + * @date 2020/11/11 + */ +public class CustomLogInOutEventProvider extends AbstractLogInOutEventProvider { + @Override + public String logoutAction(LogInOutResultInfo result) { + String logout = PropertiesUtils.getProperties("conf").getProperty("logout"); + FineLoggerFactory.getLogger().info("get logout is {}",logout); + return logout; + } +} diff --git a/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/DesECBUtil.java b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/DesECBUtil.java new file mode 100644 index 0000000..5c431cb --- /dev/null +++ b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/DesECBUtil.java @@ -0,0 +1,65 @@ +package com.fr.plugin.xxxx.tpyy.adfs; + +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 2020/11/11 + */ +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/xxxx/tpyy/adfs/KeysUtil.java b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/KeysUtil.java new file mode 100644 index 0000000..3cf01fa --- /dev/null +++ b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/KeysUtil.java @@ -0,0 +1,115 @@ +package com.fr.plugin.xxxx.tpyy.adfs; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import java.io.UnsupportedEncodingException; +import java.security.SecureRandom; + +/** + * @author fr.open + * @date 2020/11/11 + */ +public class KeysUtil { + /** 加密解密KEY */ + + /** 加密解密算法 */ + private final static String ALGORITHM = "DES"; + /** + * 加密 + * @param data 未加密数据 + * @param key 加密key + * @return + * @throws Exception + */ + public static String encrypt(String data, String key) throws Exception { + + byte[] dataBytes = data.getBytes("UTF-8"); + + byte[] keyBytes = key.getBytes("UTF-8"); + + // DES算法要求有一个可信任的随机数源 + SecureRandom sr = new SecureRandom(); + + // 从原始密匙数据创建DESKeySpec对象 + DESKeySpec dks = new DESKeySpec(keyBytes); + + // 创建一个密匙工厂,然后用它把DESKeySpec转换成 + // 一个SecretKey对象 + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM); + SecretKey securekey = keyFactory.generateSecret(dks); + + // Cipher对象实际完成加密操作 + Cipher cipher = Cipher.getInstance(ALGORITHM); + + // 用密匙初始化Cipher对象 + cipher.init(Cipher.ENCRYPT_MODE, securekey, sr); + + // 正式执行加密操作 + byte[] bytes = cipher.doFinal(dataBytes); + return byte2hex(bytes); + } + + /** + * 解密 + * @param data 加密后的数据 + * @param key 加密key + * @return + * @throws Exception + */ + public static String decrypt(String data, String key) throws Exception { + + byte[] dataBytes = data.getBytes("UTF-8"); + byte[] hex2byte = hex2byte(dataBytes); + byte[] keyBytes = key.getBytes("UTF-8"); + + // DES算法要求有一个可信任的随机数源 + SecureRandom sr = new SecureRandom(); + + // 从原始密匙数据创建一个DESKeySpec对象 + DESKeySpec dks = new DESKeySpec(keyBytes); + + // 创建一个密匙工厂,然后用它把DESKeySpec对象转换成 + // 一个SecretKey对象 + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM); + SecretKey securekey = keyFactory.generateSecret(dks); + + // Cipher对象实际完成解密操作 + Cipher cipher = Cipher.getInstance(ALGORITHM); + + // 用密匙初始化Cipher对象 + cipher.init(Cipher.DECRYPT_MODE, securekey, sr); + + // 正式执行解密操作 + byte[] doFinal = cipher.doFinal(hex2byte); + + return new String(doFinal,"UTF-8"); + } + + private static byte[] hex2byte(byte[] b) throws UnsupportedEncodingException { + if ((b.length % 2) != 0) { + throw new IllegalArgumentException("长度不是偶数"); + } + byte[] b2 = new byte[b.length / 2]; + for (int n = 0; n < b.length; n += 2) { + String item = new String(b, n, 2, "UTF-8"); + b2[n / 2] = (byte) Integer.parseInt(item, 16); + } + return b2; + } + + private static String byte2hex(byte[] b) { + StringBuilder bulid = new StringBuilder(); + String stmp = ""; + for (int n = 0; n < b.length; n++) { + stmp = (Integer.toHexString(b[n] & 0XFF)); + if (stmp.length() == 1) { + bulid.append("0").append(stmp); + } else { + bulid.append(stmp); + } + } + return bulid.toString().toUpperCase(); + } +} diff --git a/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/SsoFilter.java b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/SsoFilter.java new file mode 100644 index 0000000..7cac8fb --- /dev/null +++ b/src/main/java/com/fr/plugin/xxxx/tpyy/adfs/SsoFilter.java @@ -0,0 +1,225 @@ +/* + * The contents of this file are subject to the Mozilla Public + * License Version 1.1 (the "License"); you may not use this + * file except in compliance with the License. You may obtain + * a copy of the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an + * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express + * or implied. See the License for the specific language governing + * rights and limitations under the License. + * + * + * The Original Code is OIOSAML Java Service Provider. + * + * The Initial Developer of the Original Code is Trifork A/S. Portions + * created by Trifork A/S are Copyright (C) 2008 Danish National IT + * and Telecom Agency (http://www.itst.dk). All Rights Reserved. + * + * Contributor(s): + * Joakim Recht + * Rolf Njor Jensen + * Aage Nielsen + * + */ +package com.fr.plugin.xxxx.tpyy.adfs; + +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.utils.DecisionServiceConstants; +import com.fr.decision.webservice.utils.WebServiceUtils; +import com.fr.decision.webservice.v10.config.ConfigService; +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.PropertiesUtils; +import com.fr.intelli.record.Focus; +import com.fr.intelli.record.Original; +import com.fr.locale.InterProviderFactory; +import com.fr.log.FineLoggerFactory; +import com.fr.plugin.context.PluginContexts; +import com.fr.plugin.transform.FunctionRecorder; +import com.fr.record.analyzer.EnableMetrics; +import com.fr.stable.StringUtils; +import com.fr.stable.fun.Authorize; +import com.fr.stable.web.Device; +import com.fr.web.utils.WebUtils; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +/** + * @author fr.open + * @date 2020/11/11 + */ +@FunctionRecorder +@Authorize(callSignKey = Constants.PLUGIN_ID) +@EnableMetrics +public class SsoFilter extends AbstractGlobalRequestFilterProvider { + + @Override + public String filterName() { + return "tpyy"; + } + + @Override + @Focus(id = Constants.PLUGIN_ID, text = "adfs", source = Original.PLUGIN) + public String[] urlPatterns() { + if (PluginContexts.currentContext().isAvailable()) { + String servletPathName = "decision"; + try { + servletPathName = ConfigService.getInstance().getBasicParam().getServletPathName(); + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return new String[]{ + "/" + servletPathName, + "/" + servletPathName + "/view/report", + + }; + } else { + return new String[0]; + } + } + + @Override + public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) { + String samlToken = req.getParameter("samlToken"); + try { + if (isAccept(req) || isLogin(req)) { + next(req, res, filterChain); + return; + } + if (StringUtils.isNotBlank(samlToken)) { + String key = PropertiesUtils.getProperties("conf").getProperty("key"); + FineLoggerFactory.getLogger().info("get key config is {}", key); + String format = DesECBUtil.decryptDES(samlToken, key); + FineLoggerFactory.getLogger().info("decode format is {}", format); + String user = format.split("_")[0]; + String timeout = format.split("_")[1]; + String out = PropertiesUtils.getProperties("conf").getProperty("timeout"); + FineLoggerFactory.getLogger().info("get timeout config is {}", out); + if (StringUtils.isBlank(out)) { + out = "10"; + } + if (Instant.ofEpochMilli(Long.valueOf(timeout)).plusSeconds(Long.valueOf(out)).isBefore(Instant.now())) { + setError(res, "format timeout!"); + return; + } + if (!checkUser(user)) { + setError(res, "user: 【" + user + "】 not exist !"); + return; + } + loginFromToken(req, res, user); + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + FineLoggerFactory.getLogger().info("current request {} not login send redirect login", getUrl(req)); + String saml = PropertiesUtils.getProperties("conf").getProperty("saml"); + try { + res.sendRedirect(saml); + } catch (IOException e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + } + + private String getUrl(HttpServletRequest req) { + String requestURL = req.getRequestURL().toString(); + String queryString = req.getQueryString(); + if (StringUtils.isNotBlank(queryString)) { + requestURL += "?" + queryString; + } + return requestURL; + } + + private boolean isLogin(HttpServletRequest request) throws Exception { + 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; + } + + private boolean isAccept(HttpServletRequest req) { + if (req.getRequestURI().endsWith("/view/form") || req.getRequestURI().endsWith("/view/report")) { + return StringUtils.isBlank(WebUtils.getHTTPRequestParameter(req, "viewlet")) && StringUtils.isNotBlank(req.getParameter("samlToken")); + } + return false; + } + + private void next(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { + try { + chain.doFilter(request, response); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private boolean checkUser(String username) { + User user = null; + try { + user = UserService.getInstance().getUserByUserName(username); + FineLoggerFactory.getLogger().info("get user:" + user); + if (user != null) { + return true; + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + + + return false; + } + + private boolean loginFromToken(HttpServletRequest req, HttpServletResponse res, String username) throws Exception { + try { + if (StringUtils.isNotEmpty(username)) { + FineLoggerFactory.getLogger().info("current username:" + username); + 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 setError(HttpServletResponse res, String reason) { + try { + PrintWriter printWriter = WebUtils.createPrintWriter(res); + Map map = new HashMap<>(); + map.put("result", InterProviderFactory.getProvider().getLocText("Fine-Engine_Error_Page_Result")); + map.put("reason", reason); + map.put("solution", InterProviderFactory.getProvider().getLocText("Fine-Engine_Please_Contact_Platform_Admin")); + String page = WebServiceUtils.parseWebPageResourceSafe("com/fr/web/controller/decision/entrance/resources/unavailable.html", map); + printWriter.write(page); + printWriter.flush(); + printWriter.close(); + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + } +} + diff --git a/src/main/resources/conf.properties b/src/main/resources/conf.properties new file mode 100644 index 0000000..6bb035a --- /dev/null +++ b/src/main/resources/conf.properties @@ -0,0 +1,4 @@ +key= +timeout= +saml=https://ip:8075/oiosaml2/saml/login +logout=https://ip:8075/oiosaml2/saml/logout \ No newline at end of file