本文章同时发布于:
MediumiT邦帮忙大家好,继上一篇介绍完OAuth2.0 & OIDC后这次要介绍的就是如何实作Line Login
与NestJS
整合的部分。
这篇文章会讨论
Line Login设定ngrokNestJS使用CLI创建MVC架构Line Login设定
在与NestJS
整合之前,我们必须先到Line Console上面创建Providers
,并创建Line Channel
,以获得Channel ID, secret等等资讯。
NestJS
时会使用到设定ngrok
ngrok是可以再开发的时候让你免费调用HTTPS的工具
我们进入ngrok的网页注册登入
下载ngrok,并透过ngrok执行授权指令,最后执行./ngrok http 3000
複製ngrok
URL,并贴至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整体的概念我们可以以官网的这张图来解释:
Server路由
有了MVC的整体概念后,我们可以借用上一篇OIDC的流程来说明有哪些路由,我们总共有两个分别是:
/login: 回传网页,会生成一个Line Login的button,点选之后就会跳制Line 认证网站。/login/auth: Ling Login认证网站认证成功后会发一个名为code
的代码至line/auth
,而line/auth
的controller
会透过此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 token
与OIDC 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。