用户管理与单点登录

  • 用户管理,用户登录,登录校验(界面+接口)

  • 用户管理功能
    用户表设计与持久层代码生成
    基本的增删改查
    用户名不能重复
    关于密码的两层加密处理:加密传输+加密存储
    重置密码

  • 登录功能
    前端登录界面
    后端登录接口
    登录成功后的处理
    退出登录

  • 登录校验
    接口登录校验
    界面登录校验

用户表设计与代码生成

1
2
3
4
5
6
7
8
9
10
-- 用户表
drop table if exists `user`;
create table `user` (
`id` bigint not null comment 'ID',
`login_name` varchar(50) not null comment '登陆名',
`name` varchar(50) comment '昵称',
`password` char(32) not null comment '密码',
primary key (`id`),
unique key `login_name_unique` (`login_name`)
) engine=innodb default charset=utf8mb4 comment='用户';

完成用户表基本增删改查功能

  • 按照电子书管理,复制出一套用户管理的代码

用户名重复校验与自定义异常

  • 自定义异常的使用
  • 统一异常处理可以处理内置异常,也可以处理自定义异常
  • 一般出现异常就表示程序(接口、线程)结束了,不要使用异常来处理逻辑,比如
    1
    2
    3
    4
    5
    try {
    insert
    } catch {
    update
    }
  • 使用!!user.id可以绕过前端类型校验

controller/ControllerExceptionHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import com.javami.wiki.exception.BusinessException;
import com.javami.wiki.resp.CommonResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* 统一异常处理、数据预处理等
*/
@ControllerAdvice
public class ControllerExceptionHandler {

private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);

/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = BindException.class)
@ResponseBody
public CommonResp validExceptionHandler(BindException e) {
CommonResp commonResp = new CommonResp();
LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
commonResp.setSuccess(false);
commonResp.setMessage(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return commonResp;
}

/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public CommonResp validExceptionHandler(BusinessException e) {
CommonResp commonResp = new CommonResp();
LOG.warn("业务异常:{}", e.getCode().getDesc());
commonResp.setSuccess(false);
commonResp.setMessage(e.getCode().getDesc());
return commonResp;
}

/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public CommonResp validExceptionHandler(Exception e) {
CommonResp commonResp = new CommonResp();
LOG.error("系统异常:", e);
commonResp.setSuccess(false);
commonResp.setMessage("系统出现异常,请联系管理员");
return commonResp;
}
}

exception/BusinessException.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class BusinessException extends RuntimeException{

private BusinessExceptionCode code;

public BusinessException (BusinessExceptionCode code) {
super(code.getDesc());
this.code = code;
}

public BusinessExceptionCode getCode() {
return code;
}

public void setCode(BusinessExceptionCode code) {
this.code = code;
}

/**
* 不写入堆栈信息,提高性能
*/
@Override
public Throwable fillInStackTrace() {
return this;
}
}

exception/BusinessExceptionCode.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum BusinessExceptionCode {

USER_LOGIN_NAME_EXIST("登录名已存在"),
;

private String desc;

BusinessExceptionCode(String desc) {
this.desc = desc;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}
}

修改的时候,设置user.setLoginName(null);用户名不允许修改

1
2
3
 // 更新
user.setLoginName(null);
userMapper.updateByPrimaryKeySelective(user);

关于密码的两层加密处理

  • 概念:MD5加密,盐值
  • 基本的密码安全性设计:加密传输 + 加密存储

md5.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
var KEY = "!@#QWERT";
/*
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */

/*
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
function hexMd5(s) {
return hex_md5(s);
}
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }

/*
* Perform a simple self-test to see if the VM is working
*/
function md5_vm_test()
{
return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}

/*
* Calculate the MD5 of an array of little-endian words, and a bit length
*/
function core_md5(x, len)
{
/* append padding */
x[len >> 5] |= 0x80 << ((len) % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;

var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;

for(var i = 0; i < x.length; i += 16)
{
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;

a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);

a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return Array(a, b, c, d);

}

/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5_cmn(q, a, b, x, s, t)
{
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
* Calculate the HMAC-MD5, of a key and some data
*/
function core_hmac_md5(key, data)
{
var bkey = str2binl(key);
if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);

var ipad = Array(16), opad = Array(16);
for(var i = 0; i < 16; i++)
{
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5C5C5C5C;
}

var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
return core_md5(opad.concat(hash), 512 + 128);
}

/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y)
{
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}

/*
* Bitwise rotate a 32-bit number to the left.
*/
function bit_rol(num, cnt)
{
return (num << cnt) | (num >>> (32 - cnt));
}

/*
* Convert a string to an array of little-endian words
* If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
*/
function str2binl(str)
{
var bin = Array();
var mask = (1 << chrsz) - 1;
for(var i = 0; i < str.length * chrsz; i += chrsz)
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
return bin;
}

