tsurumure 10 mesiacov pred
rodič
commit
4899c1f01c

+ 53 - 112
README.md

@@ -13,20 +13,20 @@ Maven 配置 (用户配置)
 ./configuration/.m2/settings.xml
 ```
 
-### 手动部署
-> 目前项目使用自动部署
-#### 打包
-第三方JAR包在 `./configuration/.m2/repository` 目录,使用命令添加到本地 Maven 仓库
+### 部署
+#### 1.自动打包
+目前项目使用自动部署
+#### 2.手动打包
+第三方JAR包放在 `./configuration/.m2/repository` 目录,并使用命令添加到本地 Maven 仓库
 ```
 mvn install:install-file -Dfile=configuration/.m2/repository/ziniao-sdk-java-5.1.0.jar -DgroupId=com.ziniao -DartifactId=ziniao-sdk-java -Dversion=5.1.0 -Dpackaging=jar
-// 如果出现拒绝访问,则需要使用管理员权限执行命令
 ```
 
 首次部署需在 pom.xml 中去除 `copy-dependencies` 注释,启用分包功能 (将包分离到 `libs` 文件夹中),像这样:
 ```
 /home/www/project/BackendSys/libs
 ```
-将分离出来的文件,上传到远程服务器目录
+将分离出来的文件,然后上传到远程服务器目录
 
 执行打包命令:
 ```
@@ -47,136 +47,77 @@ sh backendsys.sh start
 tail -f backendsys.log
 ```
 
-#### 工具类
-##### SecurityUtil
+### 开发指南
 
-|Method|Descript|
-|-|-|
-|SecurityUtil.getUserId()|获得当前登录用户ID|
-|SecurityUtil.hasPermission("3.2.1")|判断当前用户是否具备权限|
-|SecurityUtil.hasPermission("3.2.1")|判断当前用户是否具备权限|
+#### 自定义工具类
 
+###### SecurityUtil
 
+注意:`@Anonymous` 下不可调用,否则会抛出错误
+```java
+/*
+ * 权限工具类
+ * SecurityUtil.hasPermission("3.2.1")
+ * SecurityUtil.hasPermissions(Arrays.asList("3.2.1", "3.2.2"))
+ * SecurityUtil.hasPermissions(Arrays.asList("3.2.1", "3.2.2"), MatchType.OR)
+ *
+ * 权限注解
+ * @PreAuthorize("@sr.hasPermission('3.2.1')")
+ * @PreAuthorize("@sr.hasPermissions(T(java.util.Arrays).asList('3.2.1', '3.2.2'))")
+ */
+ ```
 
-### 项目开发指南
-#### 主要目录说明
-* (database/) 存放数据库.sql文件的目录
-* (config/Security/SecurityConfig/) Security放行白名单/跨域资源配置
-* (config/SwaggerConfig/) Swagger文档配置
-* (filter/JwtAuthenticationFilter/) JWT放行白名单配置
-* (exception/GlobalExceptionHandler/) 全局异常处理类
+#### 自定义注解
 
-#### 更改项目名称
-- cmd + Shift + F 将 com.xxx 名称进行全局替换;
-- 将 src/main/java/com/xxx 名称进行修改
-
-### 约束与规范
-
-- controller 控制器层,包含 `权限`
-- service 业务逻辑层,包含 `查询`、`关联查询`、`参数/返回值格式化`
-
-#### 1.变量
-1) 所有返回值的输出字段,使用 `蛇形命名法` (单词之间使用下划线 _ 分隔,所有字母一般都小写)
-2表字段的初始值及赋值,尽量不为 0,以及不从 0 开始
-
-### 2.常用
-#### 1) 实体类
-创建实体类时,创建 id 的同时必须要创建多一个 类id,比如 (article_id),
-在 {编辑} 与 {删除} 操作时,传参和返回值 必须使用该 类id
-
-#### 2) 分页
-页码从1开始,分页使用 PageHelper 插件,
-Service 写法参考:
-com.backendsys.service.System.SysUserServiceImpl.queryUser
-```spring
-public Map<String, Object> queryUser(Integer pageNum, Integer pageSize, SysUserDTO sysUserDTO) {
-    // 分页查询
-    if (pageNum != null && pageSize != null) {
-        PageHelper.startPage(pageNum, pageSize);
-    }
-    // 分页输出 (自定义)
-    List<Map<String, Object>> list = sysUserMapper.queryUserList(sysUserDTO);
-    PageInfoResult pageInfoResult = new PageInfoResult(list);
-    return pageInfoResult.toMap();
-}
-```
-#### 3) `增删改` 操作必须要加 `redisson` 分布式锁,锁的命名为小驼峰格式 (aA)
-#### 4) 提示文字
-```"message": "该用户不存在 (flag)"```
-提示出现 flag 字眼的代表是 逻辑删除,字段 del_flag
-#### 5) GetMapper
-不要在 Controller class 上面写路由,要在每个方法上面写上完整路径 (方便检索)
-#### 6) Token
-如果想在 token 加字段,在 JwtUtil.createSystemToken 中增加
-
-### 3.数据库 / Mybatis
-#### 1) 外键
-建表,禁止使用外键
-#### 2) 字段
-2.1) Mybatis.xml 在 `非必填`,`空值` 的情况下,要在查询语句加入空值转义:
-```
-COALESCE(uf.nickname, '') nickname,
-```
-
-2.2) SQL时间字段创建统一按照以下
-```
-`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+- 免登录权限 (No token)
 ```
