Просмотр исходного кода

交易市场平台(供应商)

wwh недель назад: 2
Родитель
Сommit
2ddbd9db7c

+ 6 - 2
baqing-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java

@@ -20,6 +20,7 @@ import com.ruoyi.common.core.page.TableDataInfo;
20
 import com.ruoyi.common.core.redis.RedisCache;
20
 import com.ruoyi.common.core.redis.RedisCache;
21
 import com.ruoyi.common.enums.BusinessType;
21
 import com.ruoyi.common.enums.BusinessType;
22
 import com.ruoyi.common.utils.StringUtils;
22
 import com.ruoyi.common.utils.StringUtils;
23
+import com.ruoyi.framework.web.service.LoginUserRedisStore;
23
 import com.ruoyi.system.domain.SysUserOnline;
24
 import com.ruoyi.system.domain.SysUserOnline;
24
 import com.ruoyi.system.service.ISysUserOnlineService;
25
 import com.ruoyi.system.service.ISysUserOnlineService;
25
 
26
 
@@ -38,6 +39,9 @@ public class SysUserOnlineController extends BaseController
38
     @Autowired
39
     @Autowired
39
     private RedisCache redisCache;
40
     private RedisCache redisCache;
40
 
41
 
42
+    @Autowired
43
+    private LoginUserRedisStore loginUserRedisStore;
44
+
41
     @PreAuthorize("@ss.hasPermi('monitor:online:list')")
45
     @PreAuthorize("@ss.hasPermi('monitor:online:list')")
42
     @GetMapping("/list")
46
     @GetMapping("/list")
43
     public TableDataInfo list(String ipaddr, String userName)
47
     public TableDataInfo list(String ipaddr, String userName)
@@ -46,7 +50,7 @@ public class SysUserOnlineController extends BaseController
46
         List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
50
         List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
47
         for (String key : keys)
51
         for (String key : keys)
