Selaa lähdekoodia

会员管理代码

wwh 1 viikko sitten
vanhempi
commit
09335af39a

+ 6 - 2
baqing-shop/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
 }

+ 1 - 1
baqing-shop/src/main/resources/logback.xml

@@ -1,7 +1,7 @@
1 1
 <?xml version="1.0" encoding="UTF-8"?>
2 2
 <configuration>
3 3
     <!-- 日志存放路径 -->
4
-	<property name="log.path" value="/home/ruoyi/logs" />
4
+	<property name="log.path" value="/home/baqingShop/logs" />
5 5
     <!-- 日志输出格式 -->
6 6
 	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
7 7
 

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

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

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

@@ -6,12 +6,15 @@ 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;
12 14
 import com.ruoyi.common.annotation.Excel.Type;
13 15
 import com.ruoyi.common.annotation.Excels;
14 16
 import com.ruoyi.common.core.domain.BaseEntity;
17
+import com.alibaba.fastjson2.annotation.JSONField;
15 18
 import com.ruoyi.common.utils.SecurityUtils;
16 19
 import com.ruoyi.common.xss.Xss;
17 20
 
@@ -20,6 +23,7 @@ import com.ruoyi.common.xss.Xss;
20 23
  * 
21 24
  * @author ruoyi
22 25
  */
26
+@JsonIgnoreProperties(ignoreUnknown = true)
23 27
 public class SysUser extends BaseEntity
24 28
 {
25 29
     private static final long serialVersionUID = 1L;
@@ -116,11 +120,19 @@ public class SysUser extends BaseEntity
116 120
         this.userId = userId;
117 121
     }
118 122
 
123
+    @JSONField(serialize = false, deserialize = false)
124
+    @JsonIgnore
119 125
     public boolean isAdmin()
120 126
     {
121 127
         return SecurityUtils.isAdmin(this.userId);
122 128
     }
123 129
 
130
+    /** 兼容历史 Redis 缓存中的 admin 字段,反序列化时忽略 */
131
+    @JSONField(serialize = false, deserialize = false)
132
+    public void setAdmin(boolean admin)
133
+    {
134
+    }
135
+
124 136
     public Long getDeptId()
125 137
     {
126 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
     {

+ 1 - 1
ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java

@@ -47,6 +47,6 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
47 47
         }
48 48
         String str = new String(bytes, DEFAULT_CHARSET);
49 49
 
50
-        return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER, JSONReader.Feature.SupportAutoType);
50
+        return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
51 51
     }
52 52
 }

+ 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
+}

+ 45 - 37
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java

@@ -3,13 +3,12 @@ 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;
6
+
7 7
 import org.slf4j.Logger;
8 8
 import org.slf4j.LoggerFactory;
9 9
 import org.springframework.beans.factory.annotation.Autowired;
10 10
 import org.springframework.beans.factory.annotation.Value;
11 11
 import org.springframework.stereotype.Component;
12
-import com.alibaba.fastjson2.JSONObject;
13 12
 import com.ruoyi.common.constant.CacheConstants;
14 13
 import com.ruoyi.common.constant.Constants;
15 14
 import com.ruoyi.common.core.domain.model.LoginUser;
@@ -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,25 +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
         {
71
+            return null;
72
+        }
70 73
             try
71 74
             {
72
-                Claims claims = parseToken(token);
73
-                // 解析对应的权限以及用户信息
75
+            Claims claims = parseToken(jwt);
74 76
                 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
75
-                String userKey = getTokenKey(uuid);
76
-                LoginUser user = redisCache.getCacheObject(userKey);
77
-                return user;
77
+            return loginUserRedisStore.load(getTokenKey(uuid));
78 78
             }
79 79
             catch (Exception e)
80 80
             {
81 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))
96
+            {
97
+                loginUserRedisStore.delete(getTokenKey(uuid));
82 98
             }
83 99
         }
84
-        return null;
100
+        catch (Exception ignored)
101
+        {
102
+            // ignore
103
+        }
85 104
     }
86 105
 
87 106
     /**
@@ -102,8 +121,19 @@ public class TokenService
102 121
     {
103 122
         if (StringUtils.isNotEmpty(token))
104 123
         {
105
-            String userKey = getTokenKey(token);
106
-            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);
107 137
         }
108 138
     }
109 139
 
@@ -153,7 +183,7 @@ public class TokenService
153 183
         loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
154 184
         // 根据uuid将loginUser缓存
155 185
         String userKey = getTokenKey(loginUser.getToken());
156
-        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
186
+        loginUserRedisStore.save(userKey, loginUser, expireTime);
157 187
     }
158 188
 
159 189
     /**
@@ -232,28 +262,6 @@ public class TokenService
232 262
         return CacheConstants.LOGIN_TOKEN_KEY + uuid;
233 263
     }
234 264
 
235
-    /**
236
-     * 解析 Redis 中的在线用户缓存,兼容 fastjson2 反序列化为 JSONObject 的情况
237
-     */
238
-    private LoginUser resolveLoginUser(String key)
239
-    {
240
-        Object cached = redisCache.getCacheObject(key);
241
-        if (cached == null)
242
-        {
243
-            return null;
244
-        }
245
-        if (cached instanceof LoginUser)
246
-        {
247
-            return (LoginUser) cached;
248
-        }
249
-        if (cached instanceof JSONObject)
250
-        {
251
-            return ((JSONObject) cached).to(LoginUser.class);
252
-        }
253
-        log.warn("跳过无法识别的在线用户缓存: key={}, type={}", key, cached.getClass().getName());
254
-        return null;
255
-    }
256
-
257 265
     /**
258 266
      * 角色权限变更后,刷新所有持有该角色的在线用户权限
259 267
      *
@@ -271,7 +279,7 @@ public class TokenService
271 279
         }
272 280
         for (String key : keys)
273 281
         {
274
-            LoginUser loginUser = resolveLoginUser(key);
282
+            LoginUser loginUser = loginUserRedisStore.load(key);
275 283
             if (loginUser == null || loginUser.getUser() == null || loginUser.getUser().isAdmin())
276 284
             {
277 285
                 // 管理员拥有所有权限,跳过