Week16 - 用NestJS整合Line Login,一个基于OAuth2.0的OpenID Connect系统 -

本文章同时发布于:

MediumiT邦帮忙

大家好,继上一篇介绍完OAuth2.0 & OIDC后这次要介绍的就是如何实作Line LoginNestJS整合的部分。

这篇文章会讨论

Line Login设定ngrokNestJS使用CLI创建MVC架构

Line Login设定

在与NestJS整合之前,我们必须先到Line Console上面创建Providers,并创建Line Channel,以获得Channel ID, secret等等资讯。

登入Line Console

创建Providers

创建Line Login Channel

填写Channel name, description, Email address,并勾选Web app,最后勾选同意条款并创建

複製Channel ID, secret,等等再整合NestJS时会使用到

接下来我们要设定Callback URL,让Line认证网站认证成功之后有有办法把认证资讯带回前端,而这个URL必须拥有HTTPS,所以我们必须用到ngrok

设定ngrok

ngrok是可以再开发的时候让你免费调用HTTPS的工具

我们进入ngrok的网页注册登入

下载ngrok,并透过ngrok执行授权指令,最后执行./ngrok http 3000

複製ngrokURL,并贴至Line Console的Callback URL

NestJS使用CLI创建MVC架构

整体程式码在此,大家可以Clone起来Run会比较好理解文章。

MVC架构

NestJS将MVC的分层切得很清楚,我们可以先看看最后的目录架构:

views资料夹:即是V层,这个曾就是登入的网页页面。副档名为controller.ts的档案:即是C层,业务逻辑都会放在这里,例如要如何透过API去拿取access token,这类的「应对行为」逻辑就会放在此处。副档名为service.ts的档案:即是M层,任何资料的来源都会来自此档案,例如Line API、资料库、物联网装置等等第三方的资料,都由此档案提供。

所以MVC整体的概念我们可以以官网的这张图来解释:

图片来自NestJS官网

Server路由

有了MVC的整体概念后,我们可以借用上一篇OIDC的流程来说明有哪些路由,我们总共有两个分别是:

/login: 回传网页,会生成一个Line Login的button,点选之后就会跳制Line 认证网站。/login/auth: Ling Login认证网站认证成功后会发一个名为code的代码至line/auth,而line/authcontroller会透过此code发Post至Line API来取得access token与OIDC的ID Token。

来Run Server吧!

我把需要设定的Config都集中放到.env.example里了,请把刚刚获得的资讯都贴至此档案,并将此档案命名为.env

LINE_API_URI=https://api.line.meLINE_ACCESS_URI=https://access.line.meLINE_CLIENT_ID=<刚刚在Line Console获得的Line Channel ID>LINE_CLICLIENT_SECRET=<刚刚在Line Console获得的Line Channel Secret>SERVER_URI=<ngrok的URL,记得不要把路由/login/auth也贴上来了>

安装并Run Server

$ npm install$ npm run start:dev

实际使用

在浏览器上浏览<ngrok的URL>/login

输入自己的帐号密码

这边会询问是否同意个人档案用户识别资讯被读取,点选许可即可,如果需要更多的资源,就需要进入Console调整,目前只有个人档案的access token与用来识别的OIDC而已。许可之后Line 认证网站就会开始验证。

验证成功后就会将以下资讯带往前端,前端会再将这些资讯带至后端

// 后端必须用此code来去Line API拿取access token与OIDC ID Token"code": "0ajVvTdwHLV4wQR2eFPM"// 防止XSS攻击的随机乱码"state": "3e2e819f603adcfc6cb3f1761293efb591af206733e149351b2d19f43d9cf9c9a78a8a746449b763e471a9"

后端透过code参数发送Post至Line API,拿取access tokenOIDC ID Token,这边我有将资讯显示在前端,不过要怎么使用OIDC ID Token完全取决你怎么设计Server。