-
-2.3) COUNT 返回值默认是 Long 类型
-```
-COUNT(us.id) AS user_count
+@Anonymous
+@GetMapping("/api/getAuthDemo")
+public Result getAuthDemo() {
 ..
-Long user_count = (Long) sysUserRole.get("user_count");
 ```
+另外, `/api/public/**` 也是免登录可访问的接口
+
+#### 开发规范
 
-2.4) Mapper/Service
-- 查,列表、详情,定义变量不使用实体类,统一使用:
-`List<Map<String, Object>>` 或 `Map<String, Object>`
-- 增/删/改时,接收参数使用实体类;返回值使用Long类型或Map类型;
+###### MySQL
 
-2.5) Mapper/Service/Entity/Controller 统一使用目标的`单数`命名
+- 禁止使用外键
 
-#### 3) 关联查询 / 嵌套查询
-使用 集合类型 时:
+###### Mybatis
+- 禁止使用 where 1=1 (`update` 操作)
+- 字段非空时:
 ```
-// 列表使用 LinkedHashMap 类型,详情使用实体类
-<resultMap id="resultMapUser" type="java.util.LinkedHashMap">
-<resultMap id="resultMapUserDetail" type="com.xxx.UserDTO">
-..
-<select id="queryUser" resultMap="resultMapUserList">
+SELECT COALESCE(uf.nickname, '') nickname, ..
 ```
-或
+
+- SQL时间字段创建统一规范:
 ```
-<resultMap id="resultMapUserRoleDetail" type="com.backendsys.entity.System.SysUserRoleDTO">
-  <collection property="modules" javaType="java.util.List" ofType="java.util.LinkedHashMap" select="queryModulesByRoleId" column="id">
-..
-<select id="queryUserRoleDetail" resultMap="resultMapUserRoleDetail">
+`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 ```
-#### 4) 更新操作
-更新操作之前,需要手动判断该次更新的记录是否存在,并返回相关错误提示
+
+- 命名为 `*_count` 的统计字段,值是 Long 类型,例如:
 ```