/*
* Convert an array of little-endian words to a string
*/
function binl2str(bin)
{
var str = "";
var mask = (1 << chrsz) - 1;
for(var i = 0; i < bin.length * 32; i += chrsz)
str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
return str;
}

/*
* Convert an array of little-endian words to a hex string.
*/
function binl2hex(binarray)
{
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for(var i = 0; i < binarray.length * 4; i++)
{
str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
}
return str;
}

/*
* Convert an array of little-endian words to a base-64 string
*/
function binl2b64(binarray)
{
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var str = "";
for(var i = 0; i < binarray.length * 4; i += 3)
{
var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16)
| (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
| ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
for(var j = 0; j < 4; j++)
{
if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
}
}
return str;
}

增加重置密码功能

  • 修改用户时,不能修改密码
  • 单独开发重置密码表单和接口
1
2
3
4
// 更新
user.setLoginName(null);
user.setPassword(null);
userMapper.updateByPrimaryKeySelective(user);
1
2
3
<a-form-item label="密码" v-show="!user.id">
<a-input v-model:value="user.password" />
</a-form-item>

单点登录token与JWT介绍

登录

  • 前端输入用户名密码
  • 校验用户名密码
  • 生成token
  • 后端保存token(redis)
  • 前端保存token

校验

  • 前端请求时,带上token(放在header)
  • 登录拦截器,校验token(到redis获取token)
  • 校验成功则继续后面的业务
  • 校验失败则回到登录页面

单点登录系统

  • 淘宝 支付宝
  • A B C…
  • X:用户管理、登录、登录校验、退出登录

token与JWT

  • token+redis:token是无意义的
  • JWT:token是有意义的,加密的,包含业务信息,一般是用户信息,可以被解出来
  • 登录标识:就是令牌,就是token,就是一串唯一的字符串
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.5.0</version>
    </dependency>

JwtUtil.sign
JwtUtil.verity

登录功能开发

  • 后端增加登录接口
  • 前端增加登录模态框

登录成功处理&集成vuex

后端保存用户信息

  • 集成redis
  • 登录成功后,生成token,以token为key,以用户信息为value,放入redis中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@PostMapping("/login")
public CommonResp login(@Valid @RequestBody UserLoginReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp<UserLoginResp> resp = new CommonResp<>();
UserLoginResp userLoginResp = userService.login(req);


Long token = snowFlake.nextId();
LOG.info("生成单点登录token:{},并放入redis中", token);
userLoginResp.setToken(token.toString());
redisTemplate.opsForValue().set(token, JSONObject.toJSONString(userLoginResp), 3600 * 24, TimeUnit.SECONDS);
resp.setContent(userLoginResp);
return resp;
}

前端显示登录用户

  • header显示登录昵称
  • 使用vuex+sessionStorage保存登录信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createStore } from 'vuex'

const store = createStore({
state: {
user: {}
},
mutations: {
setUser (state, user) {
state.user = user;
}
},
actions: {
},
modules: {
}
});

export default store;

sessionStorage保存登录信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createStore } from 'vuex'

declare let SessionStorage: any;
const USER = "USER";

const store = createStore({
state: {
user: SessionStorage.get(USER) || {}
},
mutations: {
setUser (state, user) {
state.user = user;
SessionStorage.set(USER, user);
}
},
actions: {
},
modules: {
}
});

export default store;

集成Redis

  • 加入依赖
  • 配置Redis地址、端口、密码等
  • 使用RedisTemplate操作
1
2
3
4
# redis配置
spring.redis.host=r-uf6ljbcdaxobsifyctpd.redis.rds.aliyuncs.com
spring.redis.port=6379
spring.redis.password=Redis000

概念:序列化

  • DUBBO是RPC框架,就用到了序列化技术,比如:
  • 从A应用的类传到B应用去执行

使用vuex存储全局变量

增加退出登录功能

  • 目标:将token置为失效
  • 后端增加退出登录接口,退出后,清除redis用户信息
  • 前端增加退出登录按钮,退出后,清除前端用户信息

程序设计小技巧:只要有按钮(操作),就要考虑需不需要有确认

1
2
3
4
5
6
7
8
9
10
11
12
13
// 退出登录
const logout = () => {
console.log("退出登录开始");
axios.get('/user/logout/' + user.value.token).then((response) => {
const data = response.data;
if (data.success) {
message.success("退出登录成功!");
store.commit("setUser", {});
} else {
message.error(data.message);
}
});
};

