Parcourir la Source

交易市场平台(供应商)

wwh il y a 2 semaines
Parent
commit
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 20
 import com.ruoyi.common.core.redis.RedisCache;
21 21
 import com.ruoyi.common.enums.BusinessType;
22 22
 import com.ruoyi.common.utils.StringUtils;
23
+import com.ruoyi.framework.web.service.LoginUserRedisStore;
23 24
 import com.ruoyi.system.domain.SysUserOnline;
24 25
 import com.ruoyi.system.service.ISysUserOnlineService;
25 26
 
@@ -38,6 +39,9 @@ public class SysUserOnlineController extends BaseController
38 39
     @Autowired
39 40
     private RedisCache redisCache;
40 41
 
42
+    @Autowired
43
+    private LoginUserRedisStore loginUserRedisStore;
44
+
41 45
     @PreAuthorize("@ss.hasPermi('monitor:online:list')")
42 46
     @GetMapping("/list")
43 47
     public TableDataInfo list(String ipaddr, String userName)
@@ -46,7 +50,7 @@ public class SysUserOnlineController extends BaseController
46 50
         List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
47 51
         for (String key : keys)
48 52
         {
49
-            LoginUser user = redisCache.getCacheObject(key);
53
+            LoginUser user = loginUserRedisStore.load(key);
50 54
             if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
51 55
             {
52 56
                 userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
@@ -77,7 +81,7 @@ public class SysUserOnlineController extends BaseController
77 81
     @DeleteMapping("/{tokenId}")
78 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 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 13
     private static final String LEGACY_LOGIN_USER_JSON =
14 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 19
                     + "\"delFlag\":\"0\",\"dept\":{\"ancestors\":\"0,100,101\",\"children\":[],"
20 20
                     + "\"deptId\":103L,\"deptName\":\"研发部门\",\"leader\":\"若依\",\"orderNum\":1,"
21 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 23
                     + "\"loginIp\":\"115.238.57.190\",\"nickName\":\"超级管理员\","
24 24
                     + "\"params\":{\"@type\":\"java.util.HashMap\"},\"phonenumber\":\"15888888888\","
25 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 30
                     + "\"userId\":1L,\"username\":\"admin\"}";
32 31
 
33 32
     @Test
34
-    @DisplayName("反序列化含 admin 字段的历史 LoginUser 缓存")
33
+    @DisplayName("反序列化含 admin/flag 字段的历史 LoginUser 缓存")
35 34
     void deserializeLegacyLoginUserWithAdminField()
36 35
     {
37 36
         FastJson2JsonRedisSerializer<LoginUser> serializer = new FastJson2JsonRedisSerializer<>(LoginUser.class);
@@ -40,6 +39,23 @@ class FastJson2JsonRedisSerializerTest
40 39
         assertEquals(1L, loginUser.getUserId());
41 40
         assertNotNull(loginUser.getUser());
42 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 7
 import org.apache.commons.lang3.builder.ToStringBuilder;
8 8
 import org.apache.commons.lang3.builder.ToStringStyle;
9 9
 import com.alibaba.fastjson2.annotation.JSONField;
10
+import com.fasterxml.jackson.annotation.JsonIgnore;
11
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
10 12
 import com.ruoyi.common.annotation.Excel;
11 13
 import com.ruoyi.common.annotation.Excel.ColumnType;
12 14
 import com.ruoyi.common.core.domain.BaseEntity;
@@ -16,6 +18,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
16 18
  * 
17 19
  * @author ruoyi
18 20
  */
21
+@JsonIgnoreProperties(ignoreUnknown = true)
19 22
 public class SysRole extends BaseEntity
20 23
 {
21 24
     private static final long serialVersionUID = 1L;
@@ -41,9 +44,11 @@ public class SysRole extends BaseEntity
41 44
     private String dataScope;
42 45
 
43 46
     /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */
47
+    @JSONField(serialize = false, deserialize = false)
44 48
     private boolean menuCheckStrictly;
45 49
 
46 50
     /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */
51
+    @JSONField(serialize = false, deserialize = false)
47 52
     private boolean deptCheckStrictly;
48 53
 
49 54
     /** 角色状态(0正常 1停用) */
@@ -53,7 +58,8 @@ public class SysRole extends BaseEntity
53 58
     /** 删除标志(0代表存在 2代表删除) */
54 59
     private String delFlag;
55 60
 
56
-    /** 用户是否存在此角色标识 默认不存在 */
61
+    /** 用户是否存在此角色标识 默认不存在(仅前端角色分配 UI 使用,不入 Redis) */
62
+    @JSONField(serialize = false, deserialize = false)
57 63
     private boolean flag = false;
58 64
 
59 65
     /** 菜单组 */
@@ -86,11 +92,19 @@ public class SysRole extends BaseEntity
86 92
     }
87 93
 
88 94
     @JSONField(serialize = false, deserialize = false)
95
+    @JsonIgnore
89 96
     public boolean isAdmin()
90 97
     {
91 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 108
     public static boolean isAdmin(Long roleId)
95 109
     {
96 110
         return roleId != null && 1L == roleId;
@@ -141,21 +155,25 @@ public class SysRole extends BaseEntity
141 155
         this.dataScope = dataScope;
142 156
     }
143 157
 
158
+    @JsonIgnore
144 159
     public boolean isMenuCheckStrictly()
145 160
     {
146 161
         return menuCheckStrictly;
147 162
     }
148 163
 
164
+    @JsonIgnore
149 165
     public void setMenuCheckStrictly(boolean menuCheckStrictly)
150 166
     {
151 167
         this.menuCheckStrictly = menuCheckStrictly;
152 168
     }
153 169
 
170
+    @JsonIgnore
154 171
     public boolean isDeptCheckStrictly()
155 172
     {
156 173
         return deptCheckStrictly;
157 174
     }
158 175
 
176
+    @JsonIgnore
159 177
     public void setDeptCheckStrictly(boolean deptCheckStrictly)
160 178
     {
161 179
         this.deptCheckStrictly = deptCheckStrictly;
@@ -181,11 +199,13 @@ public class SysRole extends BaseEntity
181 199
         this.delFlag = delFlag;
182 200
     }
183 201
 
202
+    @JsonIgnore
184 203
     public boolean isFlag()
185 204
     {
186 205
         return flag;
187 206
     }
188 207
 
208
+    @JsonIgnore
189 209
     public void setFlag(boolean flag)
190 210
     {
191 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 6
 import org.apache.commons.lang3.builder.ToStringBuilder;
7 7
 import org.apache.commons.lang3.builder.ToStringStyle;
8 8
 import com.fasterxml.jackson.annotation.JsonFormat;
9
+import com.fasterxml.jackson.annotation.JsonIgnore;
10
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
9 11
 import com.fasterxml.jackson.annotation.JsonProperty;
10 12
 import com.ruoyi.common.annotation.Excel;
11 13
 import com.ruoyi.common.annotation.Excel.ColumnType;
@@ -21,6 +23,7 @@ import com.ruoyi.common.xss.Xss;
21 23
  * 
22 24
  * @author ruoyi
23 25
  */
26
+@JsonIgnoreProperties(ignoreUnknown = true)
24 27
 public class SysUser extends BaseEntity
25 28
 {
26 29
     private static final long serialVersionUID = 1L;
@@ -118,11 +121,18 @@ public class SysUser extends BaseEntity
118 121
     }
119 122
 
120 123
     @JSONField(serialize = false, deserialize = false)
124
+    @JsonIgnore
121 125
     public boolean isAdmin()
122 126
     {
123 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 136
     public Long getDeptId()
127 137
     {
128 138
         return deptId;

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

@@ -1,6 +1,7 @@
1 1
 package com.ruoyi.common.core.domain.model;
2 2
 
3 3
 import com.alibaba.fastjson2.annotation.JSONField;
4
+import com.fasterxml.jackson.annotation.JsonIgnore;
4 5
 import com.ruoyi.common.core.domain.entity.SysUser;
5 6
 import org.springframework.security.core.GrantedAuthority;
6 7
 import org.springframework.security.core.userdetails.UserDetails;
@@ -120,6 +121,7 @@ public class LoginUser implements UserDetails
120 121
     }
121 122
 
122 123
     @JSONField(serialize = false)
124
+    @JsonIgnore
123 125
     @Override
124 126
     public String getPassword()
125 127
     {
@@ -136,6 +138,7 @@ public class LoginUser implements UserDetails
136 138
      * 账户是否未过期,过期无法验证
137 139
      */
138 140
     @JSONField(serialize = false)
141
+    @JsonIgnore
139 142
     @Override
140 143
     public boolean isAccountNonExpired()
141 144
     {
@@ -148,6 +151,7 @@ public class LoginUser implements UserDetails
148 151
      * @return
149 152
      */
150 153
     @JSONField(serialize = false)
154
+    @JsonIgnore
151 155
     @Override
152 156
     public boolean isAccountNonLocked()
153 157
     {
@@ -160,6 +164,7 @@ public class LoginUser implements UserDetails
160 164
      * @return
161 165
      */
162 166
     @JSONField(serialize = false)
167
+    @JsonIgnore
163 168
     @Override
164 169
     public boolean isCredentialsNonExpired()
165 170
     {
@@ -172,6 +177,7 @@ public class LoginUser implements UserDetails
172 177
      * @return
173 178
      */
174 179
     @JSONField(serialize = false)
180
+    @JsonIgnore
175 181
     @Override
176 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 43
         if (StringUtils.isNotNull(loginUser))
44 44
         {
45 45
             String userName = loginUser.getUsername();
46
-            // 删除用户缓存记录
47 46
             tokenService.delLoginUser(loginUser.getToken());
48
-            // 记录用户退出日志
49 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 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 3
 import java.util.Collection;
4 4
 import java.util.HashMap;
5 5
 import java.util.Map;
6
-import java.util.concurrent.TimeUnit;
7 6
 
8 7
 import org.slf4j.Logger;
9 8
 import org.slf4j.LoggerFactory;
@@ -56,6 +55,9 @@ public class TokenService
56 55
     @Autowired
57 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 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 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 183
         loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
153 184
         // 根据uuid将loginUser缓存
154 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 280
         for (String key : keys)
250 281
         {
251
-            LoginUser loginUser = redisCache.getCacheObject(key);
282
+            LoginUser loginUser = loginUserRedisStore.load(key);
252 283
             if (loginUser == null || loginUser.getUser() == null || loginUser.getUser().isAdmin())
253 284
             {
254 285
                 // 管理员拥有所有权限,跳过