前端登录鉴权全过程

tianyi Lv3

在前端面试中总会遇到面试官问你前端登录鉴权是怎么做的,token的作用是怎么,token过期了怎么办之类的一些问题,那么在在这里就以我的智能协作项目(https://github.com/ztygod/smart-task-platform)做一次简单的梳理。

  • 技术栈
    • 前端:vueaxios
    • 后端:nest.jsmysqlTypeORM

注册阶段

在一般项目中我们会输入用户名ID和密码来作为注册的凭证。

  1. 前端获取到这两个字段,向后端发起注册请求;
  2. 后端会判断ID是否已近在数据库中,
    1. 如果是则报错,前端进行消息提示;
    2. 不存在则把数据存入数据库中
  3. 存储数据,有几个关键操作:
    1. 由于我们的密码是明文传输的,直接把密码不做任何处理存入数据库有安全风险,我们需要对密码做加密操作。。

逻辑主要集中在后端,我们来看看代码。

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
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>
) {
}
async create(createUserDto: CreateUserDto): Promise<UserRO> {
//检查唯一性
const { username, password } = createUserDto;
// 使用 DataSource 获取 Repository 并执行查询
const userRepository = dataSource.getRepository(User);
const qb = await userRepository.
createQueryBuilder('user')
.where('user.username = :username', { username })

const user = await qb.getOne();

if (user) {
const errorMessage = { username: '用户名重复' }
throw new HttpException({ message: 'Input data validation failed', errorMessage }, HttpStatus.BAD_REQUEST);
}

//创建用户
let userInstance = new User();
const hashedPassword = await argon2.hash(password);
userInstance.username = username;
userInstance.password = hashedPassword;

const error = await validate(userInstance);
if (error.length > 0) {
const errorMessageFromat = { username: 'Userinput is not valid.' };
throw new HttpException({ message: 'Input data validation failed', errorMessageFromat }, HttpStatus.BAD_REQUEST);
} else {
const saveUser = await this.userRepository.save(userInstance);
return this.buildUserRO(saveUser);
}
}

//构建并返回一个格式化后的用户响应对象(userRO)
private buildUserRO(user: User) {
const userRO = {
id: user.id,
username: user.username,
token: this.generateJWT(user),
}
return { user: userRO }
};

// 生成JWT
public generateJWT(user: User) {
let date = new Date();
let outDate = new Date(date);
outDate.setDate(date.getDate() + 60) //设置过期时间,60天后

return jwt.sign({
id: user.id,
username: user.username,
exp: outDate.getTime() / 1000
}, SECRET)
}
}

上面代码包括:

  • 检查用户名唯一性,
  • 创建用户实体,加密密码存储到数据库,
  • 根据创建时间生成JWT,设置JWT(token过期时间),
  • 构建并返回一个格式化后的用户响应对象。
    其中后面几个操作主要用于登录流程,这里为了功能完善所以列出。

登录阶段

目标:通过输入用户名和密码,成功进入主页,并在前端本地存储token用作获取资源的凭证

  1. 前端输入用户信息(用户名与密码),调用接口发起请求。
  2. 后端根据用户ID查询数据库拿到加密后的密码,用明文密码与加密密码进行对比。
  3. 对比成功后,生成JWT(token),构建并返回一个格式化的用户响应对象。
  4. 前端拿到token存储在localStorage或者cookie中,当我们需要访问受限资源时,通过axios请求拦截器使请求头中带上token config.headers[‘Authorization’] = `Bearer ${token}`;

axios.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const instance = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000
});

//请求拦截器
instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
)

user.conctroller.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Post('login')
async login(@Body() loginUserDto: LoginUserDto): Promise<UserRO> {
const _user = await this.userService.findOne(loginUserDto);

const error = { user: 'not found' };
if (!_user) {
throw new HttpException({ error }, 401);
}

const token = this.userService.generateJWT(_user);
const { username } = _user;
const user = { username, token };
return { user }
}

通过中间件实现对访问受限资源的鉴权

上面已经讲了在对受限资源请求时带上Authorization请求头,那么服务端要做的就是在通过中间件鉴权决定是否进行响应。*(这里安利一下nest.js的中间件特别好用)*
下面代码大致逻辑:

  1. 判断请求头中authorization是否存在,authorization是否带有token
  2. 通过第三方库拿到生成token时所用到的user_id
  3. 查询user是否存在,不存在则报错
  4. 存在则执行原本操作,比如请求接口

user.middleware.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private readonly userService: UserService) { }

async use(req: Request, res: Response, next: NextFunction) {
const authHeaders = req.headers.authorization;
if (authHeaders && (authHeaders as string).split(' ')[1]) {
const token = (authHeaders as string).split(' ')[1];
const decoded = jwt.verify(token, SECRET);
const user = await this.userService.findById(decoded.id);

if (!user) {
throw new HttpException('User not found', HttpStatus.UNAUTHORIZED);
}

req.user = user.user;
next();

} else {
throw new HttpException('Not authorized.', HttpStatus.UNAUTHORIZED);
}
}
}

我们可以选择要通过中间件的路由
user.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService],
exports: [UserService,]
})
export class UserModule implements NestModule {
public configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware)
.forRoutes(
{ path: 'user', method: RequestMethod.GET },
{ path: 'user', method: RequestMethod.PUT },
)
}
}

[[JWT token失效问题与解决方案.md]]

  • Title: 前端登录鉴权全过程
  • Author: tianyi
  • Created at : 2025-04-12 11:27:04
  • Updated at : 2025-04-12 15:19:01
  • Link: https://github.com/ztygod/2025/04/12/前端登录鉴权全过程/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments