最近接触了一下nestJS框架,记录一个典型的登录逻辑应该如何做。

Auth.module.ts

整体的module设计是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Module({
imports: [
UsersModule,
ConfigModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const secret = configService.get<string>('JWT_SECRET', 'defaultSecretKey');

return {
secret,
signOptions: { expiresIn: '7d' },
};
},
}),
],
providers: [AuthService, JwtStrategy, CaptchaService],
controllers: [AuthController, CaptchaController],
exports: [AuthService, CaptchaService],
})
export class AuthModule { }
  • AuthController是用来接收login和register请求的。而AuthService用来执行登录或者注册的实际操作。
  • 其中的captchaModule负责提供验证码。captchaController提供路由可以获取验证码图片,captureService负责生成验证码图片对应答案
  • JwtService负责生成JWT验证用户身份。

Auth.controller.ts

关于认证的流程应该包括两种情况:

  • 会话续期 —— 用户已经登录过了,浏览器带着有效的 jwt Cookie 来访问。
  • 首次登录 —— 用户没有 Cookie,或 Cookie 已失效,必须提交用户名和密码。
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
const token = req.cookies?.jwt;//提取jwt cookie
console.log('toekn', token);

if (token) {
try {
// 验证 token 是否有效
const payload = this.jwtService.verify(token);//验证cookie
const user = await this.authService.validateUserById(payload.sub);//验证用户
if (user) {
return {
success: true,
message: 'Already logged in',
user
};
}

res.clearCookie('jwt', {
httpOnly: true,
sameSite: 'lax',
});
return {
success: false,
message: 'Invalid token',
user
};
} catch (e) {
// token 无效,继续正常登录流程
res.clearCookie('jwt', {
httpOnly: true,
sameSite: 'lax',
});
return {
success: false,
message: 'Invalid token',
user: null
};
}
}

if (!loginDto) {
res.clearCookie('jwt', {
httpOnly: true,
sameSite: 'lax',
});
return {
success: false,
message: 'Invalid token and no login data provided',
user: null
};
}

const captchaOk = this.captchaService.validateCaptcha(loginDto.captchaId, loginDto.captchaText);
if (!captchaOk) {
return { error: 'Invalid captcha' };
}

// 正常登录流程
const user = await this.authService.validateUser(loginDto);

if (!user) {
return { error: 'Invalid credentials' };
}
const { accessToken } = await this.authService.login(user);//签发jwt token

// 设置 cookie
res.cookie('jwt', accessToken, {
httpOnly: true,//禁止被js读取
sameSite: 'lax',
// secure: true, // 生产环境建议开启
maxAge: 7 * 24 * 60 * 60 * 1000 // 7天
});

return {
success: true,
message: 'Login successful',
user
};

关于jwt的登录状态保存

首先http是无状态的,因此需要保存用户的信息,以实现登录状态。常见两种方案:session;jwt

  • session-cookie 浏览器为每个请求打上一个tag即Session ID,服务器根据cookie中的sessionID id去查找对应的session。
    这种方案有很多风险
  1. CSRF(cross-site request forgery)
    不同于SSRF,利用服务器通过发送url攻击用户,获取内网文件或者资料。CSRF利用用户的cookie伪造请求,从而实现注入控制服务器。这种方式可以通过生成随机值来解决

  2. 分布式session
    这种情况主要是指对于同一个cookie请求,session分布在不同的服务器上,而要完整的完成一次请求就需要多台服务器协同工作。

对应的解决方案为session复制,并通过框架同步操作(spring-session);把 session 保存在 redis
3. 跨域
cookie是为了安全,是做了domain的限制的,设置cookie的时候会指定一个 domain,只有这个 domain的请求才会带上cookie。而且还可以设置过期时间,路径等等:如果是二级域名不同,仅需要将domain设计成顶级域名即可。如果是顶级域名不同,就需要服务端做中转。

  • 另外一种方式是将JWT使用json方式保存(json web token),然后将其放在request header中发送。
  1. 第一个问题毫无疑问就是安全性,把数据放在header绝对是一个危险的操作。所以要放在https中才能够使用。

本站由 Edison.Chen 创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。