集成ip2region

## 集成ip2region实现离线IP地址定位 离线IP地址定位库主要用于内网或想减少对外访问http带来的资源消耗。(代码已兼容支持jar包部署) 1、引入依赖 ```xml <!-- 离线IP地址定位库 --> <dependency> <groupId>org.lionsoul</groupId> <artifactId>ip2region</artifactId> <version>1.7.2</version> </dependency> ``` 2、添加工具类RegionUtil.java ```java package com.ruoyi.common.utils; import java.io.File; import java.io.InputStream; import java.lang.reflect.Method; import org.apache.commons.io.FileUtils; import org.lionsoul.ip2region.DataBlock; import org.lionsoul.ip2region.DbConfig; import org.lionsoul.ip2region.DbSearcher; import org.lionsoul.ip2region.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; /** * 根据ip离线查询地址 * * @author ruoyi */ public class RegionUtil { private static final Logger log = LoggerFactory.getLogger(RegionUtil.class); private static final String JAVA_TEMP_DIR = "java.io.tmpdir"; static DbConfig config = null; static DbSearcher searcher = null; /** * 初始化IP库 */ static { try { // 因为jar无法读取文件,复制创建临时文件 String dbPath = RegionUtil.class.getResource("/ip2region/ip2region.db").getPath(); File file = new File(dbPath); if (!file.exists()) { String tmpDir = System.getProperties().getProperty(JAVA_TEMP_DIR); dbPath = tmpDir + "ip2region.db"; file = new File(dbPath); ClassPathResource cpr = new ClassPathResource("ip2region" + File.separator + "ip2region.db"); InputStream resourceAsStream = cpr.getInputStream(); if (resourceAsStream != null) { FileUtils.copyInputStreamToFile(resourceAsStream, file); } } config = new DbConfig(); searcher = new DbSearcher(config, dbPath); log.info("bean [{}]", config); log.info("bean [{}]", searcher); } catch (Exception e) { log.error("init ip region error:{}", e); } } /** * 解析IP * * @param ip * @return */ public static String getRegion(String ip) { try { // db if (searcher == null || StringUtils.isEmpty(ip)) { log.error("DbSearcher is null"); return StringUtils.EMPTY; } long startTime = System.currentTimeMillis(); // 查询算法 int algorithm = DbSearcher.MEMORY_ALGORITYM; Method method = null; switch (algorithm) { case DbSearcher.BTREE_ALGORITHM: method = searcher.getClass().getMethod("btreeSearch", String.class); break; case DbSearcher.BINARY_ALGORITHM: method = searcher.getClass().getMethod("binarySearch", String.class); break; case DbSearcher.MEMORY_ALGORITYM: method = searcher.getClass().getMethod("memorySearch", String.class); break; } DataBlock dataBlock = null; if (Util.isIpAddress(ip) == false) { log.warn("warning: Invalid ip address"); } dataBlock = (DataBlock) method.invoke(searcher, ip); String result = dataBlock.getRegion(); long endTime = System.currentTimeMillis(); log.debug("region use time[{}] result[{}]", endTime - startTime, result); return result; } catch (Exception e) { log.error("error:{}", e); } return StringUtils.EMPTY; } } ``` 3、修改AddressUtils.java ```java package com.ruoyi.common.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ruoyi.common.config.RuoYiConfig; /** * 获取地址类 * * @author ruoyi */ public class AddressUtils { private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); // 未知地址 public static final String UNKNOWN = "XX XX"; public static String getRealAddressByIP(String ip) { String address = UNKNOWN; // 内网不查询 if (IpUtils.internalIp(ip)) { return "内网IP"; } if (RuoYiConfig.isAddressEnabled()) { try { String rspStr = RegionUtil.getRegion(ip); if (StringUtils.isEmpty(rspStr)) { log.error("获取地理位置异常 {}", ip); return UNKNOWN; } String[] obj = rspStr.split("\\|"); String region = obj[2]; String city = obj[3]; return String.format("%s %s", region, city); } catch (Exception e) { log.error("获取地理位置异常 {}", e); } } return address; } } ``` 4、添加离线IP地址库插件 下载前端插件相关包和代码实现ruoyi/集成ip2region离线地址定位.zip 链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt 5、添加离线IP地址库 在src/main/resources下新建ip2region复制文件ip2region.db到目录下。 ## 集成jsencrypt实现密码加密传输方式 目前登录接口密码是明文传输,如果安全性有要求,可以调整成加密方式传输。参考如下 1、修改前端login.js对密码进行rsa加密。 ```js // 密钥对生成 http://web.chacuo.net/netrsakeypair const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' // 加密 function encrypt(txt) { const encryptor = new JSEncrypt() encryptor.setPublicKey(publicKey) // 设置公钥 return encryptor.encrypt(txt) // 对数据进行加密 } $(function() { validateKickout(); validateRule(); $('.imgcode').click(function() { var url = ctx + "captcha/captchaImage?type=" + captchaType + "&s=" + Math.random(); $(".imgcode").attr("src", url); }); }); $.validator.setDefaults({ submitHandler: function() { login(); } }); function login() { $.modal.loading($("#btnSubmit").data("loading")); var username = $.common.trim($("input[name='username']").val()); var password = $.common.trim($("input[name='password']").val()); var validateCode = $("input[name='validateCode']").val(); var rememberMe = $("input[name='rememberme']").is(':checked'); $.ajax({ type: "post", url: ctx + "login", data: { "username": username, "password": encrypt(password), "validateCode": validateCode, "rememberMe": rememberMe }, success: function(r) { if (r.code == web_status.SUCCESS) { location.href = ctx + 'index'; } else { $.modal.closeLoading(); $('.imgcode').click(); $(".code").val(""); $.modal.msg(r.msg); } } }); } function validateRule() { var icon = "<i class='fa fa-times-circle'></i> "; $("#signupForm").validate({ rules: { username: { required: true }, password: { required: true } }, messages: { username: { required: icon + "请输入您的用户名", }, password: { required: icon + "请输入您的密码", } } }) } function validateKickout() { if (getParam("kickout") == 1) { layer.alert("<font color='red'>您已在别处登录,请您修改密码或重新登录</font>", { icon: 0, title: "系统提示" }, function(index) { //关闭弹窗 layer.close(index); if (top != self) { top.location = self.location; } else { var url = location.search; if (url) { var oldUrl = window.location.href; var newUrl = oldUrl.substring(0, oldUrl.indexOf('?')); self.location = newUrl; } } }); } } function getParam(paramName) { var reg = new RegExp("(^|&)" + paramName + "=([^&]*)(&|$)"); var r = window.location.search.substr(1).match(reg); if (r != null) return decodeURI(r[2]); return null; } ``` 2、修改login.html文件,引入jsencrypt插件 ```js <script src="../static/js/jsencrypt.min.js" th:src="@{/js/jsencrypt.min.js}"></script> ``` 3、工具类security包下添加RsaUtils.java,用于RSA加密解密。 ```java package com.ruoyi.common.utils.security; import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; import java.security.*; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; /** * RSA加密解密 * * @author ruoyi **/ public class RsaUtils { // Rsa 私钥 public static String privateKey = "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY" + "7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN" + "PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA" + "kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow" + "cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv" + "DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh" + "YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3" + "UP8iWi1Qw0Y="; /** * 私钥解密 * * @param privateKeyString 私钥 * @param text 待解密的文本 * @return 解密后的文本 */ public static String decryptByPrivateKey(String text) throws Exception { return decryptByPrivateKey(privateKey, text); } /** * 公钥解密 * * @param publicKeyString 公钥 * @param text 待解密的信息 * @return 解密后的文本 */ public static String decryptByPublicKey(String publicKeyString, String text) throws Exception { X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, publicKey); byte[] result = cipher.doFinal(Base64.decodeBase64(text)); return new String(result); } /** * 私钥加密 * * @param privateKeyString 私钥 * @param text 待加密的信息 * @return 加密后的文本 */ public static String encryptByPrivateKey(String privateKeyString, String text) throws Exception { PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, privateKey); byte[] result = cipher.doFinal(text.getBytes()); return Base64.encodeBase64String(result); } /** * 私钥解密 * * @param privateKeyString 私钥 * @param text 待解密的文本 * @return 解密后的文本 */ public static String decryptByPrivateKey(String privateKeyString, String text) throws Exception { PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] result = cipher.doFinal(Base64.decodeBase64(text)); return new String(result); } /** * 公钥加密 * * @param publicKeyString 公钥 * @param text 待加密的文本 * @return 加密后的文本 */ public static String encryptByPublicKey(String publicKeyString, String text) throws Exception { X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] result = cipher.doFinal(text.getBytes()); return Base64.encodeBase64String(result); } /** * 构建RSA密钥对 * * @return 生成后的公私钥信息 */ public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.generateKeyPair(); RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded()); String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded()); return new RsaKeyPair(publicKeyString, privateKeyString); } /** * RSA密钥对对象 */ public static class RsaKeyPair { private final String publicKey; private final String privateKey; public RsaKeyPair(String publicKey, String privateKey) { this.publicKey = publicKey; this.privateKey = privateKey; } public String getPublicKey() { return publicKey; } public String getPrivateKey() { return privateKey; } } } ``` 4、登录方法SysLoginController.java,对密码进行rsa解密。 ```java @Controller public class SysLoginController extends BaseController { @PostMapping("/login") @ResponseBody public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe) { try { UsernamePasswordToken token = new UsernamePasswordToken(username, RsaUtils.decryptByPrivateKey(password), rememberMe); Subject subject = SecurityUtils.getSubject(); subject.login(token); return success(); } catch (Exception e) { String msg = "用户或密码错误"; if (StringUtils.isNotEmpty(e.getMessage())) { msg = e.getMessage(); } return error(msg); } } } ``` 4、测试访问验证 访问 http://localhost/login 登录页面。提交时检查密码是否为加密传输,且后台也能正常解密。 下载前端插件相关包和代码实现ruoyi/集成jsencrypt实现密码加密传输方式.zip 链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt ## 集成druid实现数据库密码加密功能 数据库密码直接写在配置中,对运维安全来说,是一个很大的挑战。可以使用Druid为此提供一种数据库密码加密的手段ConfigFilter。项目已经集成druid所以只需按要求配置即可。 1、执行命令加密数据库密码 ```sh java -cp druid-1.2.4.jar com.alibaba.druid.filter.config.ConfigTools password ``` password输入你的数据库密码,输出的是加密后的结果。 ``` privateKey:MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAuLMVAFmcew+mPfVnzI6utEvhHWO2s6e4R1bVW3a9IpH+pEypeNV6KtZ/w9PuysPfdPxW5fN3BmnKFZUAIMvWhQIDAQABAkA6rnsfr1juKFyzFsMx1KthETKmucWUctczoz0KYEFbN+joNsd/ApQqsS/2MVG1QWbDJLUsSLWkchvRbtiqOlVJAiEA6KmgVeLR2qUU9gv6DJfuWk4Ol1M9GJnTamgyDttsSGcCIQDLOdjcht29s954vApG1fiPTP/kMvZ5aLrccw1lEuEGMwIhAKoe3c3u++MTsi/2se9jaDU/vguIIbRLRfsYFQIoDxUhAiAnCm/cvZPvk5RTgVxAC276qIIoJpou7K2pF/kkx6Gu/QIgKUVFiM8GVZkOWZC+nUm3UIfpGjrKXjvGrlHNvt89uBA= publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ== password:gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA== ``` 2、配置数据源,提示Druid数据源需要对数据库密码进行解密。 ``` # 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # 主库数据源 master: url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA== # 从库数据源 slave: # 从数据源开关/默认关闭 enabled: false url: username: password: # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false connectProperties: config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ== webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /druid/* # 控制台管理用户名和密码 login-username: login-password: filter: config: # 是否配置加密 enabled: true stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true ``` 3、DruidProperties配置connectProperties属性 ```java package com.ruoyi.framework.config.properties; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import com.alibaba.druid.pool.DruidDataSource; /** * druid 配置属性 * * @author ruoyi */ @Configuration public class DruidProperties { @Value("${spring.datasource.druid.initialSize}") private int initialSize; @Value("${spring.datasource.druid.minIdle}") private int minIdle; @Value("${spring.datasource.druid.maxActive}") private int maxActive; @Value("${spring.datasource.druid.maxWait}") private int maxWait; @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") private int timeBetweenEvictionRunsMillis; @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") private int minEvictableIdleTimeMillis; @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") private int maxEvictableIdleTimeMillis; @Value("${spring.datasource.druid.validationQuery}") private String validationQuery; @Value("${spring.datasource.druid.testWhileIdle}") private boolean testWhileIdle; @Value("${spring.datasource.druid.testOnBorrow}") private boolean testOnBorrow; @Value("${spring.datasource.druid.testOnReturn}") private boolean testOnReturn; @Value("${spring.datasource.druid.connectProperties}") private String connectProperties; public DruidDataSource dataSource(DruidDataSource datasource) { /** 配置初始化大小、最小、最大 */ datasource.setInitialSize(initialSize); datasource.setMaxActive(maxActive); datasource.setMinIdle(minIdle); /** 配置获取连接等待超时的时间 */ datasource.setMaxWait(maxWait); /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); /** * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 */ datasource.setValidationQuery(validationQuery); /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ datasource.setTestWhileIdle(testWhileIdle); /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnBorrow(testOnBorrow); /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnReturn(testOnReturn); /** 为数据库密码提供加密功能 */ datasource.setConnectionProperties(connectProperties); return datasource; } } ``` 4、启动应用程序测试验证加密结果 > 提示 > 如若忘记密码可以使用工具类解密(传入生成的公钥+密码) ```java public static void main(String[] args) throws Exception { String password = ConfigTools.decrypt( "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ==", "gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA=="); System.out.println("解密密码:" + password); } ```