后端接口增加登录校验

  • 每个接口个性化的参数,放在请求的body里。所有接口通用的参数,放在header里面,如token
  • 拦截器优势:可以针对URL来配置拦截规则
  • 拦截器配置的URL是可以模糊匹配的,如:/all/**
  • 拦截器指定URL&排除URL
    addPathPatterns
    excludePathPatterns
  • redis的key的类型,放进去和取出来要统一
    比如放进去是字符串”123”,要取出来用数值123是取不到的

后端增加拦截器,校验token有效性

config/SpringMvcConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.javami.wiki.config;

import com.javami.wiki.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

@Resource
LoginInterceptor loginInterceptor;

public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/test/**",
"/redis/**",
"/user/login",
"/category/all",
"/ebook/list",
"/doc/all/**",
"/doc/find-content/**"
);
}
}

interceptor/LoginInterceptor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.javami.wiki.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {

private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);

@Resource
private RedisTemplate redisTemplate;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LoginInterceptor 开始 -------------");
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);

// OPTIONS请求不做校验,
// 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
if(request.getMethod().toUpperCase().equals("OPTIONS")){
return true;
}

String path = request.getRequestURL().toString();
LOG.info("接口登录拦截:,path:{}", path);

//获取header的token参数
String token = request.getHeader("token");
LOG.info("登录校验开始,token:{}", token);
if (token == null || token.isEmpty()) {
LOG.info( "token为空,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
Object object = redisTemplate.opsForValue().get(token);
if (object == null) {
LOG.warn( "token无效,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
} else {
LOG.info("已登录:{}", object);
return true;
}
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// LOG.info("LogInterceptor 结束");
}
}

前端请求增加token参数
src/main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {Tool} from "@/util/tool";

/**
* axios拦截器
*/
axios.interceptors.request.use(function (config) {
const token = store.state.user.token;
if (Tool.isNotEmpty(token)) {
config.headers.token = token;
console.log("请求headers增加token:", token);
}
return config;
}, error => {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
console.log('返回结果:', response);
return response;
}, error => {
console.log('返回错误:', error);
return Promise.reject(error);
});

前端界面增加登录校验

未登录时,管理菜单要隐藏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<a-menu-item key="/">
<router-link to="/">首页</router-link>
</a-menu-item>
<a-menu-item key="/admin/user" :style="user.id? {} : {display:'none'}">
<router-link to="/admin/user">用户管理</router-link>
</a-menu-item>
<a-menu-item key="/admin/ebook" :style="user.id? {} : {display:'none'}">
<router-link to="/admin/ebook">电子书管理</router-link>
</a-menu-item>
<a-menu-item key="/admin/category" :style="user.id? {} : {display:'none'}">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
  • 对路由做判断,防止用户通过手敲url访问管理页面
  • 使用meta自定义路由属性
  • 前端路由拦截器的写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/home.vue'
import About from '../views/about.vue'
import Doc from '../views/doc.vue'
import AdminUser from '../views/admin/admin-user.vue'
import AdminEbook from '../views/admin/admin-ebook.vue'
import AdminCategory from '../views/admin/admin-category.vue'
import AdminDoc from '../views/admin/admin-doc.vue'
import store from "@/store";
import {Tool} from "@/util/tool";

const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/doc',
name: 'Doc',
component: Doc
},
{
path: '/about',
name: 'About',
component: About
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "about" */ '../views/about.vue')
},
{
path: '/admin/user',
name: 'AdminUser',
component: AdminUser,
meta: {
loginRequire: true
}
},
{
path: '/admin/ebook',
name: 'AdminEbook',
component: AdminEbook,
meta: {
loginRequire: true
}
},
{
path: '/admin/category',
name: 'AdminCategory',
component: AdminCategory,
meta: {
loginRequire: true
}
},
{
path: '/admin/doc',
name: 'AdminDoc',
component: AdminDoc,
meta: {
loginRequire: true
}
},
]

const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})

// 路由登录拦截
router.beforeEach((to, from, next) => {
// 要不要对meta.loginRequire属性做监控拦截
if (to.matched.some(function (item) {
console.log(item, "是否需要登录校验:", item.meta.loginRequire);
return item.meta.loginRequire
})) {
const loginUser = store.state.user;
if (Tool.isEmpty(loginUser)) {
console.log("用户未登录!");
next('/');
} else {
next();
}
} else {
next();
}
});

export default router

逻辑:在每一次的跳转之前,to:新路由,from:旧路由,next:是一个方法。

1
2
3
4
if (to.matched.some(function (item) {
console.log(item, "是否需要登录校验:", item.meta.loginRequire);
return item.meta.loginRequire
})) {

如果返回的是True,说明我要做路由校验,往下走!判断loginUser是否为空,如果为空,就表示用户没有登录,直接跳转回首页,如果已经登录,那么就往后面的逻辑走。

1
2
3
else{
//否则就是继续走旧的逻辑
}

用户密码初始化

前端校验与后端校验

  • 后端校验:必须,防止别人绕过界面直接调用接口。
  • 前端校验:非必须,可减少服务器压力

Ant Design Vue表单校验

  • https://2x.antdv.com/components/form-cn


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!