在前端面试中总会遇到面试官问你前端登录鉴权是怎么做的,token的作用是怎么,token过期了怎么办之类的一些问题,那么在在这里就以我的智能协作项目(https://github.com/ztygod/smart-task-platform)做一次简单的梳理。
- 技术栈
- 前端:
vue,axios
- 后端:
nest.js,mysql,TypeORM
注册阶段
在一般项目中我们会输入用户名ID和密码来作为注册的凭证。
- 前端获取到这两个字段,向后端发起注册请求;
- 后端会判断ID是否已近在数据库中,
- 如果是则报错,前端进行消息提示;
- 不存在则把数据存入数据库中
- 存储数据,有几个关键操作:
- 由于我们的密码是明文传输的,直接把密码不做任何处理存入数据库有安全风险,我们需要对密码做加密操作。。
逻辑主要集中在后端,我们来看看代码。
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; 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); } }
private buildUserRO(user: User) { const userRO = { id: user.id, username: user.username, token: this.generateJWT(user), } return { user: userRO } };
public generateJWT(user: User) { let date = new Date(); let outDate = new Date(date); outDate.setDate(date.getDate() + 60)
return jwt.sign({ id: user.id, username: user.username, exp: outDate.getTime() / 1000 }, SECRET) } }
|
上面代码包括:
- 检查用户名唯一性,
- 创建用户实体,加密密码存储到数据库,
- 根据创建时间生成JWT,设置JWT(token过期时间),
- 构建并返回一个格式化后的用户响应对象。
其中后面几个操作主要用于登录流程,这里为了功能完善所以列出。
登录阶段
目标:通过输入用户名和密码,成功进入主页,并在前端本地存储token用作获取资源的凭证
- 前端输入用户信息(用户名与密码),调用接口发起请求。
- 后端根据用户ID查询数据库拿到加密后的密码,用明文密码与加密密码进行对比。
- 对比成功后,生成JWT(token),构建并返回一个格式化的用户响应对象。
- 前端拿到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的中间件特别好用)*
下面代码大致逻辑:
- 判断请求头中authorization是否存在,authorization是否带有token
- 通过第三方库拿到生成token时所用到的user_id
- 查询user是否存在,不存在则报错
- 存在则执行原本操作,比如请求接口
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]]