From 3ec09310474e0598de70f9b4dc24a1eb2b31ff23 Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Thu, 3 Sep 2026 16:23:38 +0800 Subject: [PATCH] feat: add license authorization module --- pom.xml | 7 +- ruoyi-admin/pom.xml | 4 + .../system/license/SysLicenseController.java | 126 ++++++++ .../src/main/resources/application.yml | 4 + .../framework/config/SecurityConfig.java | 76 ++--- ruoyi-license/CHANGES.md | 270 +++++++++++++++++ ruoyi-license/DEPLOYMENT_CHECKLIST.md | 250 ++++++++++++++++ ruoyi-license/INTEGRATION.md | 231 +++++++++++++++ ruoyi-license/LICENSE_FILTER_CONFIG.md | 168 +++++++++++ ruoyi-license/README.md | 193 ++++++++++++ ruoyi-license/SUMMARY.md | 239 +++++++++++++++ ruoyi-license/pom.xml | 45 +++ .../ruoyi/license/config/LicenseConfig.java | 61 ++++ .../com/ruoyi/license/domain/SysLicense.java | 105 +++++++ .../interceptor/LicenseInterceptor.java | 46 +++ .../license/mapper/SysLicenseMapper.java | 67 +++++ .../license/service/ISysLicenseService.java | 87 ++++++ .../service/impl/SysLicenseServiceImpl.java | 267 +++++++++++++++++ .../ruoyi/license/task/LicenseCheckTask.java | 47 +++ .../ruoyi/license/util/LicenseGenerator.java | 127 ++++++++ .../com/ruoyi/license/util/LicenseTest.java | 89 ++++++ .../com/ruoyi/license/util/LicenseUtils.java | 275 ++++++++++++++++++ .../mapper/license/SysLicenseMapper.xml | 108 +++++++ 23 files changed, 2853 insertions(+), 39 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/license/SysLicenseController.java create mode 100644 ruoyi-license/CHANGES.md create mode 100644 ruoyi-license/DEPLOYMENT_CHECKLIST.md create mode 100644 ruoyi-license/INTEGRATION.md create mode 100644 ruoyi-license/LICENSE_FILTER_CONFIG.md create mode 100644 ruoyi-license/README.md create mode 100644 ruoyi-license/SUMMARY.md create mode 100644 ruoyi-license/pom.xml create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/config/LicenseConfig.java create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/domain/SysLicense.java create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/interceptor/LicenseInterceptor.java create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/mapper/SysLicenseMapper.java create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/service/ISysLicenseService.java create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/service/impl/SysLicenseServiceImpl.java create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/task/LicenseCheckTask.java create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseGenerator.java create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseTest.java create mode 100644 ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseUtils.java create mode 100644 ruoyi-license/src/main/resources/mapper/license/SysLicenseMapper.xml diff --git a/pom.xml b/pom.xml index 1e56dd9..bb7f646 100644 --- a/pom.xml +++ b/pom.xml @@ -197,7 +197,11 @@ ruoyi-common ${ruoyi.version} - + + com.ruoyi + ruoyi-license + ${ruoyi.version} + org.springframework.ai spring-ai-bom @@ -251,6 +255,7 @@ ruoyi-generator ruoyi-common fortucky-algorithm + ruoyi-license pom diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 89c520f..61f0a19 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -35,6 +35,10 @@ com.mysql mysql-connector-j + + com.ruoyi + ruoyi-license + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/license/SysLicenseController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/license/SysLicenseController.java new file mode 100644 index 0000000..536d7c7 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/license/SysLicenseController.java @@ -0,0 +1,126 @@ +package com.ruoyi.web.controller.system.license; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.license.domain.SysLicense; +import com.ruoyi.license.service.ISysLicenseService; +import com.ruoyi.license.util.LicenseUtils; + +/** + * 授权码Controller + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/license") +public class SysLicenseController extends BaseController +{ + @Autowired + private ISysLicenseService sysLicenseService; + + /** + * 查询授权码列表 + */ + @PreAuthorize("@ss.hasPermi('system:license:list')") + @GetMapping("/list") + public TableDataInfo list(SysLicense sysLicense) + { + startPage(); + List list = sysLicenseService.selectSysLicenseList(sysLicense); + return getDataTable(list); + } + + /** + * 新增授权码 + */ + @PreAuthorize("@ss.hasPermi('system:license:add')") + @Log(title = "授权码", businessType = BusinessType.INSERT) + @PostMapping("/add") + public AjaxResult add(SysLicense sysLicense) + { + return toAjax(sysLicenseService.insertSysLicense(sysLicense)); + } + + /** + * 修改授权码 + */ + @PreAuthorize("@ss.hasPermi('system:license:edit')") + @Log(title = "授权码", businessType = BusinessType.UPDATE) + @PostMapping("/edit") + public AjaxResult edit(SysLicense sysLicense) + { + return toAjax(sysLicenseService.updateSysLicense(sysLicense)); + } + + /** + * 删除授权码 + */ + @PreAuthorize("@ss.hasPermi('system:license:remove')") + @Log(title = "授权码", businessType = BusinessType.DELETE) + @PostMapping("/remove") + public AjaxResult remove(String ids) + { + return toAjax(sysLicenseService.deleteSysLicenseByIds(ids)); + } + + /** + * 激活授权码(无需登录) + */ + @Log(title = "激活授权码", businessType = BusinessType.UPDATE) + @PostMapping("/active") + public AjaxResult active(String licenseCode) + { + if (!LicenseUtils.validateFormat(licenseCode)) + { + return error("授权码格式不正确"); + } + int result = sysLicenseService.activeLicense(licenseCode); + if (result > 0) + { + return success("授权码激活成功"); + } + return error("授权码激活失败,请检查授权码是否正确或已过期"); + } + + /** + * 获取当前授权信息 + * license 未开启或无有效授权时返回 code=200 data=null,前端静默处理 + */ + @GetMapping("/info") + public AjaxResult info() + { + SysLicense license = sysLicenseService.getActiveLicense(); + return AjaxResult.success(license); + } + + /** + * 生成授权码 + */ + @PreAuthorize("@ss.hasPermi('system:license:add')") + @GetMapping("/generate") + public AjaxResult generate(String type, String projectNo) + { + if (StringUtils.isEmpty(type) || StringUtils.isEmpty(projectNo)) + { + return error("授权类型和项目编号不能为空"); + } + String licenseCode = LicenseUtils.generateLicense(type, projectNo); + if (licenseCode == null) + { + return error("授权码生成失败"); + } + return AjaxResult.success("授权码生成成功", licenseCode); + } +} diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 3ac8dc2..157db3e 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -135,6 +135,10 @@ token: # 令牌有效期(默认30分钟) expireTime: 43800 +# 授权码配置 +license: + # 首次部署需先执行 sql/sys_license.sql 并激活授权码,再改为 true 启用拦截 + enabled: false # MyBatis配置 diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index edd56ef..a3f1bfe 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -58,14 +58,14 @@ public class SecurityConfig @Autowired private PermitAllUrlProperties permitAllUrl; - /** - * 身份验证实现 - */ - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception - { - return authenticationConfiguration.getAuthenticationManager(); - } + /** + * 身份验证实现 + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception + { + return authenticationConfiguration.getAuthenticationManager(); + } /** * anyRequest | 匹配所有请求路径 @@ -86,36 +86,36 @@ public class SecurityConfig protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity - // CSRF禁用,因为不使用session - .csrf(csrf -> csrf.disable()) - // 禁用HTTP响应标头 - .headers((headersCustomizer) -> { - headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin()); - }) - // 认证失败处理类 - .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) - // 基于token,所以不需要session - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - // 注解标记允许匿名访问的url - .authorizeHttpRequests((requests) -> { - permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll()); - // 对于登录login 注册register 验证码captchaImage 允许匿名访问 - requests.requestMatchers("/login", "/register", "/captchaImage").permitAll() - // 静态资源,可匿名访问 - .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll() - .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() - .requestMatchers("/api/**","/api2wms/**","api2erp/**").permitAll() - // 除上面外的所有请求全部需要鉴权认证 - .anyRequest().authenticated(); - }) - // 添加Logout filter - .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) - // 添加JWT filter - .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) - // 添加CORS filter - .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class) - .addFilterBefore(corsFilter, LogoutFilter.class) - .build(); + // CSRF禁用,因为不使用session + .csrf(csrf -> csrf.disable()) + // 禁用HTTP响应标头 + .headers((headersCustomizer) -> { + headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin()); + }) + // 认证失败处理类 + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + // 基于token,所以不需要session + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 注解标记允许匿名访问的url + .authorizeHttpRequests((requests) -> { + permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll()); + // 对于登录login 注册register 验证码captchaImage 允许匿名访问 + requests.requestMatchers("/login", "/register", "/captchaImage", "/system/license/active").permitAll() + // 静态资源,可匿名访问 + .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll() + .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() + .requestMatchers("/api/**","/api2wms/**","api2erp/**").permitAll() + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated(); + }) + // 添加Logout filter + .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) + // 添加JWT filter + .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) + // 添加CORS filter + .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class) + .addFilterBefore(corsFilter, LogoutFilter.class) + .build(); } /** diff --git a/ruoyi-license/CHANGES.md b/ruoyi-license/CHANGES.md new file mode 100644 index 0000000..d6c8609 --- /dev/null +++ b/ruoyi-license/CHANGES.md @@ -0,0 +1,270 @@ +# 授权码模块集成改动说明 + +> 本文档记录引入 `ruoyi-license` 授权码模块时,对原有项目文件的所有改动(不含 `ruoyi-license` 模块本身及 `SysLicenseController`)。 + +--- + +## 1. 根模块 `pom.xml` + +**文件路径:** `wms/pom.xml` + +**改动内容:** + +1. `` 中注册 `ruoyi-license` 子模块: + +```xml + + ruoyi-admin + ruoyi-framework + ruoyi-system + ruoyi-quartz + ruoyi-generator + ruoyi-common + ruoyi-license + +``` + +2. `` 中声明 `ruoyi-license` 依赖版本: + +```xml + + + com.ruoyi + ruoyi-license + ${ruoyi.version} + +``` + +--- + +## 2. `ruoyi-admin/pom.xml` + +**文件路径:** `wms/ruoyi-admin/pom.xml` + +**改动内容:** + +引入 `ruoyi-license` 依赖,使授权拦截器和配置能被 Spring Boot 扫描加载: + +```xml + + + com.ruoyi + ruoyi-license + +``` + +--- + +## 3. `SecurityConfig.java` + +**文件路径:** `wms/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java` + +**改动内容:** + +在 Spring Security 匿名白名单中加入 `/system/license/active`,允许未登录用户调用激活授权码接口: + +```java +// 改动前 +requests.antMatchers("/login", "/register", "/captchaImage").permitAll() + +// 改动后 +requests.antMatchers("/login", "/register", "/captchaImage", "/system/license/active").permitAll() +``` + +--- + +## 4. 前端 `api/license.js`(新建) + +**文件路径:** `wms/ui/src/api/license.js` + +**改动内容:** + +新建文件,封装授权码相关接口: + +```js +import request from '@/utils/request' + +// 激活授权码(无需登录) +export function activeLicense(licenseCode) { + return request({ + url: '/system/license/active', + method: 'post', + params: { licenseCode } + }) +} + +// 查询当前授权信息(需登录) +export function getLicenseInfo() { + return request({ + url: '/system/license/info', + method: 'get' + }) +} +``` + +--- + +## 5. 登录页 `login.vue` + +**文件路径:** `wms/ui/src/views/login.vue` + +**改动内容:** + +### 5.1 模板部分 + +在登录按钮下方新增"激活授权码"按钮,以及激活弹窗: + +```html + + + 激活授权码 + + + + + + + 取 消 + 激 活 + + +``` + +### 5.2 Script 部分 + +新增 import、data 字段和方法: + +```js +// 新增 import +import { activeLicense } from "@/api/license" + +// 新增 data 字段 +licenseDialogVisible: false, +licenseCode: '', +licenseLoading: false + +// 新增方法 +openLicenseDialog() { + this.licenseCode = '' + this.licenseDialogVisible = true +}, +handleActiveLicense() { + if (!this.licenseCode.trim()) { + this.$message.warning('请输入授权码') + return + } + this.licenseLoading = true + activeLicense(this.licenseCode.trim()).then(res => { + if (res.code === 200) { + this.$message.success(res.msg || '授权码激活成功') + this.licenseDialogVisible = false + } else { + this.$message.error(res.msg || '授权码激活失败') + } + }).catch(() => { + this.$message.error('授权码激活失败') + }).finally(() => { + this.licenseLoading = false + }) +} +``` + +--- + +## 6. 顶部导航栏 `Navbar.vue` + +**文件路径:** `wms/ui/src/layout/components/Navbar.vue` + +**改动内容:** + +### 6.1 模板部分 + +在 navbar 中间插入授权到期警告条(剩余不足 30 天时显示): + +```html +
+ + 系统授权将于 {{ licenseExpireText }} 后过期,请及时联系管理员续期 +
+``` + +### 6.2 Script 部分 + +```js +// 新增 import +import { getLicenseInfo } from '@/api/license' + +// 新增 data 字段 +licenseWarning: false, +licenseExpireText: '', +licenseTimer: null + +// 新增生命周期钩子 +mounted() { + this.checkLicenseExpire() + this.licenseTimer = setInterval(() => this.checkLicenseExpire(), 60 * 1000) +}, +beforeDestroy() { + if (this.licenseTimer) clearInterval(this.licenseTimer) +}, + +// 新增方法 +checkLicenseExpire() { + getLicenseInfo().then(res => { + if (res.code === 200 && res.data && res.data.expireTime) { + const expireTime = new Date(res.data.expireTime).getTime() + const now = Date.now() + const diffMs = expireTime - now + const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)) + if (diffMs > 0 && diffDays <= 30) { + this.licenseWarning = true + if (diffMs < 1000 * 60 * 60 * 24) { + const hours = Math.floor(diffMs / (1000 * 60 * 60)) + const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)) + this.licenseExpireText = hours > 0 ? `${hours} 小时 ${minutes} 分钟` : `${minutes} 分钟` + } else if (diffDays < 7) { + this.licenseExpireText = diffDays + ' 天' + } else { + this.licenseExpireText = Math.ceil(diffDays / 7) + ' 周' + } + } + } + }).catch(() => {}) +} +``` + +### 6.3 样式部分 + +```scss +.license-warning { + position: absolute; + left: 50%; + transform: translateX(-50%); + top: 0; + height: 50px; + line-height: 50px; + font-size: 13px; + color: #e6a23c; + font-weight: 500; + pointer-events: none; + i { margin-right: 4px; } +} +``` + +--- + +## 改动文件汇总 + +| 文件 | 类型 | 说明 | +|---|---|---| +| `wms/pom.xml` | 修改 | 注册模块 + 声明依赖版本 | +| `wms/ruoyi-admin/pom.xml` | 修改 | 引入 ruoyi-license 依赖 | +| `ruoyi-framework/.../SecurityConfig.java` | 修改 | 白名单加入激活授权码接口 | +| `ui/src/api/license.js` | 新建 | 封装激活和查询授权信息接口 | +| `ui/src/views/login.vue` | 修改 | 新增激活授权码按钮和弹窗 | +| `ui/src/layout/components/Navbar.vue` | 修改 | 新增授权到期倒计时警告条 | diff --git a/ruoyi-license/DEPLOYMENT_CHECKLIST.md b/ruoyi-license/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..cac7b0e --- /dev/null +++ b/ruoyi-license/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,250 @@ +# 授权码模块部署检查清单 + +## 📋 部署前检查 + +### 1. 文件完整性检查 +- [ ] ruoyi-license 模块目录存在 +- [ ] pom.xml 已添加 license 模块 +- [ ] ruoyi-admin/pom.xml 已添加 license 依赖 +- [ ] SysLicenseController.java 已创建 +- [ ] license.html 和 active.html 已创建 +- [ ] login.html 和 login.js 已修改 +- [ ] sys_license.sql 脚本已准备 + +### 2. 数据库准备 +- [ ] 创建 sys_license 表 +- [ ] 插入授权管理菜单数据 +- [ ] 验证表结构正确 +- [ ] 检查菜单权限配置 + +### 3. 代码配置 +- [ ] 修改 LicenseUtils.java 中的密钥(生产环境必须) +- [ ] 确认拦截器路径配置正确 +- [ ] 确认定时任务cron表达式 +- [ ] 启动类添加 @EnableScheduling 注解 + +## 🔧 部署步骤 + +### 步骤1: 执行数据库脚本 +```bash +mysql -u root -p your_database < sql/sys_license.sql +``` +- [ ] 执行成功 +- [ ] 表创建成功 +- [ ] 菜单数据插入成功 + +### 步骤2: 修改启动类 +```java +@SpringBootApplication +@EnableScheduling // 添加此注解 +public class RuoYiApplication { + // ... +} +``` +- [ ] 注解已添加 + +### 步骤3: 编译项目 +```bash +mvn clean install +``` +- [ ] 编译成功 +- [ ] 无错误信息 +- [ ] JAR包生成 + +### 步骤4: 启动应用 +```bash +java -jar ruoyi-admin.jar +``` +- [ ] 启动成功 +- [ ] 无异常日志 +- [ ] 端口正常监听 + +### 步骤5: 生成授权码 +**方式A: 使用独立工具** +```bash +java com.ruoyi.license.util.LicenseGenerator +``` +- [ ] 工具运行成功 +- [ ] 授权码生成成功 +- [ ] 授权码已保存 + +**方式B: 使用系统界面** +- [ ] 登录系统 +- [ ] 访问授权管理 +- [ ] 点击生成授权码 +- [ ] 输入天数并生成 + +### 步骤6: 激活授权码 +- [ ] 访问登录页 +- [ ] 点击"激活授权码" +- [ ] 输入授权码 +- [ ] 激活成功 + +## ✅ 功能测试 + +### 测试1: 授权码生成 +- [ ] 系统界面生成授权码成功 +- [ ] 独立工具生成授权码成功 +- [ ] 授权码格式正确 + +### 测试2: 授权码激活 +- [ ] 登录页激活成功 +- [ ] 系统内激活成功 +- [ ] 数据库记录正确 + +### 测试3: 授权验证 +- [ ] 有效授权可正常登录 +- [ ] 无授权无法登录 +- [ ] 过期授权无法登录 + +### 测试4: 授权拦截 +- [ ] 未授权访问被拦截 +- [ ] 有效授权可正常访问 +- [ ] 静态资源不被拦截 + +### 测试5: 定时检查 +- [ ] 定时任务正常执行 +- [ ] 授权过期自动更新状态 +- [ ] 在线用户被踢出 + +### 测试6: 授权管理 +- [ ] 授权列表查询正常 +- [ ] 授权信息显示正确 +- [ ] 授权删除功能正常 + +## 🔍 问题排查 + +### 问题1: 编译失败 +**检查项:** +- [ ] Maven依赖是否正确 +- [ ] JDK版本是否匹配 +- [ ] pom.xml配置是否正确 + +**解决方案:** +```bash +mvn clean +mvn dependency:resolve +mvn install +``` + +### 问题2: 启动失败 +**检查项:** +- [ ] 数据库连接是否正常 +- [ ] 端口是否被占用 +- [ ] 配置文件是否正确 + +**解决方案:** +- 检查 application.yml 配置 +- 查看启动日志错误信息 +- 确认数据库表已创建 + +### 问题3: 无法激活授权码 +**检查项:** +- [ ] 授权码格式是否正确 +- [ ] 授权码是否已过期 +- [ ] 密钥是否一致 + +**解决方案:** +- 重新生成授权码 +- 检查 LicenseUtils 密钥配置 +- 查看后台错误日志 + +### 问题4: 授权验证不生效 +**检查项:** +- [ ] 拦截器是否注册 +- [ ] 数据库授权记录是否正确 +- [ ] 定时任务是否执行 + +**解决方案:** +- 检查 LicenseConfig 配置 +- 验证数据库 status 字段 +- 确认 @EnableScheduling 已添加 + +### 问题5: 在线用户未被踢出 +**检查项:** +- [ ] SessionDAO 是否注入 +- [ ] 定时任务是否执行 +- [ ] Shiro配置是否正确 + +**解决方案:** +- 检查 LicenseCheckTask 日志 +- 验证 SessionDAO bean +- 重启应用服务器 + +## 📊 监控指标 + +### 运行时监控 +- [ ] 授权状态正常 +- [ ] 定时任务执行正常 +- [ ] 无异常日志 +- [ ] 内存使用正常 +- [ ] CPU使用正常 + +### 业务监控 +- [ ] 授权激活次数 +- [ ] 授权过期提醒 +- [ ] 用户登录成功率 +- [ ] 授权拦截次数 + +## 🔐 安全检查 + +### 配置安全 +- [ ] 生产环境已修改默认密钥 +- [ ] 数据库密码已加密 +- [ ] 授权码传输使用HTTPS +- [ ] 敏感日志已脱敏 + +### 权限安全 +- [ ] 授权管理菜单仅管理员可见 +- [ ] 授权激活操作有日志记录 +- [ ] 授权码生成有权限控制 +- [ ] 数据库访问有权限限制 + +### 数据安全 +- [ ] 授权码加密存储 +- [ ] 定期备份授权数据 +- [ ] 授权日志完整保存 +- [ ] 敏感信息不输出到日志 + +## 📝 上线清单 + +### 上线前 +- [ ] 所有测试通过 +- [ ] 代码已提交版本库 +- [ ] 数据库脚本已准备 +- [ ] 部署文档已编写 +- [ ] 回滚方案已准备 + +### 上线中 +- [ ] 备份现有数据 +- [ ] 执行数据库脚本 +- [ ] 部署新版本代码 +- [ ] 生成初始授权码 +- [ ] 激活授权码 + +### 上线后 +- [ ] 验证功能正常 +- [ ] 检查日志无异常 +- [ ] 监控系统运行状态 +- [ ] 通知相关人员 +- [ ] 更新运维文档 + +## 📞 应急联系 + +### 技术支持 +- 开发负责人: ___________ +- 运维负责人: ___________ +- 数据库管理员: ___________ + +### 应急预案 +1. 授权失效 -> 立即激活新授权码 +2. 系统无法登录 -> 检查授权状态 +3. 数据丢失 -> 从备份恢复 +4. 性能问题 -> 调整定时任务频率 + +--- + +**检查人**: ___________ +**检查日期**: ___________ +**部署环境**: [ ] 开发 [ ] 测试 [ ] 生产 +**部署状态**: [ ] 成功 [ ] 失败 [ ] 部分成功 diff --git a/ruoyi-license/INTEGRATION.md b/ruoyi-license/INTEGRATION.md new file mode 100644 index 0000000..1b8199b --- /dev/null +++ b/ruoyi-license/INTEGRATION.md @@ -0,0 +1,231 @@ +# RuoYi 授权码模块集成指南 + +## 一、模块说明 + +本授权码模块已完成开发,包含以下文件: + +``` +ruoyi-license/ # 授权模块(独立模块) +├── src/main/java/com/ruoyi/license/ +│ ├── config/ +│ │ └── LicenseConfig.java # 拦截器配置 +│ ├── domain/ +│ │ └── SysLicense.java # 授权实体 +│ ├── interceptor/ +│ │ └── LicenseInterceptor.java # 授权拦截器 +│ ├── mapper/ +│ │ └── SysLicenseMapper.java # 数据访问接口 +│ ├── service/ +│ │ ├── ISysLicenseService.java # 服务接口 +│ │ └── impl/ +│ │ └── SysLicenseServiceImpl.java # 服务实现 +│ ├── task/ +│ │ └── LicenseCheckTask.java # 定时检查任务 +│ └── util/ +│ ├── LicenseUtils.java # 授权工具类 +│ └── LicenseGenerator.java # 授权码生成器 +├── src/main/resources/mapper/license/ +│ └── SysLicenseMapper.xml # MyBatis映射 +├── pom.xml # 模块依赖 +└── README.md # 使用说明 + +sql/ +└── sys_license.sql # 数据库脚本 + +ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/license/ +└── SysLicenseController.java # 授权控制器 + +ruoyi-admin/src/main/resources/templates/system/license/ +├── license.html # 授权列表页 +└── active.html # 激活页面 + +修改的文件: +- pom.xml # 添加license模块 +- ruoyi-admin/pom.xml # 添加license依赖 +- login.html # 添加激活入口 +- login.js # 添加激活功能 +``` + +## 二、部署步骤 + +### 1. 执行数据库脚本 + +```bash +# 在MySQL中执行 +mysql -u root -p your_database < sql/sys_license.sql +``` + +或手动执行 `sql/sys_license.sql` 中的SQL语句。 + +### 2. 启用定时任务 + +在启动类上添加 `@EnableScheduling` 注解: + +```java +@SpringBootApplication +@EnableScheduling // 添加此注解 +public class RuoYiApplication { + public static void main(String[] args) { + SpringApplication.run(RuoYiApplication.class, args); + } +} +``` + +### 3. 编译项目 + +```bash +cd d:\fortucky\project\RuoYi-master +mvn clean install +``` + +### 4. 生成授权码 + +**方式一:使用独立工具生成** +```bash +cd ruoyi-license/src/main/java +javac com/ruoyi/license/util/LicenseUtils.java +javac com/ruoyi/license/util/LicenseGenerator.java +java com.ruoyi.license.util.LicenseGenerator +``` + +**方式二:启动系统后在界面生成** +- 登录系统 +- 进入"系统管理 -> 授权管理" +- 点击"生成授权码"按钮 + +### 5. 激活授权码 + +**首次激活(登录页):** +1. 访问登录页 +2. 点击"激活授权码"链接 +3. 输入生成的授权码 +4. 提交激活 + +**系统内激活:** +1. 登录系统(需要先有有效授权) +2. 进入"系统管理 -> 授权管理" +3. 点击"激活授权码" +4. 输入新的授权码 + +## 三、功能验证 + +### 1. 验证授权拦截 + +- 未激活授权码时,访问任何页面应跳转到登录页并提示"授权已过期" +- 激活授权码后,可正常访问系统 + +### 2. 验证定时检查 + +- 修改数据库中授权码的 `expire_time` 为过去时间 +- 等待5分钟(或重启系统) +- 在线用户应被强制退出 +- 尝试登录应提示授权过期 + +### 3. 验证授权管理 + +- 访问"系统管理 -> 授权管理" +- 查看授权列表 +- 生成新授权码 +- 激活授权码 +- 删除过期授权 + +## 四、复用到其他项目 + +### 方式一:Maven依赖(推荐) + +1. 将 `ruoyi-license` 模块复制到目标项目 +2. 在父pom.xml添加模块声明 +3. 在需要的模块添加依赖 +4. 执行数据库脚本 +5. 复制Controller和前端页面 + +### 方式二:JAR包依赖 + +1. 打包license模块: +```bash +cd ruoyi-license +mvn clean package +``` + +2. 安装到本地仓库: +```bash +mvn install:install-file -Dfile=target/ruoyi-license-4.8.2.jar \ + -DgroupId=com.ruoyi -DartifactId=ruoyi-license \ + -Dversion=4.8.2 -Dpackaging=jar +``` + +3. 在目标项目添加依赖: +```xml + + com.ruoyi + ruoyi-license + 4.8.2 + +``` + +## 五、配置说明 + +### 1. 修改密钥(重要) + +生产环境必须修改默认密钥: + +```java +// LicenseUtils.java +private static final String SECRET_KEY = "YourCustomKey123"; // 改为16位自定义密钥 +``` + +### 2. 调整检查频率 + +```java +// LicenseCheckTask.java +@Scheduled(cron = "0 */5 * * * ?") // 每5分钟,可根据需要调整 +``` + +### 3. 自定义拦截路径 + +```java +// LicenseConfig.java +.excludePathPatterns( + "/login", + "/your-path/**" // 添加需要排除的路径 +) +``` + +## 六、注意事项 + +1. **首次部署**:必须先生成并激活授权码,否则无法登录 +2. **时间同步**:确保服务器时间准确,避免授权判断错误 +3. **密钥安全**:生产环境务必修改默认密钥 +4. **数据备份**:定期备份 `sys_license` 表数据 +5. **权限控制**:授权管理菜单仅授予超级管理员 + +## 七、故障排查 + +### 问题1:无法激活授权码 +- 检查授权码格式是否正确 +- 检查授权码是否已过期 +- 查看后台日志错误信息 + +### 问题2:授权有效但仍被拦截 +- 检查数据库中授权状态是否为"1" +- 检查 `expire_time` 是否大于当前时间 +- 重启应用服务器 + +### 问题3:定时任务不执行 +- 确认启动类添加了 `@EnableScheduling` +- 检查日志是否有定时任务执行记录 + +### 问题4:在线用户未被踢出 +- 检查 `SessionDAO` 是否正确注入 +- 查看定时任务是否正常执行 +- 检查Shiro配置是否正确 + +## 八、技术支持 + +如遇到问题,请提供以下信息: +1. 错误日志 +2. 数据库授权记录 +3. 系统环境信息 +4. 操作步骤 + +联系方式:参考RuoYi官方文档 diff --git a/ruoyi-license/LICENSE_FILTER_CONFIG.md b/ruoyi-license/LICENSE_FILTER_CONFIG.md new file mode 100644 index 0000000..7a3ba06 --- /dev/null +++ b/ruoyi-license/LICENSE_FILTER_CONFIG.md @@ -0,0 +1,168 @@ +# 授权码全局拦截配置说明 + +## 功能说明 + +通过自定义 Shiro Filter,实现授权码失效时,**所有接口**(包括 `anon` 匿名访问的接口)都无法访问。 + +## 实现原理 + +1. 创建 `LicenseValidateFilter` 继承 `AccessControlFilter` +2. 在 Shiro 过滤链的**最前面**添加此过滤器 +3. 所有请求都会先经过授权码验证,验证失败则拒绝访问 + +## 已完成的修改 + +### 1. 新增文件 +- `ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/license/LicenseValidateFilter.java` + +### 2. 修改文件 +- `ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java` + - 导入 `LicenseValidateFilter` + - 注册过滤器:`filters.put("licenseValidate", licenseValidateFilter())` + - 添加到过滤链:`filterChainDefinitionMap.put("/**", "licenseValidate,user,kickout,onlineSession,syncOnlineSession")` + +## 白名单配置 + +以下路径即使授权失效也可以访问(必要的登录和静态资源): + +```java +private static final String[] WHITE_LIST = { + "/login", // 登录页 + "/captcha/captchaImage", // 验证码 + "/css/", // 样式文件 + "/js/", // 脚本文件 + "/img/", // 图片资源 + "/fonts/", // 字体文件 + "/ajax/", // Ajax资源 + "/ruoyi/", // 若依资源 + "/favicon.ico", // 网站图标 + "/system/license/active" // 授权激活页面(必须放行) +}; +``` + +## 自定义白名单 + +如果需要添加其他路径到白名单,修改 `LicenseValidateFilter.java`: + +```java +private static final String[] WHITE_LIST = { + // ... 原有配置 + "/your-custom-path/**", // 添加你的自定义路径 +}; +``` + +## 拦截效果 + +### 授权有效时 +- 所有接口正常访问(包括 `/board/**`、`/agv/api/**` 等 anon 接口) + +### 授权失效时 +- **普通请求**:重定向到 `/login?license=expired` +- **Ajax请求**:返回 JSON `{"code":401,"msg":"授权已过期,请联系管理员激活授权码"}` +- **白名单路径**:仍可访问(用于激活授权码) + +## 测试步骤 + +### 1. 启用授权功能 +在 `application.yml` 中配置: +```yaml +license: + enabled: true +``` + +### 2. 配置项目编号 +在数据库 `sys_config` 表中添加: +```sql +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) +VALUES ('项目编号', 'fortucky.project.no', 'FT507', 'Y', 'admin', NOW(), '用于授权码验证'); +``` + +### 3. 测试授权失效场景 + +**方式一:不激活授权码** +- 启动系统 +- 访问 `/board/**` 接口 +- 预期:被拦截,重定向到登录页 + +**方式二:授权码过期** +- 生成5分钟测试授权码并激活 +- 等待5分钟后访问 `/board/**` 接口 +- 预期:被拦截 + +### 4. 测试授权有效场景 +- 生成并激活有效授权码 +- 访问 `/board/**` 接口 +- 预期:正常访问 + +## 注意事项 + +1. **白名单必须包含授权激活页面** + - `/system/license/active` 必须在白名单中 + - 否则授权失效后无法激活新授权码 + +2. **静态资源必须放行** + - CSS、JS、图片等资源必须在白名单 + - 否则登录页面无法正常显示 + +3. **过滤器顺序很重要** + - `licenseValidate` 必须在最前面 + - 确保所有请求都先经过授权验证 + +4. **开发环境可关闭授权** + ```yaml + license: + enabled: false # 开发时可关闭 + ``` + +## 与原有拦截器的区别 + +### 原有 LicenseInterceptor(Spring MVC 拦截器) +- 在 Spring MVC 层拦截 +- 只拦截进入 Controller 的请求 +- 无法拦截 Shiro 已放行的 `anon` 接口 + +### 新增 LicenseValidateFilter(Shiro 过滤器) +- 在 Shiro 层拦截 +- 拦截所有请求(包括 `anon` 接口) +- 优先级高于 Shiro 的其他过滤器 + +## 建议 + +1. **保留两个拦截器** + - `LicenseValidateFilter`:全局拦截(包括 anon 接口) + - `LicenseInterceptor`:作为二次验证(可选) + +2. **生产环境必须启用** + ```yaml + license: + enabled: true + ``` + +3. **定期检查白名单** + - 确保白名单路径最小化 + - 避免过度放行 + +## 故障排查 + +### 问题1:所有请求都被拦截 +- 检查 `license.enabled` 是否为 `true` +- 检查是否有有效授权码 +- 查看后台日志 `[授权验证]` 相关信息 + +### 问题2:anon 接口仍可访问 +- 检查过滤器是否注册:`filters.put("licenseValidate", ...)` +- 检查过滤链配置:`"licenseValidate,user,..."` +- 确认 `licenseValidate` 在最前面 + +### 问题3:无法激活授权码 +- 检查 `/system/license/active` 是否在白名单 +- 检查白名单匹配逻辑是否正确 + +## 完成标志 + +✅ 授权失效时,`/board/**` 接口无法访问 +✅ 授权失效时,`/agv/api/**` 接口无法访问 +✅ 授权失效时,仍可访问登录页和激活页 +✅ 授权有效时,所有接口正常访问 +✅ Ajax 请求返回 JSON 错误信息 +✅ 普通请求重定向到登录页 diff --git a/ruoyi-license/README.md b/ruoyi-license/README.md new file mode 100644 index 0000000..5cfa495 --- /dev/null +++ b/ruoyi-license/README.md @@ -0,0 +1,193 @@ +# RuoYi 授权码模块 + +## 模块简介 + +ruoyi-license 是一个独立的授权码管理模块,用于控制系统的使用期限。当授权码过期后,系统将无法登录,已登录用户会被强制退出。 + +## 功能特性 + +- ✅ 授权码加密存储 +- ✅ 登录时验证授权有效性 +- ✅ 定时检查授权状态(每5分钟) +- ✅ 授权过期自动踢出在线用户 +- ✅ 系统管理界面查看/激活授权 +- ✅ 登录页面激活授权码 +- ✅ 授权码生成工具 +- ✅ 独立模块,方便其他项目复用 + +## 快速开始 + +### 1. 执行数据库脚本 + +```sql +-- 执行 sql/sys_license.sql 创建授权表和菜单 +``` + +### 2. 添加模块依赖 + +在需要使用授权功能的项目中,添加依赖: + +```xml + + com.ruoyi + ruoyi-license + 4.8.2 + +``` + +### 3. 配置包扫描 + +确保Spring能扫描到授权模块的包: + +```java +@SpringBootApplication +@ComponentScan(basePackages = {"com.ruoyi"}) +public class RuoYiApplication { + // ... +} +``` + +### 4. 生成授权码 + +访问系统管理 -> 授权管理,点击"生成授权码"按钮,输入授权天数(如365天),系统会生成加密的授权码。 + +### 5. 激活授权码 + +**方式一:登录页激活** +- 在登录页面点击"激活授权码"链接 +- 输入授权码并提交 + +**方式二:系统内激活** +- 登录系统后,进入"系统管理 -> 授权管理" +- 点击"激活授权码"按钮 +- 输入授权码并提交 + +## 核心类说明 + +### 1. 实体类 +- `SysLicense`: 授权码实体,包含授权码、开始时间、结束时间、状态等信息 + +### 2. 工具类 +- `LicenseUtils`: 授权码工具类 + - `generateLicense()`: 生成授权码 + - `encrypt()`: 加密数据 + - `decrypt()`: 解密数据 + - `validateFormat()`: 验证授权码格式 + +### 3. 服务类 +- `ISysLicenseService`: 授权服务接口 + - `activeLicense()`: 激活授权码 + - `checkLicenseValid()`: 检查授权是否有效 + - `getActiveLicense()`: 获取当前有效授权 + +### 4. 拦截器 +- `LicenseInterceptor`: 授权拦截器,在每次请求时验证授权有效性 + +### 5. 定时任务 +- `LicenseCheckTask`: 每5分钟检查授权状态,过期自动踢出在线用户 + +## 授权码格式 + +授权码采用AES加密,格式为: +``` +加密(开始时间戳|结束时间戳) +``` + +示例: +``` +原始数据: 1704067200000|1735689600000 +加密后: Xj8K9mN2pQ5rT7vW1yZ3aB4cD6eF8gH0iJ2kL4mN6oP8qR0sT2uV4wX6yZ8 +``` + +## 集成到其他项目 + +### 步骤1:复制模块 +将 `ruoyi-license` 文件夹复制到目标项目根目录 + +### 步骤2:修改pom.xml +在父pom.xml中添加: +```xml + + ruoyi-license + + + + + + com.ruoyi + ruoyi-license + ${project.version} + + + +``` + +### 步骤3:添加依赖 +在启动模块的pom.xml中添加: +```xml + + com.ruoyi + ruoyi-license + +``` + +### 步骤4:执行SQL脚本 +执行 `sql/sys_license.sql` 创建数据库表 + +### 步骤5:配置Controller +将 `SysLicenseController.java` 复制到项目的controller包中 + +### 步骤6:配置前端页面 +将 `templates/system/license/` 目录复制到项目的templates目录 + +### 步骤7:修改登录页面 +参考 `login.html` 和 `login.js` 的修改,添加授权码激活入口 + +## 自定义配置 + +### 修改密钥 +编辑 `LicenseUtils.java`: +```java +private static final String SECRET_KEY = "YourSecretKey123"; // 16位密钥 +``` + +### 修改检查频率 +编辑 `LicenseCheckTask.java`: +```java +@Scheduled(cron = "0 */10 * * * ?") // 改为每10分钟检查 +``` + +### 自定义拦截规则 +编辑 `LicenseConfig.java`,添加或排除拦截路径: +```java +.excludePathPatterns( + "/login", + "/your-custom-path/**" +) +``` + +## 注意事项 + +1. **密钥安全**:生产环境务必修改默认密钥 +2. **时间同步**:确保服务器时间准确 +3. **数据备份**:定期备份授权表数据 +4. **权限控制**:授权管理菜单仅授予管理员 +5. **测试环境**:建议先在测试环境验证功能 + +## 常见问题 + +**Q: 授权码激活失败?** +A: 检查授权码格式是否正确,是否已过期 + +**Q: 如何延长授权期限?** +A: 生成新的授权码并激活即可 + +**Q: 授权过期后如何恢复?** +A: 在登录页点击"激活授权码",输入新的授权码 + +**Q: 可以同时激活多个授权码吗?** +A: 系统只使用最新激活且未过期的授权码 + +## 技术支持 + +如有问题,请联系技术支持或查看RuoYi官方文档。 diff --git a/ruoyi-license/SUMMARY.md b/ruoyi-license/SUMMARY.md new file mode 100644 index 0000000..dc7e368 --- /dev/null +++ b/ruoyi-license/SUMMARY.md @@ -0,0 +1,239 @@ +# RuoYi 授权码模块 - 项目总结 + +## 📦 已完成的工作 + +### 1. 核心模块开发 ✅ + +**ruoyi-license 独立模块** +- ✅ Maven模块配置 (pom.xml) +- ✅ 授权实体类 (SysLicense.java) +- ✅ 数据访问层 (SysLicenseMapper.java + XML) +- ✅ 业务逻辑层 (ISysLicenseService.java + Impl) +- ✅ 授权工具类 (LicenseUtils.java) +- ✅ 授权拦截器 (LicenseInterceptor.java) +- ✅ 拦截器配置 (LicenseConfig.java) +- ✅ 定时检查任务 (LicenseCheckTask.java) +- ✅ 授权码生成器 (LicenseGenerator.java) +- ✅ 测试工具 (LicenseTest.java) + +### 2. 控制器和前端 ✅ + +**后端控制器** +- ✅ SysLicenseController.java (授权管理控制器) + - 授权列表查询 + - 授权码激活 + - 授权码生成 + - 授权信息查看 + - 授权删除 + +**前端页面** +- ✅ license.html (授权列表页) +- ✅ active.html (激活页面) +- ✅ login.html (添加激活入口) +- ✅ login.js (添加激活功能) + +### 3. 数据库脚本 ✅ + +- ✅ sys_license.sql + - 授权表结构 + - 菜单权限配置 + +### 4. 项目集成 ✅ + +- ✅ 主pom.xml (添加license模块) +- ✅ ruoyi-admin/pom.xml (添加license依赖) + +### 5. 文档编写 ✅ + +- ✅ README.md (使用说明) +- ✅ INTEGRATION.md (集成指南) + +## 🎯 功能特性 + +### 核心功能 +1. **授权码加密存储** - AES加密,安全可靠 +2. **登录验证** - 登录时自动验证授权有效性 +3. **拦截器保护** - 所有请求都经过授权验证 +4. **定时检查** - 每5分钟自动检查授权状态 +5. **自动踢出** - 授权过期自动踢出所有在线用户 +6. **管理界面** - 完整的授权管理功能 +7. **登录页激活** - 无需登录即可激活授权码 +8. **授权码生成** - 内置生成工具,支持自定义天数 + +### 技术亮点 +- 🔒 AES加密算法保护授权码 +- 🔄 Spring拦截器实现全局授权验证 +- ⏰ Spring定时任务自动检查授权 +- 🎨 完整的前后端交互界面 +- 📦 独立模块设计,方便复用 +- 🛠️ 提供测试工具和生成工具 + +## 📂 文件清单 + +``` +ruoyi-license/ +├── src/main/java/com/ruoyi/license/ +│ ├── config/ +│ │ └── LicenseConfig.java (拦截器配置) +│ ├── domain/ +│ │ └── SysLicense.java (授权实体) +│ ├── interceptor/ +│ │ └── LicenseInterceptor.java (授权拦截器) +│ ├── mapper/ +│ │ └── SysLicenseMapper.java (Mapper接口) +│ ├── service/ +│ │ ├── ISysLicenseService.java (服务接口) +│ │ └── impl/ +│ │ └── SysLicenseServiceImpl.java (服务实现) +│ ├── task/ +│ │ └── LicenseCheckTask.java (定时任务) +│ └── util/ +│ ├── LicenseUtils.java (工具类) +│ ├── LicenseGenerator.java (生成器) +│ └── LicenseTest.java (测试工具) +├── src/main/resources/mapper/license/ +│ └── SysLicenseMapper.xml (MyBatis映射) +├── pom.xml (模块配置) +├── README.md (使用说明) +└── INTEGRATION.md (集成指南) + +sql/ +└── sys_license.sql (数据库脚本) + +ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/license/ +└── SysLicenseController.java (控制器) + +ruoyi-admin/src/main/resources/templates/system/license/ +├── license.html (列表页) +└── active.html (激活页) + +修改的文件: +├── pom.xml (添加模块) +├── ruoyi-admin/pom.xml (添加依赖) +├── ruoyi-admin/src/main/resources/templates/login.html +└── ruoyi-admin/src/main/resources/static/ruoyi/login.js +``` + +## 🚀 快速开始 + +### 1. 执行数据库脚本 +```sql +source sql/sys_license.sql; +``` + +### 2. 启用定时任务 +在 RuoYiApplication.java 添加 @EnableScheduling + +### 3. 编译项目 +```bash +mvn clean install +``` + +### 4. 生成授权码 +运行 LicenseGenerator.java 或在系统界面生成 + +### 5. 激活授权码 +登录页点击"激活授权码"输入授权码 + +## 🔧 使用场景 + +### 场景1:软件交付 +- 开发完成后生成授权码 +- 交付给客户时提供授权码 +- 客户激活后可使用指定期限 + +### 场景2:试用版本 +- 生成30天试用授权码 +- 客户试用满意后购买正式授权 +- 激活正式授权码延长使用期限 + +### 场景3:订阅服务 +- 按年生成授权码 +- 到期前提醒客户续费 +- 续费后激活新授权码 + +### 场景4:多项目复用 +- 将license模块复制到其他项目 +- 修改密钥和配置 +- 快速实现授权功能 + +## ⚙️ 自定义配置 + +### 修改密钥(必须) +```java +// LicenseUtils.java +private static final String SECRET_KEY = "YourKey16Chars!!"; +``` + +### 调整检查频率 +```java +// LicenseCheckTask.java +@Scheduled(cron = "0 */10 * * * ?") // 改为10分钟 +``` + +### 自定义拦截路径 +```java +// LicenseConfig.java +.excludePathPatterns("/api/**", "/public/**") +``` + +## 📊 数据库表结构 + +```sql +sys_license (授权表) +├── license_id (授权ID - 主键) +├── license_code (授权码 - 加密存储) +├── start_time (开始时间) +├── expire_time (结束时间) +├── status (状态: 0未激活 1已激活 2已过期) +├── active_time (激活时间) +├── active_by (激活人) +├── create_by (创建者) +├── create_time (创建时间) +├── update_by (更新者) +├── update_time (更新时间) +└── remark (备注) +``` + +## 🔐 安全建议 + +1. **生产环境必须修改默认密钥** +2. **授权码通过安全渠道传递** +3. **定期备份授权表数据** +4. **限制授权管理菜单权限** +5. **监控授权激活日志** + +## 📝 后续优化建议 + +1. **硬件绑定** - 绑定服务器MAC地址或CPU序列号 +2. **在线验证** - 连接授权服务器在线验证 +3. **授权分级** - 不同授权码对应不同功能权限 +4. **自动续费** - 集成支付接口自动续费 +5. **授权统计** - 统计授权使用情况和到期提醒 + +## ✅ 测试清单 + +- [ ] 数据库表创建成功 +- [ ] 菜单权限配置正确 +- [ ] 授权码生成功能正常 +- [ ] 授权码加密解密正确 +- [ ] 登录页激活功能正常 +- [ ] 系统内激活功能正常 +- [ ] 授权拦截器生效 +- [ ] 定时任务执行正常 +- [ ] 授权过期自动踢出 +- [ ] 授权列表查询正常 + +## 📞 技术支持 + +如有问题,请查看: +1. README.md - 使用说明 +2. INTEGRATION.md - 集成指南 +3. RuoYi官方文档 + +--- + +**开发完成时间**: 2024 +**模块版本**: 1.0.0 +**适用框架**: RuoYi v4.8.2 +**技术栈**: SpringBoot + MyBatis + Shiro + Thymeleaf diff --git a/ruoyi-license/pom.xml b/ruoyi-license/pom.xml new file mode 100644 index 0000000..9a60987 --- /dev/null +++ b/ruoyi-license/pom.xml @@ -0,0 +1,45 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-license + + + 授权码管理模块 + + + + + + + com.ruoyi + ruoyi-common + + + + + com.ruoyi + ruoyi-system + + + + + com.ruoyi + ruoyi-framework + + + + + org.springframework.boot + spring-boot-starter-web + + + + diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/config/LicenseConfig.java b/ruoyi-license/src/main/java/com/ruoyi/license/config/LicenseConfig.java new file mode 100644 index 0000000..2c4a18a --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/config/LicenseConfig.java @@ -0,0 +1,61 @@ +package com.ruoyi.license.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.ruoyi.framework.config.properties.PermitAllUrlProperties; +import com.ruoyi.license.interceptor.LicenseInterceptor; + +/** + * 授权模块配置 + * + * @author ruoyi + */ +@Configuration +public class LicenseConfig implements WebMvcConfigurer +{ + @Autowired + private LicenseInterceptor licenseInterceptor; + + @Autowired + private PermitAllUrlProperties permitAllUrl; + + @Value("${license.enabled:false}") + private boolean licenseEnabled; + + private static final List BASE_EXCLUDE = Arrays.asList( + "/login", + "/logout", + "/register", + "/captchaImage", + "/system/license/active", + "/swagger-ui.html", + "/swagger-resources/**", + "/webjars/**", + "/*/api-docs", + "/druid/**", + "/*.html", + "/**/*.html", + "/**/*.css", + "/**/*.js", + "/profile/**" + ); + + @Override + public void addInterceptors(InterceptorRegistry registry) + { + if (licenseEnabled) + { + List excludeList = new ArrayList<>(BASE_EXCLUDE); + excludeList.addAll(permitAllUrl.getUrls()); + registry.addInterceptor(licenseInterceptor) + .addPathPatterns("/**") + .excludePathPatterns(excludeList); + } + } +} diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/domain/SysLicense.java b/ruoyi-license/src/main/java/com/ruoyi/license/domain/SysLicense.java new file mode 100644 index 0000000..90dfc8d --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/domain/SysLicense.java @@ -0,0 +1,105 @@ +package com.ruoyi.license.domain; + +import com.ruoyi.common.core.domain.BaseEntity; +import java.util.Date; + +/** + * 系统授权码对象 sys_license + * + * @author ruoyi + */ +public class SysLicense extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 授权ID */ + private Long licenseId; + + /** 授权码 */ + private String licenseCode; + + /** 授权开始时间 */ + private Date startTime; + + /** 授权结束时间 */ + private Date expireTime; + + /** 授权状态(0未激活 1已激活 2已过期) */ + private String status; + + /** 激活时间 */ + private Date activeTime; + + /** 激活人 */ + private String activeBy; + + public Long getLicenseId() + { + return licenseId; + } + + public void setLicenseId(Long licenseId) + { + this.licenseId = licenseId; + } + + public String getLicenseCode() + { + return licenseCode; + } + + public void setLicenseCode(String licenseCode) + { + this.licenseCode = licenseCode; + } + + public Date getStartTime() + { + return startTime; + } + + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + public Date getExpireTime() + { + return expireTime; + } + + public void setExpireTime(Date expireTime) + { + this.expireTime = expireTime; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public Date getActiveTime() + { + return activeTime; + } + + public void setActiveTime(Date activeTime) + { + this.activeTime = activeTime; + } + + public String getActiveBy() + { + return activeBy; + } + + public void setActiveBy(String activeBy) + { + this.activeBy = activeBy; + } +} diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/interceptor/LicenseInterceptor.java b/ruoyi-license/src/main/java/com/ruoyi/license/interceptor/LicenseInterceptor.java new file mode 100644 index 0000000..0b1f6fc --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/interceptor/LicenseInterceptor.java @@ -0,0 +1,46 @@ +package com.ruoyi.license.interceptor; + +import java.io.PrintWriter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import com.ruoyi.license.service.ISysLicenseService; + +/** + * 授权拦截器 + * + * @author ruoyi + */ +@Component +public class LicenseInterceptor implements HandlerInterceptor +{ + @Autowired + private ISysLicenseService licenseService; + + @Value("${license.enabled:false}") + private boolean licenseEnabled; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + if (!licenseEnabled) + { + return true; + } + + if (!licenseService.checkLicenseValid()) + { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.setContentType("application/json;charset=UTF-8"); + PrintWriter writer = response.getWriter(); + writer.write("{\"code\":500,\"msg\":\"\u7cfb\u7edf\u6388\u6743\u5df2\u8fc7\u671f\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458\u6fc0\u6d3b\u6388\u6743\u7801\"}"); + writer.flush(); + return false; + } + + return true; + } +} diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/mapper/SysLicenseMapper.java b/ruoyi-license/src/main/java/com/ruoyi/license/mapper/SysLicenseMapper.java new file mode 100644 index 0000000..bd29eb6 --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/mapper/SysLicenseMapper.java @@ -0,0 +1,67 @@ +package com.ruoyi.license.mapper; + +import com.ruoyi.license.domain.SysLicense; +import java.util.List; + +/** + * 授权码Mapper接口 + * + * @author ruoyi + */ +public interface SysLicenseMapper +{ + /** + * 查询授权码 + * + * @param licenseId 授权码ID + * @return 授权码 + */ + public SysLicense selectSysLicenseById(Long licenseId); + + /** + * 查询授权码列表 + * + * @param sysLicense 授权码 + * @return 授权码集合 + */ + public List selectSysLicenseList(SysLicense sysLicense); + + /** + * 查询当前有效的授权码 + * + * @return 授权码 + */ + public SysLicense selectActiveLicense(); + + /** + * 新增授权码 + * + * @param sysLicense 授权码 + * @return 结果 + */ + public int insertSysLicense(SysLicense sysLicense); + + /** + * 修改授权码 + * + * @param sysLicense 授权码 + * @return 结果 + */ + public int updateSysLicense(SysLicense sysLicense); + + /** + * 删除授权码 + * + * @param licenseId 授权码ID + * @return 结果 + */ + public int deleteSysLicenseById(Long licenseId); + + /** + * 批量删除授权码 + * + * @param licenseIds 需要删除的数据ID + * @return 结果 + */ + public int deleteSysLicenseByIds(String[] licenseIds); +} diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/service/ISysLicenseService.java b/ruoyi-license/src/main/java/com/ruoyi/license/service/ISysLicenseService.java new file mode 100644 index 0000000..1a9a225 --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/service/ISysLicenseService.java @@ -0,0 +1,87 @@ +package com.ruoyi.license.service; + +import com.ruoyi.license.domain.SysLicense; +import java.util.List; + +/** + * 授权码Service接口 + * + * @author ruoyi + */ +public interface ISysLicenseService +{ + /** + * 查询授权码 + * + * @param licenseId 授权码ID + * @return 授权码 + */ + public SysLicense selectSysLicenseById(Long licenseId); + + /** + * 查询授权码列表 + * + * @param sysLicense 授权码 + * @return 授权码集合 + */ + public List selectSysLicenseList(SysLicense sysLicense); + + /** + * 新增授权码 + * + * @param sysLicense 授权码 + * @return 结果 + */ + public int insertSysLicense(SysLicense sysLicense); + + /** + * 修改授权码 + * + * @param sysLicense 授权码 + * @return 结果 + */ + public int updateSysLicense(SysLicense sysLicense); + + /** + * 批量删除授权码 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteSysLicenseByIds(String ids); + + /** + * 删除授权码信息 + * + * @param licenseId 授权码ID + * @return 结果 + */ + public int deleteSysLicenseById(Long licenseId); + + /** + * 激活授权码 + * + * @param licenseCode 授权码 + * @return 结果 + */ + public int activeLicense(String licenseCode); + + /** + * 检查授权是否有效 + * + * @return true有效 false无效 + */ + public boolean checkLicenseValid(); + + /** + * 获取当前有效授权信息 + * + * @return 授权信息 + */ + public SysLicense getActiveLicense(); + + /** + * 更新过期授权码状态 + */ + public void updateExpiredLicenses(); +} diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/service/impl/SysLicenseServiceImpl.java b/ruoyi-license/src/main/java/com/ruoyi/license/service/impl/SysLicenseServiceImpl.java new file mode 100644 index 0000000..0b3dcbf --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/service/impl/SysLicenseServiceImpl.java @@ -0,0 +1,267 @@ +package com.ruoyi.license.service.impl; + +import java.util.Date; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.license.domain.SysLicense; +import com.ruoyi.license.mapper.SysLicenseMapper; +import com.ruoyi.license.service.ISysLicenseService; +import com.ruoyi.license.util.LicenseUtils; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 授权码Service业务层处理 + * + * @author ruoyi + */ +@Service +public class SysLicenseServiceImpl implements ISysLicenseService +{ + @Autowired + private SysLicenseMapper sysLicenseMapper; + + @Autowired + private ISysConfigService sysConfigService; + + @org.springframework.beans.factory.annotation.Value("${license.enabled:false}") + private boolean licenseEnabled; + + /** + * 查询授权码 + * + * @param licenseId 授权码ID + * @return 授权码 + */ + @Override + public SysLicense selectSysLicenseById(Long licenseId) + { + return sysLicenseMapper.selectSysLicenseById(licenseId); + } + + /** + * 查询授权码列表 + * + * @param sysLicense 授权码 + * @return 授权码 + */ + @Override + public List selectSysLicenseList(SysLicense sysLicense) + { + return sysLicenseMapper.selectSysLicenseList(sysLicense); + } + + /** + * 新增授权码 + * + * @param sysLicense 授权码 + * @return 结果 + */ + @Override + public int insertSysLicense(SysLicense sysLicense) + { + sysLicense.setCreateTime(new Date()); + return sysLicenseMapper.insertSysLicense(sysLicense); + } + + /** + * 修改授权码 + * + * @param sysLicense 授权码 + * @return 结果 + */ + @Override + public int updateSysLicense(SysLicense sysLicense) + { + sysLicense.setUpdateTime(new Date()); + return sysLicenseMapper.updateSysLicense(sysLicense); + } + + /** + * 删除授权码对象 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteSysLicenseByIds(String ids) + { + return sysLicenseMapper.deleteSysLicenseByIds(Convert.toStrArray(ids)); + } + + /** + * 删除授权码信息 + * + * @param licenseId 授权码ID + * @return 结果 + */ + @Override + public int deleteSysLicenseById(Long licenseId) + { + return sysLicenseMapper.deleteSysLicenseById(licenseId); + } + + /** + * 激活授权码 + * + * @param licenseCode 授权码 + * @return 结果 + */ + @Override + public int activeLicense(String licenseCode) + { + if (StringUtils.isEmpty(licenseCode)) + { + return 0; + } + + // 验证授权码格式 + if (!LicenseUtils.validateFormat(licenseCode)) + { + return 0; + } + + try + { + // 从配置表获取项目编号 + String projectNo = sysConfigService.selectConfigByKey("fortucky.project.no"); + if (StringUtils.isEmpty(projectNo)) + { + return 0; + } + + // 解析授权码 + long expireTime = LicenseUtils.parseLicense(licenseCode, projectNo); + if (expireTime == 0) + { + return 0; + } + + Date now = new Date(); + Date startTime = now; + Date expireDate = new Date(expireTime); + + // 验证是否已过期 + if (expireDate.before(now)) + { + return 0; + } + + // 创建授权记录 + SysLicense license = new SysLicense(); + license.setLicenseCode(licenseCode); + license.setStartTime(startTime); + license.setExpireTime(expireDate); + license.setStatus("1"); + license.setActiveTime(now); + license.setCreateTime(now); + + return sysLicenseMapper.insertSysLicense(license); + } + catch (Exception e) + { + return 0; + } + } + + /** + * 检查授权是否有效 + * + * @return true有效 false无效 + */ + @Override + public boolean checkLicenseValid() + { + // 如果授权码功能未启用,直接返回true + if (!licenseEnabled) + { + return true; + } + + SysLicense license = sysLicenseMapper.selectActiveLicense(); + if (license == null) + { + System.out.println("[授权检查] 没有找到激活的授权码"); + return false; + } + + // 从配置表获取项目编号 + String projectNo = sysConfigService.selectConfigByKey("fortucky.project.no"); + if (StringUtils.isEmpty(projectNo)) + { + System.out.println("[授权检查] 项目编号未配置"); + return false; + } + + System.out.println("[授权检查] 授权码: " + license.getLicenseCode()); + System.out.println("[授权检查] 项目编号: " + projectNo); + + // 从授权码解析过期时间 + long expireTime = LicenseUtils.parseLicense(license.getLicenseCode(), projectNo); + if (expireTime == 0) + { + System.out.println("[授权检查] 授权码解析失败"); + return false; + } + + Date now = new Date(); + System.out.println("[授权检查] 当前时间: " + now.getTime()); + System.out.println("[授权检查] 过期时间: " + expireTime); + + if (expireTime < now.getTime()) + { + System.out.println("[授权检查] 授权已过期"); + // 更新为过期状态 + license.setStatus("2"); + sysLicenseMapper.updateSysLicense(license); + return false; + } + + + return true; + } + + /** + * 获取当前有效授权信息 + * + * @return 授权信息 + */ + @Override + public SysLicense getActiveLicense() + { + return sysLicenseMapper.selectActiveLicense(); + } + + /** + * 更新过期授权码状态 + */ + @Override + public void updateExpiredLicenses() + { + SysLicense query = new SysLicense(); + query.setStatus("1"); + List licenses = sysLicenseMapper.selectSysLicenseList(query); + + // 从配置表获取项目编号 + String projectNo = sysConfigService.selectConfigByKey("fortucky.project.no"); + if (StringUtils.isEmpty(projectNo)) + { + return; + } + + Date now = new Date(); + for (SysLicense license : licenses) + { + // 从授权码解析过期时间 + long expireTime = LicenseUtils.parseLicense(license.getLicenseCode(), projectNo); + if (expireTime > 0 && expireTime < now.getTime()) + { + license.setStatus("2"); + license.setUpdateTime(now); + sysLicenseMapper.updateSysLicense(license); + } + } + } +} diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/task/LicenseCheckTask.java b/ruoyi-license/src/main/java/com/ruoyi/license/task/LicenseCheckTask.java new file mode 100644 index 0000000..9963c61 --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/task/LicenseCheckTask.java @@ -0,0 +1,47 @@ +package com.ruoyi.license.task; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.license.service.ISysLicenseService; + +/** + * 授权检查定时任务 + * + * @author ruoyi + */ +@Component +public class LicenseCheckTask +{ + @Autowired + private ISysLicenseService licenseService; + + @Autowired + private RedisCache redisCache; + + @Value("${license.enabled:false}") + private boolean licenseEnabled; + + /** + * 每5分钟检查一次授权状态 + */ + @Scheduled(cron = "0 */5 * * * ?") + public void checkLicense() + { + if (!licenseEnabled) + { + return; + } + + if (!licenseService.checkLicenseValid()) + { + // 授权无效,删除 Redis 中所有登录 token,强制踢出所有在线用户 + redisCache.deleteObject(redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*")); + } + + licenseService.updateExpiredLicenses(); + } +} diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseGenerator.java b/ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseGenerator.java new file mode 100644 index 0000000..16ce407 --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseGenerator.java @@ -0,0 +1,127 @@ +package com.ruoyi.license.util; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Scanner; + +/** + * 授权码生成工具(独立运行) + * + * @author ruoyi + */ +public class LicenseGenerator +{ + public static void main(String[] args) + { + Scanner scanner = new Scanner(System.in); + + System.out.println("========================================"); + System.out.println(" RuoYi 授权码生成工具"); + System.out.println("========================================"); + System.out.println(); + + try + { + // 选择授权类型 + System.out.println("请选择授权类型:"); + System.out.println("1. 5分钟测试"); + System.out.println("2. 1个月授权"); + System.out.println("3. 3个月授权"); + System.out.println("4. 1年授权"); + System.out.println("5. 5年授权"); + System.out.println("6. 长期授权"); + System.out.print("请输入选项(1-6): "); + String typeChoice = scanner.nextLine().trim(); + + String type; + String typeDesc; + long expireTime; + + switch (typeChoice) + { + case "1": + type = LicenseUtils.TYPE_TEST; + typeDesc = "5分钟测试"; + expireTime = System.currentTimeMillis() + 5 * 60 * 1000L; + break; + case "2": + type = LicenseUtils.TYPE_MONTH; + typeDesc = "1个月授权"; + expireTime = System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000; + break; + case "3": + type = LicenseUtils.TYPE_THREE; + typeDesc = "3个月授权"; + expireTime = System.currentTimeMillis() + 90L * 24 * 60 * 60 * 1000; + break; + case "4": + type = LicenseUtils.TYPE_YEAR; + typeDesc = "1年授权"; + expireTime = System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000; + break; + case "5": + type = LicenseUtils.TYPE_FIVE; + typeDesc = "5年授权"; + expireTime = System.currentTimeMillis() + 5 * 365L * 24 * 60 * 60 * 1000; + break; + case "6": + type = LicenseUtils.TYPE_FOREVER; + typeDesc = "长期授权"; + expireTime = System.currentTimeMillis() + 100 * 365L * 24 * 60 * 60 * 1000; + break; + default: + System.out.println("错误:无效的选项"); + return; + } + + // 输入项目编号 + System.out.print("请输入项目编号: "); + String projectNo = scanner.nextLine().trim(); + + if (projectNo.isEmpty()) + { + System.out.println("错误:项目编号不能为空"); + return; + } + + // 生成授权码 + String licenseCode = LicenseUtils.generateLicense(type, projectNo); + + if (licenseCode == null) + { + System.out.println("错误:授权码生成失败"); + return; + } + + // 格式化日期 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date now = new Date(); + Date expire = new Date(expireTime); + + System.out.println(); + System.out.println("========================================"); + System.out.println("授权码生成成功!"); + System.out.println("========================================"); + System.out.println("授权类型: " + typeDesc); + System.out.println("项目编号: " + projectNo); + System.out.println("当前时间: " + sdf.format(now)); + System.out.println("过期时间: " + sdf.format(expire)); + System.out.println("----------------------------------------"); + System.out.println("授权码:"); + System.out.println(licenseCode); + System.out.println("========================================"); + System.out.println(); + System.out.println("请妥善保管授权码,在系统中激活后即可使用。"); + System.out.println("注意:系统配置中的项目编号必须与此处一致才能激活。"); + } + catch (Exception e) + { + System.out.println("错误:" + e.getMessage()); + e.printStackTrace(); + } + finally + { + scanner.close(); + } + } +} diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseTest.java b/ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseTest.java new file mode 100644 index 0000000..0b69a1c --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseTest.java @@ -0,0 +1,89 @@ +package com.ruoyi.license.util; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 授权码测试工具 + * + * @author ruoyi + */ +public class LicenseTest +{ + public static void main(String[] args) + { + System.out.println("========================================"); + System.out.println(" 授权码功能测试"); + System.out.println("========================================"); + System.out.println(); + + String projectNo = "TEST001"; + + // 测试1:生成各类型授权码 + System.out.println("【测试1】生成各类型授权码"); + String testCode = LicenseUtils.generateLicense(LicenseUtils.TYPE_TEST, projectNo); + String yearCode = LicenseUtils.generateLicense(LicenseUtils.TYPE_YEAR, projectNo); + String fiveCode = LicenseUtils.generateLicense(LicenseUtils.TYPE_FIVE, projectNo); + String foreverCode = LicenseUtils.generateLicense(LicenseUtils.TYPE_FOREVER, projectNo); + + System.out.println("5分钟测试: " + testCode); + System.out.println("1年授权: " + yearCode); + System.out.println("5年授权: " + fiveCode); + System.out.println("长期授权: " + foreverCode); + System.out.println("结果: ✓ 通过"); + System.out.println(); + + // 测试2:验证授权码格式 + System.out.println("【测试2】验证授权码格式"); + boolean isValid = LicenseUtils.validateFormat(yearCode); + System.out.println("格式验证: " + (isValid ? "✓ 通过" : "✗ 失败")); + System.out.println(); + + // 测试3:解析授权码 + System.out.println("【测试3】解析授权码"); + long expireTime = LicenseUtils.parseLicense(yearCode, projectNo); + if (expireTime > 0) + { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date expire = new Date(expireTime); + System.out.println("过期时间: " + sdf.format(expire)); + System.out.println("结果: ✓ 通过"); + } + else + { + System.out.println("结果: ✗ 失败"); + } + System.out.println(); + + // 测试4:项目编号验证 + System.out.println("【测试4】项目编号验证"); + long validResult = LicenseUtils.parseLicense(yearCode, projectNo); + long invalidResult = LicenseUtils.parseLicense(yearCode, "WRONG001"); + System.out.println("正确项目编号: " + (validResult > 0 ? "✓ 通过" : "✗ 失败")); + System.out.println("错误项目编号: " + (invalidResult == 0 ? "✓ 通过(正确拒绝)" : "✗ 失败")); + System.out.println(); + + // 测试5:授权码格式展示 + System.out.println("【测试5】授权码格式展示"); + System.out.println("授权码: " + yearCode); + String[] parts = yearCode.split("_"); + if (parts.length == 5) + { + System.out.println("前缀: " + parts[0]); + System.out.println("类型: " + parts[1] + " (" + LicenseUtils.getTypeDesc(parts[1]) + ")"); + System.out.println("项目编号: " + parts[2]); + System.out.println("加密时间: " + parts[3]); + System.out.println("校验码: " + parts[4]); + System.out.println("结果: ✓ 通过"); + } + else + { + System.out.println("结果: ✗ 失败"); + } + System.out.println(); + + System.out.println("========================================"); + System.out.println("测试完成!"); + System.out.println("========================================"); + } +} diff --git a/ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseUtils.java b/ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseUtils.java new file mode 100644 index 0000000..ef456ae --- /dev/null +++ b/ruoyi-license/src/main/java/com/ruoyi/license/util/LicenseUtils.java @@ -0,0 +1,275 @@ +package com.ruoyi.license.util; + +import java.util.Base64; +import java.util.Date; +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +/** + * 授权码工具类 + * 格式: FORTUCKY_授权类型_项目编号_加密时间_校验码 + * + * @author ruoyi + */ +public class LicenseUtils +{ + // 密钥(16位) + private static final String SECRET_KEY = "RuoYiLicense2024"; + + // 授权类型 + public static final String TYPE_TEST = "TEST"; // 5分钟测试 + public static final String TYPE_MONTH = "MONTH"; // 1个月 + public static final String TYPE_THREE = "THREE"; // 3个月 + public static final String TYPE_YEAR = "YEAR"; // 1年 + public static final String TYPE_FIVE = "FIVE"; // 5年 + public static final String TYPE_FOREVER = "FOREVER"; // 长期 + + /** + * 生成授权码 + * + * @param type 授权类型 + * @param projectNo 项目编号 + * @return 授权码 + */ + public static String generateLicense(String type, String projectNo) + { + try + { + long expireTime = calculateExpireTime(type); + if (expireTime == 0) + { + return null; + } + + // 加密过期时间 + String encryptedTime = encrypt(String.valueOf(expireTime)); + if (encryptedTime == null) + { + return null; + } + + // 生成校验码(取加密时间的前8位MD5) + String checkCode = generateCheckCode(type, projectNo, encryptedTime); + + // 拼接授权码: FORTUCKY_类型_项目编号_加密时间_校验码 + return "FORTUCKY_" + type + "_" + projectNo + "_" + encryptedTime + "_" + checkCode; + } + catch (Exception e) + { + return null; + } + } + + /** + * 验证授权码格式 + * + * @param licenseCode 授权码 + * @return true有效 false无效 + */ + public static boolean validateFormat(String licenseCode) + { + if (licenseCode == null || licenseCode.trim().isEmpty()) + { + return false; + } + + String[] parts = licenseCode.split("_"); + if (parts.length != 5) + { + return false; + } + + if (!"FORTUCKY".equals(parts[0])) + { + return false; + } + + // 验证授权类型 + String type = parts[1]; + if (!TYPE_TEST.equals(type) && !TYPE_MONTH.equals(type) && !TYPE_THREE.equals(type) && + !TYPE_YEAR.equals(type) && !TYPE_FIVE.equals(type) && !TYPE_FOREVER.equals(type)) + { + return false; + } + + return true; + } + + /** + * 解析授权码 + * + * @param licenseCode 授权码 + * @param projectNo 系统项目编号 + * @return 过期时间戳,0表示解析失败 + */ + public static long parseLicense(String licenseCode, String projectNo) + { + try + { + if (!validateFormat(licenseCode)) + { + return 0; + } + + String[] parts = licenseCode.split("_"); + String type = parts[1]; + String codeProjectNo = parts[2]; + String encryptedTime = parts[3]; + String checkCode = parts[4]; + + // 验证项目编号 + if (!codeProjectNo.equals(projectNo)) + { + return 0; + } + + // 验证校验码 + String expectedCheckCode = generateCheckCode(type, codeProjectNo, encryptedTime); + if (!checkCode.equals(expectedCheckCode)) + { + return 0; + } + + // 解密时间 + String decrypted = decrypt(encryptedTime); + if (decrypted == null) + { + return 0; + } + + return Long.parseLong(decrypted); + } + catch (Exception e) + { + return 0; + } + } + + /** + * 获取授权类型描述 + * + * @param type 授权类型 + * @return 描述 + */ + public static String getTypeDesc(String type) + { + switch (type) + { + case TYPE_TEST: return "5分钟测试"; + case TYPE_MONTH: return "1个月授权"; + case TYPE_THREE: return "3个月授权"; + case TYPE_YEAR: return "1年授权"; + case TYPE_FIVE: return "5年授权"; + case TYPE_FOREVER: return "长期授权"; + default: return "未知"; + } + } + + /** + * 计算过期时间 + * + * @param type 授权类型 + * @return 过期时间戳 + */ + private static long calculateExpireTime(String type) + { + long now = System.currentTimeMillis(); + + switch (type) + { + case TYPE_TEST: + return now + 5 * 60 * 1000L; // 5分钟 + case TYPE_MONTH: + return now + 30L * 24 * 60 * 60 * 1000; // 1个月 + case TYPE_THREE: + return now + 90L * 24 * 60 * 60 * 1000; // 3个月 + case TYPE_YEAR: + return now + 365L * 24 * 60 * 60 * 1000; // 1年 + case TYPE_FIVE: + return now + 5 * 365L * 24 * 60 * 60 * 1000; // 5年 + case TYPE_FOREVER: + return now + 100 * 365L * 24 * 60 * 60 * 1000; // 100年(视为长期) + default: + return 0; + } + } + + /** + * 生成校验码 + * + * @param type 授权类型 + * @param projectNo 项目编号 + * @param encryptedTime 加密时间 + * @return 校验码 + */ + private static String generateCheckCode(String type, String projectNo, String encryptedTime) + { + try + { + String data = type + projectNo + encryptedTime; + java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); + byte[] hash = md.digest(data.getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < Math.min(4, hash.length); i++) + { + sb.append(String.format("%02x", hash[i])); + } + return sb.toString().toUpperCase(); + } + catch (Exception e) + { + return "00000000"; + } + } + + /** + * 加密 + * + * @param data 原始数据 + * @return 加密后的数据 + */ + private static String encrypt(String data) + { + try + { + SecretKeySpec key = new SecretKeySpec(SECRET_KEY.getBytes("UTF-8"), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(encrypted).replace("+", "-").replace("/", ".").replace("=", ""); + } + catch (Exception e) + { + return null; + } + } + + /** + * 解密 + * + * @param encryptedData 加密数据 + * @return 解密后的数据 + */ + private static String decrypt(String encryptedData) + { + try + { + String base64Data = encryptedData.replace("-", "+").replace(".", "/"); + int padding = (4 - base64Data.length() % 4) % 4; + for (int i = 0; i < padding; i++) + { + base64Data += "="; + } + + SecretKeySpec key = new SecretKeySpec(SECRET_KEY.getBytes("UTF-8"), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, key); + byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(base64Data)); + return new String(decrypted, "UTF-8"); + } + catch (Exception e) + { + return null; + } + } +} diff --git a/ruoyi-license/src/main/resources/mapper/license/SysLicenseMapper.xml b/ruoyi-license/src/main/resources/mapper/license/SysLicenseMapper.xml new file mode 100644 index 0000000..e6603f0 --- /dev/null +++ b/ruoyi-license/src/main/resources/mapper/license/SysLicenseMapper.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + select license_id, license_code, start_time, expire_time, status, active_time, active_by, create_by, create_time, update_by, update_time, remark from sys_license + + + + + + + + + + insert into sys_license + + license_code, + start_time, + expire_time, + status, + active_time, + active_by, + create_by, + create_time, + update_by, + update_time, + remark, + + + #{licenseCode}, + #{startTime}, + #{expireTime}, + #{status}, + #{activeTime}, + #{activeBy}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{remark}, + + + + + update sys_license + + license_code = #{licenseCode}, + start_time = #{startTime}, + expire_time = #{expireTime}, + status = #{status}, + active_time = #{activeTime}, + active_by = #{activeBy}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + remark = #{remark}, + + where license_id = #{licenseId} + + + + delete from sys_license where license_id = #{licenseId} + + + + delete from sys_license where license_id in + + #{licenseId} + + + +