{  // 可以用来取的使用者档案的access token  "access_token": "eyJhbGciOiJIUzI1NiJ9.ohEOAni8Y89mKrUGy1hrVl6oPmwsG5mcIQ1lOfAezzgy_s5tH-NNjWysXP8NEVEwlJwTR8V3XyiZsakzfd__HNdBGUK9um2AyqFroYzTSklGuEhFx3DtbDBKwZdAO1XmtSsZrcB90ka6dkqdK8BhYIIme6krvDDlsRPFV6Yg1QY.hto6BEx2C6mcDtcYQw43LK2kRpbm1mZ1fgM38xnoZFQ",  // access token的type  "token_type": "Bearer",  // 当access token过期时,可以拿此token去重新拿token  "refresh_token": "uRweZyH1XdgKhAoXnp2X",  // access token的期效  "expires_in": 2592000,  // 此次要求的资源  "scope": "openid profile",  // OIDC取得的ID Token  "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FjY2Vzcy5saW5lLm1lIiwic3ViIjoiVTAxMDZkNWVhZDFhYzYyOTdmNDZmOTYxM2RhNmIzZDRiIiwiYXVkIjoiMTY1NDMxNDI3MSIsImV4cCI6MTU5MTU1MTkzNiwiaWF0IjoxNTkxNTQ4MzM2LCJub25jZSI6IjdlMzY1NDQ2M2NiNGY4ZGZiMzY1MTQxMzU2OGI2NDY2NmRmNDlkMWMyMjg3NWExODYyYzM0ZmU5MjliM2MyYzk0ZWVlNzQ3YjdiYzJhZjlmMTU1MzA5IiwiYW1yIjpbInB3ZCJdLCJuYW1lIjoi4ZWVKCDhkJsgKeGVl-aIkeS4jeaYr-WuuOWPsyDmiJHmmK_otoXntJrlkbHlkbEiLCJwaWN0dXJlIjoiaHR0cHM6Ly9wcm9maWxlLmxpbmUtc2Nkbi5uZXQvMGhpTFkxZTh3WE5tTmtTQjVqUmwxSk5GZ05PQTRUWmpBckhDMV9CQlpNT0ZZWmYzQTlXM2twQmhaQU9GTWRLM2sxRENrdVZVSkxhd1JQIn0.GVt5zG_QjiGZT3DfguGmz9jNX3hLwbPu4DxHHITHI5Y"}

我们可以把ID Token贴到jwt.io解析参数,最重要的就是sub这个参数,这就像使用者的身分证字号,是唯一的,你可以拿此ID至你的Server帐户系统整合。

{  // 发ID Token的Line API URL  "iss": "https://access.line.me",  // 关键的Line ID,可以拿此ID至你的Server帐户系统整合  "sub": "U0106d5ead1ac6297f46f9613da6b3d4b",  // Line Channel ID  "aud": "1654314271",  // access token的过期的时间  "exp": 1591551936,  // access token生产的时间  "iat": 1591548336,  // 授权在URL中指定的值  "nonce": "7e3654463cb4f8dfb3651413568b64666df49d1c22875a1862c34fe929b3c2c94eee747b7bc2af9f155309",  // 使用者登入的方法,pwd只使用帐密登入  "amr": [    "pwd"  ],  // 使用者名称  "name": "ᕕ( ᐛ )ᕗ我不是宸右 我是超级呱呱",  // 使用者大头贴  "picture": "https://profile.line-scdn.net/0hiLY1e8wXNmNkSB5jRl1JNFgNOA4TZjArHC1_BBZMOFYZf3A9W3kpBhZAOFMdK3k1DCkuVUJLawRP"}

Server Controller & Service & CLI讲解

NestJS的CLI设计得相当完善,我们可以不用设定太多dirty thing就可以自动生产出一个NestJS Server。

首先我们安装NestJS CLI,并创建project。

$ npm i -g @nestjs/cli$ nest new project

安装hbs作为网页渲染的引擎

$ npm install --save hbs

main.ts设定hbs为网页渲染引擎

// main.tsimport { NestFactory } from '@nestjs/core';import { NestExpressApplication } from '@nestjs/platform-express';import { join } from 'path';import { AppModule } from './app.module';async function bootstrap() {  const app = await NestFactory.create<NestExpressApplication>(    AppModule,  );  app.useStaticAssets(join(__dirname, '..', 'public'));  app.setBaseViewsDir(join(__dirname, '..', 'views'));  app.setViewEngine('hbs');  await app.listen(3000);}bootstrap();

安装Config模组

$ npm i --save @nestjs/config

透过CLI直接创建auth Controller

$ nest g controller auth

设定app.module.ts,将Config、API、Auth Controller通通引入

// app.module.tsimport { Module, HttpModule } from '@nestjs/common';import { AppController } from './app.controller';import { AppService } from './app.service';import { AuthController } from './auth/auth.controller';import { AuthService } from './auth/auth.service';import { ConfigModule } from '@nestjs/config';@Module({  imports: [HttpModule, ConfigModule.forRoot()],  controllers: [AppController, AuthController],  providers: [AppService, AuthService],})export class AppModule {}

Auth Controller讲解,我注解在程式码上

// auth.controller.tsimport { Get, Controller, Render, Query } from '@nestjs/common';import { ConfigService } from '@nestjs/config';import * as crypto from 'crypto';import * as qs from 'querystring';import { AuthService } from './auth.service';// login的前缀,也就是说,底下所有的Method的Route前面都会包含/login路径@Controller('login')export class AuthController {  constructor(private authService: AuthService, private configService: ConfigService){  }  // /login的route,会生成带有button的网页回传  @Get()  @Render('index')  root() {    // 乱数生成state    const state:string = crypto.randomBytes(43).toString('hex');    // 乱数生成nonce    const nonce:string = crypto.randomBytes(43).toString('hex');    // 将response_type, client_id, redirect_uri, state, scope, nonce组成button的query参数,传至index.hbs渲染给浏览器    const query:string = qs.stringify({      response_type: 'code',      client_id: this.configService.get<string>('LINE_CLIENT_ID'),      redirect_uri: `${this.configService.get<string>('SERVER_URI')}/login/auth`,      state,      scope: 'profile openid',      nonce    })    return { lineAuthLoginURI: `${this.configService.get<string>('LINE_ACCESS_URI')}/oauth2/v2.1/authorize?${query}` };  }  @Get('/auth')  @Render('auth')  async auth(@Query('code') code) {    try {      // 拿到Line 认证网站的code之后,透过此code去Line API拿access token与OIDC ID Token      const token = await this.authService.postToken(code).toPromise()      return { token: JSON.stringify(token)}    } catch (err) {      console.log(err)    }  }}
// auth.service.ts// 引入HttpService模组,NestJS封装的HTTP模组是透过Axios与RxJS整合的,拥有RxJS的方便的observable, observer, operator,可以组合各种非同步操作并利用operator管理资料流import { Injectable, HttpService } from '@nestjs/common';import { ConfigService } from '@nestjs/config';import { map } from 'rxjs/operators';import * as qs from 'querystring';@Injectable()export class AuthService {  constructor(private http: HttpService, private configService: ConfigService) {  }  postToken(code){    // 将code以Post的方式送市Line API,并且也要携带client_id, client_secret等等参数,另外redirect_uri必须与Line Console上设定的相同此request才会成功    return this.http.post(      `${this.configService.get<string>('LINE_API_URI')}/oauth2/v2.1/token`,      qs.stringify({        grant_type: 'authorization_code',        code,        redirect_uri: `${this.configService.get<string>('SERVER_URI')}/login/auth`,        client_id: this.configService.get<string>('LINE_CLIENT_ID'),        client_secret: this.configService.get<string>('LINE_CLICLIENT_SECRET')      }),      {        headers: {'Content-Type': 'application/x-www-form-urlencoded'}      })      .pipe(        map(response => response.data)      );          }}

最后再把整体程式码附上,怕大家有遗漏掉XD。


谢谢你的阅读,也欢迎分享讨论~最后再次附上家人为NestJS所画的吉祥物图XD。


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章