48
         {
52
         {
49
-            LoginUser user = redisCache.getCacheObject(key);
53
+            LoginUser user = loginUserRedisStore.load(key);
50
             if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
54
             if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
51
             {
55
             {
52
                 userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
56
                 userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
@@ -77,7 +81,7 @@ public class SysUserOnlineController extends BaseController
77
     @DeleteMapping("/{tokenId}")
81
     @DeleteMapping("/{tokenId}")
78
     public AjaxResult forceLogout(@PathVariable String tokenId)
82
     public AjaxResult forceLogout(@PathVariable String tokenId)
79
     {
83
     {
80
-        redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
84
+        loginUserRedisStore.delete(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
81
         return success();
85
         return success();
82
     }
86
     }
83
 }
87
 }

+ 28 - 12
baqing-admin/src/test/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializerTest.java

@@ -12,26 +12,25 @@ class FastJson2JsonRedisSerializerTest
12
 {
12
 {
13
     private static final String LEGACY_LOGIN_USER_JSON =
13
     private static final String LEGACY_LOGIN_USER_JSON =
14
             "{\"@type\":\"com.ruoyi.common.core.domain.model.LoginUser\",\"browser\":\"Chrome 148\","
14
             "{\"@type\":\"com.ruoyi.common.core.domain.model.LoginUser\",\"browser\":\"Chrome 148\","
15
-                    + "\"deptId\":103L,\"expireTime\":1780467419148,\"ipaddr\":\"115.238.57.190\","
16
-                    + "\"loginLocation\":\"XX XX\",\"loginTime\":1780465619148,\"os\":\"Windows10\","
17
-                    + "\"permissions\":Set[\"*:*:*\"],\"token\":\"22e5d643-b819-4e3a-bb97-0fd65739ff51\","
18
-                    + "\"user\":{\"admin\":true,\"createBy\":\"admin\",\"createTime\":\"2026-05-09 10:26:46\","
15
+                    + "\"deptId\":103L,\"expireTime\":1780482471190,\"ipaddr\":\"115.238.57.190\","
16
+                    + "\"loginLocation\":\"XX XX\",\"loginTime\":1780480671190,\"os\":\"Windows10\","
17
+                    + "\"permissions\":Set[\"*:*:*\"],\"token\":\"96dc6244-2273-489d-865f-36da1c8999bc\","
18
+                    + "\"user\":{\"createBy\":\"admin\",\"createTime\":\"2026-05-09 10:26:46\","
19
                     + "\"delFlag\":\"0\",\"dept\":{\"ancestors\":\"0,100,101\",\"children\":[],"
19
                     + "\"delFlag\":\"0\",\"dept\":{\"ancestors\":\"0,100,101\",\"children\":[],"
20
                     + "\"deptId\":103L,\"deptName\":\"研发部门\",\"leader\":\"若依\",\"orderNum\":1,"
20
                     + "\"deptId\":103L,\"deptName\":\"研发部门\",\"leader\":\"若依\",\"orderNum\":1,"
21
                     + "\"params\":{\"@type\":\"java.util.HashMap\"},\"parentId\":101L,\"status\":\"0\"},"
21
                     + "\"params\":{\"@type\":\"java.util.HashMap\"},\"parentId\":101L,\"status\":\"0\"},"
22
-                    + "\"deptId\":103L,\"email\":\"ry@163.com\",\"loginDate\":\"2026-06-03 13:46:16\","
22
+                    + "\"deptId\":103L,\"email\":\"ry@163.com\",\"loginDate\":\"2026-06-03 17:44:28\","
23
                     + "\"loginIp\":\"115.238.57.190\",\"nickName\":\"超级管理员\","
23
                     + "\"loginIp\":\"115.238.57.190\",\"nickName\":\"超级管理员\","
24
                     + "\"params\":{\"@type\":\"java.util.HashMap\"},\"phonenumber\":\"15888888888\","
24
                     + "\"params\":{\"@type\":\"java.util.HashMap\"},\"phonenumber\":\"15888888888\","
25
                     + "\"pwdUpdateDate\":\"2026-05-09 10:26:46\",\"remark\":\"管理员\","
25
                     + "\"pwdUpdateDate\":\"2026-05-09 10:26:46\",\"remark\":\"管理员\","
26
-                    + "\"roles\":[{\"admin\":true,\"dataScope\":\"1\",\"deptCheckStrictly\":false,"
27
-                    + "\"flag\":false,\"menuCheckStrictly\":false,"
28
-                    + "\"params\":{\"@type\":\"java.util.HashMap\"},\"roleId\":1L,\"roleKey\":\"admin\","
29
-                    + "\"roleName\":\"超级管理员\",\"roleSort\":1,\"status\":\"0\"}],"
30
-                    + "\"sex\":\"1\",\"status\":\"0\",\"userId\":1L,\"userName\":\"admin\"},"
26
+                    + "\"roles\":[{\"dataScope\":\"1\",\"deptCheckStrictly\":false,\"flag\":false,"
27
+                    + "\"menuCheckStrictly\":false,\"params\":{\"@type\":\"java.util.HashMap\"},"
28
+                    + "\"roleId\":1L,\"roleKey\":\"admin\",\"roleName\":\"超级管理员\",\"roleSort\":1,"
29
+                    + "\"status\":\"0\"}],\"sex\":\"1\",\"status\":\"0\",\"userId\":1L,\"userName\":\"admin\"},"
31
                     + "\"userId\":1L,\"username\":\"admin\"}";
30
                     + "\"userId\":1L,\"username\":\"admin\"}";
32
 
31
 
33
     @Test
32
     @Test
34
-    @DisplayName("反序列化含 admin 字段的历史 LoginUser 缓存")
33
+    @DisplayName("反序列化含 admin/flag 字段的历史 LoginUser 缓存")
35
     void deserializeLegacyLoginUserWithAdminField()
34
     void deserializeLegacyLoginUserWithAdminField()
36
     {
35
     {
37
         FastJson2JsonRedisSerializer<LoginUser> serializer = new FastJson2JsonRedisSerializer<>(LoginUser.class);
36
         FastJson2JsonRedisSerializer<LoginUser> serializer = new FastJson2JsonRedisSerializer<>(LoginUser.class);
@@ -40,6 +39,23 @@ class FastJson2JsonRedisSerializerTest
40
         assertEquals(1L, loginUser.getUserId());
39
         assertEquals(1L, loginUser.getUserId());
41
         assertNotNull(loginUser.getUser());
40
         assertNotNull(loginUser.getUser());
42
         assertEquals("admin", loginUser.getUser().getUserName());
41
         assertEquals("admin", loginUser.getUser().getUserName());
43
-        assertEquals("22e5d643-b819-4e3a-bb97-0fd65739ff51", loginUser.getToken());
42
+        assertEquals("96dc6244-2273-489d-865f-36da1c8999bc", loginUser.getToken());
43
+    }
44
+
45
+    @Test
46
+    @DisplayName("反序列化含 admin 的旧版 LoginUser 缓存")
47
+    void deserializeLegacyLoginUserWithAdminOnly()
48
+    {
49
+        String json =
50
+                "{\"@type\":\"com.ruoyi.common.core.domain.model.LoginUser\",\"deptId\":103L,"
51
+                        + "\"permissions\":Set[\"*:*:*\"],\"token\":\"legacy-token\","
52
+                        + "\"user\":{\"admin\":true,\"userId\":1L,\"userName\":\"admin\","
53
+                        + "\"roles\":[{\"admin\":true,\"roleId\":1L,\"roleKey\":\"admin\","
54
+                        + "\"flag\":false,\"deptCheckStrictly\":false,\"menuCheckStrictly\":false}]},"
55
+                        + "\"userId\":1L,\"username\":\"admin\"}";
56
+        FastJson2JsonRedisSerializer<LoginUser> serializer = new FastJson2JsonRedisSerializer<>(LoginUser.class);
57
+        LoginUser loginUser = serializer.deserialize(json.getBytes(FastJson2JsonRedisSerializer.DEFAULT_CHARSET));
58
+        assertNotNull(loginUser);
59
+        assertEquals("admin", loginUser.getUser().getUserName());
44
     }
60
     }
45
 }
61
 }

+ 44 - 0
baqing-admin/src/test/java/com/ruoyi/framework/config/FastJson2RedisReproduceTest.java

@@ -0,0 +1,44 @@
1
+package com.ruoyi.framework.config;
2
+
3
+import com.ruoyi.common.core.domain.model.LoginUser;
4
+import org.junit.jupiter.api.DisplayName;
5
+import org.junit.jupiter.api.Test;
6
+
7
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
8
+import static org.junit.jupiter.api.Assertions.assertNotNull;
9
+
10
+/**
11
+ * 复现 Redis 实际用法:valueSerializer 使用 Object.class。
12
+ */
13
+@DisplayName("FastJson2 Redis Object.class 反序列化")
14
+class FastJson2RedisReproduceTest
15
+{
16
+    private static final String USER_JSON =
17
+            "{\"@type\":\"com.ruoyi.common.core.domain.model.LoginUser\",\"browser\":\"Chrome 148\","
18
+                    + "\"deptId\":103L,\"expireTime\":1780482471190,\"ipaddr\":\"115.238.57.190\","
19
+                    + "\"loginLocation\":\"XX XX\",\"loginTime\":1780480671190,\"os\":\"Windows10\","
20
+                    + "\"permissions\":Set[\"*:*:*\"],\"token\":\"96dc6244-2273-489d-865f-36da1c8999bc\","
21
+                    + "\"user\":{\"createBy\":\"admin\",\"createTime\":\"2026-05-09 10:26:46\","
22
+                    + "\"delFlag\":\"0\",\"dept\":{\"ancestors\":\"0,100,101\",\"children\":[],"
23
+                    + "\"deptId\":103L,\"deptName\":\"研发部门\",\"leader\":\"若依\",\"orderNum\":1,"
24
+                    + "\"params\":{\"@type\":\"java.util.HashMap\"},\"parentId\":101L,\"status\":\"0\"},"
25
+                    + "\"deptId\":103L,\"email\":\"ry@163.com\",\"loginDate\":\"2026-06-03 17:44:28\","
26
+                    + "\"loginIp\":\"115.238.57.190\",\"nickName\":\"超级管理员\","
27
+                    + "\"params\":{\"@type\":\"java.util.HashMap\"},\"phonenumber\":\"15888888888\","
28
+                    + "\"pwdUpdateDate\":\"2026-05-09 10:26:46\",\"remark\":\"管理员\","
29
+                    + "\"roles\":[{\"dataScope\":\"1\",\"deptCheckStrictly\":false,\"flag\":false,"
30
+                    + "\"menuCheckStrictly\":false,\"params\":{\"@type\":\"java.util.HashMap\"},"
31
+                    + "\"roleId\":1L,\"roleKey\":\"admin\",\"roleName\":\"超级管理员\",\"roleSort\":1,"
32
+                    + "\"status\":\"0\"}],\"sex\":\"1\",\"status\":\"0\",\"userId\":1L,\"userName\":\"admin\"},"
33
+                    + "\"userId\":1L,\"username\":\"admin\"}";
34
+
35
+    @Test
36
+    @DisplayName("Object.class 与 RedisConfig 一致")
37
+    void deserializeAsObjectClass()
38
+    {
39
+        FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
40
+        Object raw = assertDoesNotThrow(() -> serializer.deserialize(USER_JSON.getBytes(FastJson2JsonRedisSerializer.DEFAULT_CHARSET)));
41
+        assertNotNull(raw);
42
+        assertNotNull(((LoginUser) raw).getUser());
43
+    }
44
+}

+ 62 - 0
baqing-admin/src/test/java/com/ruoyi/framework/web/service/LoginUserRedisStoreJacksonTest.java

@@ -0,0 +1,62 @@
1
+package com.ruoyi.framework.web.service;
2
+
3
+import static org.junit.jupiter.api.Assertions.assertEquals;
4
+import static org.junit.jupiter.api.Assertions.assertNotNull;
5
+import static org.junit.jupiter.api.Assertions.assertNull;
6
+
7
+import java.nio.charset.StandardCharsets;
8
+
9
+import com.fasterxml.jackson.databind.DeserializationFeature;
10
+import com.fasterxml.jackson.databind.ObjectMapper;
11
+import com.ruoyi.common.core.domain.entity.SysRole;
12
+import com.ruoyi.common.core.domain.entity.SysUser;
13
+import com.ruoyi.common.core.domain.model.LoginUser;
14
+import org.junit.jupiter.api.DisplayName;
15
+import org.junit.jupiter.api.Test;
16
+
17
+@DisplayName("LoginUserRedisStore Jackson 序列化")
18
+class LoginUserRedisStoreJacksonTest
19
+{
20
+    private static final String LEGACY_FASTJSON =
21
+            "{\"@type\":\"com.ruoyi.common.core.domain.model.LoginUser\",\"deptId\":103,"
22
+                    + "\"permissions\":[\"*:*:*\"],\"token\":\"t1\","
23
+                    + "\"user\":{\"userId\":1,\"userName\":\"admin\","
24
+                    + "\"roles\":[{\"roleId\":1,\"roleKey\":\"admin\",\"flag\":false,"
25
+                    + "\"deptCheckStrictly\":false,\"menuCheckStrictly\":false}]},"
26
+                    + "\"userId\":1,\"username\":\"admin\"}";
27
+
28
+    @Test
29
+    @DisplayName("Jackson 读写含 roles 的 LoginUser")
30
+    void jacksonRoundTrip() throws Exception
31
+    {
32
+        ObjectMapper mapper = new ObjectMapper();
33
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
34
+
35
+        SysRole role = new SysRole();
36
+        role.setRoleId(1L);
37
+        role.setRoleKey("admin");
38
+        role.setFlag(false);
39
+        SysUser user = new SysUser(1L);
40
+        user.setUserName("admin");
41
+        user.setRoles(java.util.Collections.singletonList(role));
42
+        LoginUser loginUser = new LoginUser(1L, 103L, user, java.util.Collections.singleton("*:*:*"));
43
+        loginUser.setToken("t1");
44
+
45
+        byte[] bytes = mapper.writeValueAsBytes(loginUser);
46
+        LoginUser restored = mapper.readValue(bytes, LoginUser.class);
47
+        assertNotNull(restored.getUser());
48
+        assertEquals("admin", restored.getUser().getUserName());
49
+        assertEquals(1, restored.getUser().getRoles().size());
50
+    }
51
+
52
+    @Test
53
+    @DisplayName("Legacy FastJSON 含 flag:false 可被 Jackson 忽略未知字段策略兼容读取")
54
+    void jacksonIgnoresUnknownFromPlainJson() throws Exception
55
+    {
56
+        ObjectMapper mapper = new ObjectMapper();
57
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
58
+        LoginUser loginUser = mapper.readValue(LEGACY_FASTJSON.getBytes(StandardCharsets.UTF_8), LoginUser.class);
59
+        assertNotNull(loginUser);
60
+        assertEquals("admin", loginUser.getUser().getUserName());
61
+    }
62
+}

+ 21 - 1
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java

@@ -7,6 +7,8 @@ import javax.validation.constraints.Size;
7
 import org.apache.commons.lang3.builder.ToStringBuilder;
7
 import org.apache.commons.lang3.builder.ToStringBuilder;
8
 import org.apache.commons.lang3.builder.ToStringStyle;
8
 import org.apache.commons.lang3.builder.ToStringStyle;
9
 import com.alibaba.fastjson2.annotation.JSONField;
9
 import com.alibaba.fastjson2.annotation.JSONField;
10
+import com.fasterxml.jackson.annotation.JsonIgnore;
11
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
10
 import com.ruoyi.common.annotation.Excel;
12
 import com.ruoyi.common.annotation.Excel;
11
 import com.ruoyi.common.annotation.Excel.ColumnType;
13
 import com.ruoyi.common.annotation.Excel.ColumnType;
12
 import com.ruoyi.common.core.domain.BaseEntity;
14
 import com.ruoyi.common.core.domain.BaseEntity;
@@ -16,6 +18,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
16
  * 
18
  * 
17
  * @author ruoyi
19
  * @author ruoyi
18
  */
20
  */
21
+@JsonIgnoreProperties(ignoreUnknown = true)
19
 public class SysRole extends BaseEntity
22
 public class SysRole extends BaseEntity
20
 {
23
 {
21
     private static final long serialVersionUID = 1L;
24
     private static final long serialVersionUID = 1L;
@@ -41,9 +44,11 @@ public class SysRole extends BaseEntity
41
     private String dataScope;
44
     private String dataScope;
42
 
45
 
43
     /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */
46
     /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */
47
+    @JSONField(serialize = false, deserialize = false)
44
     private boolean menuCheckStrictly;
48
     private boolean menuCheckStrictly;
45
 
49
 
46
     /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */
50
     /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */
51
+    @JSONField(serialize = false, deserialize = false)
47
     private boolean deptCheckStrictly;
52
     private boolean deptCheckStrictly;
48
 
53
 
49
     /** 角色状态(0正常 1停用) */
54
     /** 角色状态(0正常 1停用) */
@@ -53,7 +58,8 @@ public class SysRole extends BaseEntity
53
     /** 删除标志(0代表存在 2代表删除) */
58
     /** 删除标志(0代表存在 2代表删除) */
54
     private String delFlag;
59
     private String delFlag;
55
 
60
 
56
-    /** 用户是否存在此角色标识 默认不存在 */
61
+    /** 用户是否存在此角色标识 默认不存在(仅前端角色分配 UI 使用,不入 Redis) */
62
+    @JSONField(serialize = false, deserialize = false)
57
     private boolean flag = false;
63
     private boolean flag = false;
58
 
64
 
59
     /** 菜单组 */
65
     /** 菜单组 */
@@ -86,11 +92,19 @@ public class SysRole extends BaseEntity
86
     }
92
     }
87
 
93
 
88
     @JSONField(serialize = false, deserialize = false)
94
     @JSONField(serialize = false, deserialize = false)
95
+    @JsonIgnore
89
     public boolean isAdmin()
96
     public boolean isAdmin()
90
     {
97
     {
91
         return isAdmin(this.roleId);
98
         return isAdmin(this.roleId);
92
     }
99
     }
93
 
100
 
101
+    /** 兼容历史 Redis 缓存中的 admin 字段,反序列化时忽略 */
102
+    @JSONField(serialize = false, deserialize = false)
103
+    @JsonIgnore
104
+    public void setAdmin(boolean admin)
105
+    {
106
+    }
107
+
94
     public static boolean isAdmin(Long roleId)
108
     public static boolean isAdmin(Long roleId)
95
     {
109
     {
96
         return roleId != null && 1L == roleId;
110
         return roleId != null && 1L == roleId;
@@ -141,21 +155,25 @@ public class SysRole extends BaseEntity
141
         this.dataScope = dataScope;
155
         this.dataScope = dataScope;
142
     }
156
     }
143
 
157
 
158
+    @JsonIgnore
144
     public boolean isMenuCheckStrictly()
159
     public boolean isMenuCheckStrictly()
145
     {
160
     {
146
         return menuCheckStrictly;
161
         return menuCheckStrictly;
147
     }
162
     }
148
 
163
 
164
+    @JsonIgnore
149
     public void setMenuCheckStrictly(boolean menuCheckStrictly)
165
     public void setMenuCheckStrictly(boolean menuCheckStrictly)
150
     {
166
     {
151
         this.menuCheckStrictly = menuCheckStrictly;
167
         this.menuCheckStrictly = menuCheckStrictly;
152
     }
168
     }
153
 
169
 
170
+    @JsonIgnore
154
     public boolean isDeptCheckStrictly()
171
     public boolean isDeptCheckStrictly()
155
     {
172
     {
156
         return deptCheckStrictly;
173
         return deptCheckStrictly;
157
     }
174
     }
158
 
175
 
176
+    @JsonIgnore
159
     public void setDeptCheckStrictly(boolean deptCheckStrictly)
177
     public void setDeptCheckStrictly(boolean deptCheckStrictly)
160
     {
178
     {
161
         this.deptCheckStrictly = deptCheckStrictly;
179
         this.deptCheckStrictly = deptCheckStrictly;
@@ -181,11 +199,13 @@ public class SysRole extends BaseEntity
181
         this.delFlag = delFlag;
199
         this.delFlag = delFlag;
182
     }
200
     }
183
 
201
 
202
+    @JsonIgnore
184
     public boolean isFlag()
203
     public boolean isFlag()
185
     {
204
     {
186
         return flag;
205
         return flag;
187
     }
206
     }
188
 
207
 
208
+    @JsonIgnore
189
     public void setFlag(boolean flag)
209
     public void setFlag(boolean flag)
190
     {
210
     {
191
         this.flag = flag;
211
         this.flag = flag;

+ 10 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java

@@ -6,6 +6,8 @@ import javax.validation.constraints.*;
6
 import org.apache.commons.lang3.builder.ToStringBuilder;
6
 import org.apache.commons.lang3.builder.ToStringBuilder;
7
 import org.apache.commons.lang3.builder.ToStringStyle;
7
 import org.apache.commons.lang3.builder.ToStringStyle;
8
 import com.fasterxml.jackson.annotation.JsonFormat;
8
 import com.fasterxml.jackson.annotation.JsonFormat;
9
+import com.fasterxml.jackson.annotation.JsonIgnore;
10
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
9
 import com.fasterxml.jackson.annotation.JsonProperty;
11
 import com.fasterxml.jackson.annotation.JsonProperty;
10
 import com.ruoyi.common.annotation.Excel;
12
 import com.ruoyi.common.annotation.Excel;
11
 import com.ruoyi.common.annotation.Excel.ColumnType;
13
 import com.ruoyi.common.annotation.Excel.ColumnType;
@@ -21,6 +23,7 @@ import com.ruoyi.common.xss.Xss;
21
  * 
23
  * 
22
  * @author ruoyi
24
  * @author ruoyi
23
  */
25
  */
26
+@JsonIgnoreProperties(ignoreUnknown = true)
24
 public class SysUser extends BaseEntity
27
 public class SysUser extends BaseEntity
25
 {
28
 {
26
     private static final long serialVersionUID = 1L;
29
     private static final long serialVersionUID = 1L;
@@ -118,11 +121,18 @@ public class SysUser extends BaseEntity
118
     }
121
     }
119
 
122
 
120
     @JSONField(serialize = false, deserialize = false)
123
     @JSONField(serialize = false, deserialize = false)
124
+    @JsonIgnore
121
     public boolean isAdmin()
125
     public boolean isAdmin()
122
     {
126
     {
123
         return SecurityUtils.isAdmin(this.userId);
127
         return SecurityUtils.isAdmin(this.userId);
124
     }
128
     }
125
 
129
 
130
+    /** 兼容历史 Redis 缓存中的 admin 字段,反序列化时忽略 */
131
+    @JSONField(serialize = false, deserialize = false)
132
+    public void setAdmin(boolean admin)
133
+    {
134
+    }
135
+
126
     public Long getDeptId()
136
     public Long getDeptId()
127
     {
137
     {
128
         return deptId;
138
         return deptId;

+ 6 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java

@@ -1,6 +1,7 @@
1
 package com.ruoyi.common.core.domain.model;
1
 package com.ruoyi.common.core.domain.model;
2
 
2
 
3
 import com.alibaba.fastjson2.annotation.JSONField;
3
 import com.alibaba.fastjson2.annotation.JSONField;
4
+import com.fasterxml.jackson.annotation.JsonIgnore;
4
 import com.ruoyi.common.core.domain.entity.SysUser;
5
 import com.ruoyi.common.core.domain.entity.SysUser;
5
 import org.springframework.security.core.GrantedAuthority;
6
 import org.springframework.security.core.GrantedAuthority;
6
 import org.springframework.security.core.userdetails.UserDetails;
7
 import org.springframework.security.core.userdetails.UserDetails;
@@ -120,6 +121,7 @@ public class LoginUser implements UserDetails
120
     }
121
     }
121
 
122
 
122
     @JSONField(serialize = false)
123
     @JSONField(serialize = false)
124
+    @JsonIgnore
123
     @Override
125
     @Override
124
     public String getPassword()
126
     public String getPassword()
125
     {
127
     {
@@ -136,6 +138,7 @@ public class LoginUser implements UserDetails
136
      * 账户是否未过期,过期无法验证
138
      * 账户是否未过期,过期无法验证
137
      */
139
      */
138
     @JSONField(serialize = false)
140
     @JSONField(serialize = false)
141
+    @JsonIgnore
139
     @Override
142
     @Override
140
     public boolean isAccountNonExpired()
143
     public boolean isAccountNonExpired()
141
     {
144
     {
@@ -148,6 +151,7 @@ public class LoginUser implements UserDetails
148
      * @return
151
      * @return
149
      */
152
      */
150
     @JSONField(serialize = false)
153
     @JSONField(serialize = false)
154
+    @JsonIgnore
151
     @Override
155
     @Override
152
     public boolean isAccountNonLocked()
156
     public boolean isAccountNonLocked()
153
     {
157
     {
@@ -160,6 +164,7 @@ public class LoginUser implements UserDetails
160
      * @return
164
      * @return
161
      */
165
      */
162
     @JSONField(serialize = false)
166
     @JSONField(serialize = false)
167
+    @JsonIgnore
163
     @Override
168
     @Override
164
     public boolean isCredentialsNonExpired()
169
     public boolean isCredentialsNonExpired()
165
     {
170
     {
@@ -172,6 +177,7 @@ public class LoginUser implements UserDetails
172
      * @return
177
      * @return
173
      */
178
      */
174
     @JSONField(serialize = false)
179
     @JSONField(serialize = false)
180
+    @JsonIgnore
175
     @Override
181
     @Override
176
     public boolean isEnabled()
182
     public boolean isEnabled()
177
     {
183
     {

+ 4 - 2
ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java

@@ -43,11 +43,13 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
43
         if (StringUtils.isNotNull(loginUser))
43
         if (StringUtils.isNotNull(loginUser))
44
         {
44
         {
45
             String userName = loginUser.getUsername();
45
             String userName = loginUser.getUsername();
46
-            // 删除用户缓存记录
47
             tokenService.delLoginUser(loginUser.getToken());
46
             tokenService.delLoginUser(loginUser.getToken());
48
-            // 记录用户退出日志
49
             AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));
47
             AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));
50
         }
48
         }
49
+        else
50
+        {
51
+            tokenService.clearLoginCache(request);
52
+        }
51
         ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success"))));
53
         ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success"))));
52
     }
54
     }
53
 }
55
 }

+ 132 - 0
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/LoginUserRedisStore.java

@@ -0,0 +1,132 @@
1
+package com.ruoyi.framework.web.service;
2
+
3
+import java.nio.charset.StandardCharsets;
4
+
5
+import org.slf4j.Logger;
6
+import org.slf4j.LoggerFactory;
7
+import org.springframework.data.redis.core.RedisCallback;
8
+import org.springframework.data.redis.core.RedisTemplate;
9
+import org.springframework.stereotype.Component;
10
+import com.alibaba.fastjson2.JSON;
11
+import com.alibaba.fastjson2.JSONReader;
12
+import com.alibaba.fastjson2.filter.Filter;
13
+import com.fasterxml.jackson.databind.DeserializationFeature;
14
+import com.fasterxml.jackson.databind.ObjectMapper;
15
+import com.ruoyi.common.constant.Constants;
16
+import com.ruoyi.common.core.domain.model.LoginUser;
17
+import com.ruoyi.common.utils.StringUtils;
18
+
19
+/**
20
+ * 登录用户 Redis 存储:使用 Jackson 原始 JSON 字节,避免 FastJSON2 反序列化 LoginUser 嵌套对象异常。
21
+ */
22
+@Component
23
+public class LoginUserRedisStore
24
+{
25
+    private static final Logger log = LoggerFactory.getLogger(LoginUserRedisStore.class);
26
+
27
+    private static final Filter LEGACY_AUTO_TYPE_FILTER =
28
+            JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
29
+
30
+    private final RedisTemplate<Object, Object> redisTemplate;
31
+
32
+    private final ObjectMapper objectMapper;
33
+
34
+    public LoginUserRedisStore(RedisTemplate<Object, Object> redisTemplate)
35
+    {
36
+        this.redisTemplate = redisTemplate;
37
+        this.objectMapper = new ObjectMapper();
38
+        this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
39
+    }
40
+
41
+    public void save(String key, LoginUser loginUser, long timeoutMinutes)
42
+    {
43
+        if (StringUtils.isEmpty(key) || loginUser == null)
44
+        {
45
+            return;
46
+        }
47
+        try
48
+        {
49
+            byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
50
+            byte[] valueBytes = objectMapper.writeValueAsBytes(loginUser);
51
+            long seconds = Math.max(timeoutMinutes * 60L, 1L);
52
+            redisTemplate.execute((RedisCallback<Void>) connection -> {
53
+                connection.setEx(keyBytes, seconds, valueBytes);
54
+                return null;
55
+            });
56
+        }
57
+        catch (Exception e)
58
+        {
59
+            throw new IllegalStateException("登录缓存写入失败", e);
60
+        }
61
+    }
62
+
63
+    public LoginUser load(String key)
64
+    {
65
+        if (StringUtils.isEmpty(key))
66
+        {
67
+            return null;
68
+        }
69
+        byte[] valueBytes = readRawBytes(key);
70
+        if (valueBytes == null || valueBytes.length == 0)
71
+        {
72
+            return null;
73
+        }
74
+        LoginUser loginUser = readJackson(valueBytes);
75
+        if (loginUser != null)
76
+        {
77
+            return loginUser;
78
+        }
79
+        loginUser = readLegacyFastJson(valueBytes);
80
+        if (loginUser != null)
81
+        {
82
+            log.info("已兼容读取 FastJSON 格式登录缓存,key={}", key);
83
+            return loginUser;
84
+        }
85
+        log.warn("登录缓存损坏,已删除 key={}", key);
86
+        delete(key);
87
+        return null;
88
+    }
89
+
90
+    public void delete(String key)
91
+    {
92
+        if (StringUtils.isNotEmpty(key))
93
+        {
94
+            redisTemplate.delete(key);
95
+        }
96
+    }
97
+
98
+    private byte[] readRawBytes(String key)
99
+    {
100
+        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
101
+        return redisTemplate.execute((RedisCallback<byte[]>) connection -> connection.get(keyBytes));
102
+    }
103
+
104
+    private LoginUser readJackson(byte[] bytes)
105
+    {
106
+        try
107
+        {
108
+            return objectMapper.readValue(bytes, LoginUser.class);
109
+        }
110
+        catch (Exception ignored)
111
+        {
112
+            return null;
113
+        }
114
+    }
115
+
116
+    private LoginUser readLegacyFastJson(byte[] bytes)
117
+    {
118
+        try
119
+        {
120
+            String str = new String(bytes, StandardCharsets.UTF_8).trim();
121
+            if (str.startsWith("\"") && str.endsWith("\""))
122
+            {
123
+                str = objectMapper.readValue(str, String.class);
124
+            }
125
+            return JSON.parseObject(str, LoginUser.class, LEGACY_AUTO_TYPE_FILTER);
126
+        }
127
+        catch (Exception ignored)
128
+        {
129
+            return null;
130
+        }
131
+    }
132
+}

+ 50 - 19
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java

@@ -3,7 +3,6 @@ package com.ruoyi.framework.web.service;
3
 import java.util.Collection;
3
 import java.util.Collection;
4
 import java.util.HashMap;
4
 import java.util.HashMap;
5
 import java.util.Map;
5
 import java.util.Map;
6
-import java.util.concurrent.TimeUnit;
7
 
6
 
8
 import org.slf4j.Logger;
7
 import org.slf4j.Logger;
9
 import org.slf4j.LoggerFactory;
8
 import org.slf4j.LoggerFactory;
@@ -56,6 +55,9 @@ public class TokenService
56
     @Autowired
55
     @Autowired
57
     private RedisCache redisCache;
56
     private RedisCache redisCache;
58
 
57
 
58
+    @Autowired
59
+    private LoginUserRedisStore loginUserRedisStore;
60
+
59
     /**
61
     /**
60
      * 获取用户身份信息
62
      * 获取用户身份信息
61
      * 
63
      * 
@@ -63,24 +65,42 @@ public class TokenService
63
      */
65
      */
64
     public LoginUser getLoginUser(HttpServletRequest request)
66
     public LoginUser getLoginUser(HttpServletRequest request)
65
     {
67
     {
66
-        // 获取请求携带的令牌
67
-        String token = getToken(request);
68
-        if (StringUtils.isNotEmpty(token))
68
+        String jwt = getToken(request);
69
+        if (StringUtils.isEmpty(jwt))
69
         {
70
         {
70
-            try
71
-            {
72
-                Claims claims = parseToken(token);
73
-                // 解析对应的权限以及用户信息
74
-                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
75
-                String userKey = getTokenKey(uuid);
76
-                return redisCache.getCacheObject(userKey);
77
-            }
78
-            catch (Exception e)
71
+            return null;
72
+        }
73
+        try
74
+        {
75
+            Claims claims = parseToken(jwt);
76
+            String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
77
+            return loginUserRedisStore.load(getTokenKey(uuid));
78
+        }
79
+        catch (Exception e)
80
+        {
81
+            log.error("获取用户信息异常'{}'", e.getMessage());
82
+            return null;
83
+        }
84
+    }
85
+
86
+    /**
87
+     * 反序列化失败时清理损坏的登录缓存,避免退出/鉴权反复报错。
88
+     */
89
+    private void deleteLoginCacheByJwt(String jwt)
90
+    {
91
+        try
92
+        {
93
+            Claims claims = parseToken(jwt);
94
+            String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
95
+            if (StringUtils.isNotEmpty(uuid))
79
             {
96
             {
80
-                log.error("获取用户信息异常'{}'", e.getMessage());
97
+                loginUserRedisStore.delete(getTokenKey(uuid));
81
             }
98
             }
82
         }
99
         }
83
-        return null;
100
+        catch (Exception ignored)
101
+        {
102
+            // ignore
103
+        }
84
     }
104
     }
85
 
105
 
86
     /**
106
     /**
@@ -101,8 +121,19 @@ public class TokenService
101
     {
121
     {
102
         if (StringUtils.isNotEmpty(token))
122
         if (StringUtils.isNotEmpty(token))
103
         {
123
         {
104
-            String userKey = getTokenKey(token);
105
-            redisCache.deleteObject(userKey);
124
+            loginUserRedisStore.delete(getTokenKey(token));
125
+        }
126
+    }
127
+
128
+    /**
129
+     * 退出时若无法反序列化 LoginUser,仍按 JWT 清理 Redis 登录缓存。
130
+     */
131
+    public void clearLoginCache(HttpServletRequest request)
132
+    {
133
+        String jwt = getToken(request);
134
+        if (StringUtils.isNotEmpty(jwt))
135
+        {
136
+            deleteLoginCacheByJwt(jwt);
106
         }
137
         }
107
     }
138
     }
108
 
139
 
@@ -152,7 +183,7 @@ public class TokenService
152
         loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
183
         loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
153
         // 根据uuid将loginUser缓存
184
         // 根据uuid将loginUser缓存
154
         String userKey = getTokenKey(loginUser.getToken());
185
         String userKey = getTokenKey(loginUser.getToken());
155
-        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
186
+        loginUserRedisStore.save(userKey, loginUser, expireTime);
156
     }
187
     }
157
 
188
 
158
     /**
189
     /**
@@ -248,7 +279,7 @@ public class TokenService
248
         }
279
         }
249
         for (String key : keys)
280
         for (String key : keys)
250
         {
281
         {
251
-            LoginUser loginUser = redisCache.getCacheObject(key);
282
+            LoginUser loginUser = loginUserRedisStore.load(key);
252
             if (loginUser == null || loginUser.getUser() == null || loginUser.getUser().isAdmin())
283
             if (loginUser == null || loginUser.getUser() == null || loginUser.getUser().isAdmin())
253
             {
284
             {
254
                 // 管理员拥有所有权限,跳过
285
                 // 管理员拥有所有权限,跳过