-public Result updateUserRole(@Validated(SysUserRoleDTO.Update.class) @RequestBody SysUserRoleDTO sysUserRoleDTO) {
-  // 判断是否存在
-  Map<String, Object> sysUser = sysUserMapper.queryUserByIdOrName(sysUserDTO.getUser_id(), null);
-  if (sysUser == null) {
-      return Result.error(ResultEnum.DATABASE_OPERATION_FAILED.getCode(), "用户不存在");
-  }
-  //
+Long user_count = Convert.toLong(sysUserRole.get("user_count"));
 ```
 
-### 4.文档
-#### 1) 名词解释
-* `获得` 前缀表示从数据库获取数据;
-* `查询` 前缀表示从远程第三方直接获取数据;
+###### 其他规范
+- 创建实体类时,要创建多一个 `Id` 字段,比如 `article_id`,在进行 {查询详情/编辑/删除} 操作时,必须使用该 `article_id` 作为必填参数;
+
+
 
 
 
+### 其他
+##### 项目名称更换
+- cmd + Shift + F 将 com.xxx 名称进行全局替换;
+- 将 src/main/java/com/xxx 名称进行修改
+
+
 ### Future (待加功能)
 1.sys_dictionary.sql 字典表重构
 - 字段:{ id, dictionary_key, dictionary_value, lang }
 - 使用 JSON 作为储存值,例如:{ dictionary_key: "Gender", dictionary_value: "[{\"label\":\"男\",\"value\":1},{\"label\":\"女\",\"value\":2}]" }
 - 接口1:查询接口,首次调用Service时,将该值保存到Redis长效缓存
 - 接口2:手动更新缓存的接口
-

+ 21 - 13
src/main/java/com/backendsys/modules/common/config/security/PermitAllUrlProperties.java → src/main/java/com/backendsys/modules/common/config/security/AnonymousProperties.java

@@ -1,5 +1,6 @@
 package com.backendsys.modules.common.config.security;
 
+import cn.hutool.core.convert.Convert;
 import com.backendsys.modules.common.config.security.annotations.Anonymous;
 import org.apache.commons.lang3.RegExUtils;
 import org.springframework.beans.BeansException;
@@ -16,10 +17,11 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 @Configuration
-public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware {
+public class AnonymousProperties implements InitializingBean, ApplicationContextAware {
 
     private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
     private ApplicationContext applicationContext;
@@ -30,7 +32,7 @@ public class PermitAllUrlProperties implements InitializingBean, ApplicationCont
     @Override
     public void afterPropertiesSet() throws Exception {
 
-        System.out.println("afterPropertiesSet:");
+//        System.out.println("afterPropertiesSet:");
 
         // 将整个项目所有的bean对象都拿出来
         RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
@@ -47,19 +49,21 @@ public class PermitAllUrlProperties implements InitializingBean, ApplicationCont
             Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
 
             if (method != null) {
-                System.out.println("method:");
-                System.out.println(method);
-                System.out.println(info);
+                Pattern pattern = Pattern.compile("\\{(\\w+) \\[(.+?)\\]}");
+                Matcher matcher = pattern.matcher(info.toString());
+                if (matcher.find()) {
+                    String url = matcher.group(2);
+                    urls.add(url);
+                }
             }
 
 ////          ifPresent()方法就是会返回一个boolean类型值,如果对象不为空则为真,如果为空则为false
 //            Optional.ofNullable(method).ifPresent(anonymous ->
-//                    //获取url的Set集合,一个方法可能对应多个url
-//                    info.getPatternsCondition().getPatterns()
-//                        .forEach(
-//                            url ->
-//                                urls.add(RegExUtils.replaceAll(String.valueOf(url), PATTERN, ASTERISK))
-//                        ));
+//                //获取url的Set集合,一个方法可能对应多个url
+//                info.getPatternsCondition().getPatterns()
+//                    .forEach(
+//                        url -> urls.add(RegExUtils.replaceAll(String.valueOf(url), PATTERN, ASTERISK))
+//                    ));
 
             // 获取类上边的注解, 替代path variable 为 *
             Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);
@@ -76,11 +80,15 @@ public class PermitAllUrlProperties implements InitializingBean, ApplicationCont
         this.applicationContext = applicationContext;
     }
 
-    public List<String> getUrls()
-    {
+    public List<String> getUrlsList() {
         return urls;
     }
 
+    public String[] getUrls() {
+        return Convert.toStrArray(urls);
+    }
+
+
     public void setUrls(List<String> urls)
     {
         this.urls = urls;

+ 12 - 16
src/main/java/com/backendsys/modules/common/config/security/SecurityConfig.java

@@ -7,14 +7,11 @@ import com.backendsys.modules.common.config.security.filter.JwtAuthenticationFil
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -22,6 +19,8 @@ import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 
+import java.util.Arrays;
+
 @Configuration
 @EnableWebSecurity
 @RequiredArgsConstructor
@@ -51,7 +50,7 @@ public class SecurityConfig {
     private final String[] JWT_WHITELIST;
 
     // 获得配置文件中的白名单变量,注意首尾要去空格
-    private String[] getWhitelist() {
+    private String[] getWhiteUrls() {
         String [] result = ArrayUtil.addAll(JWT_WHITELIST, STATIC_WHITELIST);
         for (int i = 0; i < result.length; i++) {
             result[i] = result[i].trim();
@@ -63,22 +62,18 @@ public class SecurityConfig {
     // https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/web/server/ServerHttpSecurity.html
 
     @Autowired
-    private PermitAllUrlProperties permitAllUrl;
+    private AnonymousProperties anonymousProperties;
 
     @Bean
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 
-        String [] whitelist = getWhitelist();
-
-
-
-
-
-        System.out.println(permitAllUrl.getUrls());
-
-
-
+        // 在配置文件中的 白名单地址
+        String[] whiteUrls = getWhiteUrls();
+        // System.out.println(Arrays.toString(whiteUrls));
 
+        // 使用了 @Anonymous 注解的地址
+        String[] anonymousUrls = anonymousProperties.getUrls();
+        // System.out.println(Arrays.toString(anonymousUrls));
 
         // 路径授权
         http
@@ -89,7 +84,8 @@ public class SecurityConfig {
             // 设置白名单
             .authorizeHttpRequests((authorizeHttpRequests) ->
                 authorizeHttpRequests
-                    .requestMatchers(whitelist).permitAll()
+                    .requestMatchers(whiteUrls).permitAll()
+                    .requestMatchers(anonymousUrls).permitAll()
                     .anyRequest().authenticated()
 //                    .anyRequest().permitAll() // 开放所有
             )

+ 14 - 3
src/main/java/com/backendsys/modules/common/config/security/filter/JwtAuthenticationFilter.java

@@ -1,6 +1,9 @@
 package com.backendsys.modules.common.config.security.filter;
 
+import cn.hutool.core.convert.Convert;
 import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import com.backendsys.modules.common.config.security.AnonymousProperties;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.backendsys.utils.response.Result;
 import com.backendsys.utils.response.ResultEnum;
@@ -24,6 +27,7 @@ import org.springframework.stereotype.Component;
 import org.springframework.web.filter.OncePerRequestFilter;
 
 import java.io.IOException;
+import java.util.Arrays;
 
 /**
  * TODO 一、JWT身份验证过滤器
@@ -59,17 +63,24 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
     @Value("${REDIS_LOGIN_KEY}")
     private String REDIS_LOGIN_KEY;
 
+    @Autowired
+    private AnonymousProperties anonymousProperties;
+
     /**
      * 校验路径是否是要忽略鉴权路径
      * @param url
      */
     private boolean checkUrl(String url){
 
-        String [] whitelist = ArrayUtil.addAll(JWT_WHITELIST, STATIC_WHITELIST);
+        // 使用了 @Anonymous 注解的地址
+        String[] anonymousUrls = anonymousProperties.getUrls();
+
+        // 在配置文件中的 白名单地址
+        String[] whiteUrls = ArrayUtil.addAll(JWT_WHITELIST, STATIC_WHITELIST, anonymousUrls);
 
-        for (String item : whitelist) {
+        for (String item : whiteUrls) {
             String method = item.toString().trim();
-            if (method.equals("/**") || method.equals("**")) {
+            if (Arrays.asList("/**", "**").contains(method)) {
                 return false;
             }
             if (method.endsWith("/**")) {

+ 13 - 4
src/main/java/com/backendsys/modules/common/config/security/utils/SecurityUtil.java

@@ -1,33 +1,38 @@
 package com.backendsys.modules.common.config.security.utils;
 
 import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
+import com.backendsys.exception.CustomException;
 import com.backendsys.modules.common.config.security.entity.SecurityUserInfo;
 import com.backendsys.modules.common.enums.MatchType;
 import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.Jwts;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
 import org.springframework.stereotype.Service;
 
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.Base64;
 import java.util.List;
 
 /**
- * 权限工具类:
+ * [SecurityUtil 使用方法]
+ *
+ * 权限工具类
  * SecurityUtil.hasPermission("3.2.1")
  * SecurityUtil.hasPermissions(Arrays.asList("3.2.1", "3.2.2"))
  * SecurityUtil.hasPermissions(Arrays.asList("3.2.1", "3.2.2"), MatchType.OR)
  *
- * 权限注解
+ * 权限注解
  * @PreAuthorize("@sr.hasPermission('3.2.1')")
  * @PreAuthorize("@sr.hasPermissions(T(java.util.Arrays).asList('3.2.1', '3.2.2'))")
  */
-
 @Service("sr")
 public class SecurityUtil {
 
@@ -60,7 +65,11 @@ public class SecurityUtil {
      * {"id":1,"user_id":1,"username":"admin","phone":"13670511519","phone_area_code":"86","nickname":"超人","email":"admin@qq.com","gender":1,"avatar":"/uploads/20240430/20240430143807.png","last_login_ip":"0:0:0:0:0:0:0:1","last_login_uuid":"5b34e58d-c884-4b43-9ce1-1c85b1136dcb","last_login_time":"2024-10-25 17:26:55","is_super":1,"point_balance":9870,"status":1,"audit_status":2,"audit_note":"同意通过备注","create_time":1689734700000,"update_time":1729848415000,"del_flag":-1,"roles":[{"id":1,"role_name":"管理员"}],"token_expiration":1729934998492}
      */
     public static SecurityUserInfo getUserInfo(){
-        Claims tokenInfo = Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(getToken()).getPayload();
+        String token = getToken();
+        if (StrUtil.isEmpty(token)) throw new CustomException("getUserInfo() token is empty.");
+        if (token.contains("SessionId") || token.contains("RemoteIpAddress")) throw new CustomException("getUserInfo() need token.");
+
+        Claims tokenInfo = Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(token).getPayload();
         JSONObject userInfo = JSONUtil.parseObj(tokenInfo.get("userInfo"));
         String target = Convert.toStr(tokenInfo.get("target"));
         SecurityUserInfo securityUserInfo = JSONUtil.toBean(userInfo, SecurityUserInfo.class);

+ 11 - 6
src/main/java/com/backendsys/modules/system/controller/SysUserV2Controller.java

@@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 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.RestController;
 
@@ -21,24 +22,28 @@ public class SysUserV2Controller {
     @Autowired
     private SysUserV2Service sysUserV2Service;
 
-    @Anonymous
     @Operation(summary = "获得系统用户详情")
+    @PreAuthorize("@sr.hasPermission('3.2.1')")
     @GetMapping("/api/v2/system/user/getUserDetail")
-//    @PreAuthorize("@sr.hasPermissions(T(java.util.Arrays).asList('3.2.1', '3.2.2'))")
     public Result getUserDetail(@Parameter(description = "用户ID") Long user_id) {
 
 //        System.out.println(tokenUtil.getUserId());
 //        System.out.println(SecurityUtil.getUserInfo());
 //        System.out.println(SecurityUtil.getToken());
-        System.out.println("UserId: " + SecurityUtil.getUserId());
 
-        // 判断是否具备权限,或者是超级管理员 (重构)
+//        System.out.println("UserId: " + SecurityUtil.getUserId());
+//
+//        // 判断是否具备权限,或者是超级管理员 (重构)
         System.out.println("hasPermission: " + SecurityUtil.hasPermission("3.2.1"));
-        System.out.println("hasPermissions (AND): " + SecurityUtil.hasPermissions(Arrays.asList("3.2.1", "3.2.2")));
-        System.out.println("hasPermissions (OR): " + SecurityUtil.hasPermissions(Arrays.asList("3.2.1", "3.2.2"), MatchType.OR));
         System.out.println("isSuper: " + SecurityUtil.isSuper());
 
         return Result.success().put("data", sysUserV2Service.selectUserInfo(user_id));
     }
 
+    @Anonymous
+    @GetMapping("/api/v2/system/user/getUserDetail2")
+    public Result getUserDetail2(@Parameter(description = "用户ID") Long user_id) {
+        return Result.success();
+    }
+
 }

+ 1 - 0
src/main/resources/application.yml

@@ -99,6 +99,7 @@ whitelist:
     /api/test/**,
     /ws/**,
     /wss/**
+# /api/v2/system/user/getUserDetail,
 
 
 ## 全局静态变量 ##