如何使用FaceIO開(kāi)發(fā)基于人工智能的Web?App用戶認(rèn)證模塊
譯文譯者 | 李睿
審校 | 重樓
在過(guò)去的Web應(yīng)用信息系統(tǒng)開(kāi)發(fā)中,用戶認(rèn)證是一個(gè)不可或缺的功能模塊。用戶認(rèn)證功能包括用戶注冊(cè)和登錄認(rèn)證。在以往的開(kāi)發(fā)方法中,用戶認(rèn)證功能模塊實(shí)現(xiàn)的常見(jiàn)方式是使用電子郵件和短信進(jìn)行驗(yàn)證。很多用戶的電腦都安裝了攝像頭,采用攝像頭可以充分利用人臉識(shí)別的人工智能技術(shù)來(lái)實(shí)現(xiàn)用戶認(rèn)證。而使用FaceIO的JavaScript庫(kù)在Web應(yīng)用程序項(xiàng)目中可以實(shí)現(xiàn)用戶身份驗(yàn)證。
本文主要介紹如何通過(guò)第三方人工智能服務(wù)接口開(kāi)發(fā)Web應(yīng)用項(xiàng)目的用戶登錄模塊。Web應(yīng)用程序項(xiàng)目的源代碼已上傳到GitHub,并基于MIT協(xié)議。沒(méi)有任何限制。
本項(xiàng)目是一個(gè)簡(jiǎn)單完整的Web微服務(wù)系統(tǒng)。本項(xiàng)目采用前后端分離的開(kāi)發(fā)方法,使用不同的項(xiàng)目文件路徑。
Plain Text
Technology stack of WEB APP project
Operating System:Windows 10
Front-end: Node.js 18.0.0, React 18.2.0, FaceIO, CoreUI 4.3.1
Front-end development tool: WebStorm 2019
Back-end: Java 1.8, Spring Boot, JWT, Mybatis, Maven
Back-end development tool: IntelliJ IDEA 2019
Database: MySQL 5.7+
這個(gè)Web項(xiàng)目的源代碼包括前端、后端和數(shù)據(jù)庫(kù),是一個(gè)完整的Web應(yīng)用程序信息系統(tǒng)。前端開(kāi)發(fā)使用React,后端開(kāi)發(fā)使用Java和SpringBoot,數(shù)據(jù)庫(kù)使用MySQL。第三方AI業(yè)務(wù)接口使用FaceIO。FaceIO提供了一個(gè)在線JavaScript庫(kù),可以在前端代碼中直接引用。前端引用FaceIO庫(kù)之后,添加少量代碼即可輕松實(shí)現(xiàn)人臉認(rèn)證,在與后端集成后,即可實(shí)現(xiàn)完整的用戶認(rèn)證。前端界面使用CoreUI免費(fèi)模板。
FaceIO的使用并不局限于瀏覽器。它可以在任何瀏覽器上運(yùn)行,包括IE、Chrome、Firefox和Safari。而且,所有人工智能業(yè)務(wù)處理都是在FaceIO的服務(wù)器上完成的,所以FaceIO需要能夠訪問(wèn)用戶當(dāng)前瀏覽器上的攝像頭。
如何在前端React框架中使用FaceIO庫(kù)
步驟1:安裝和配置Node.js環(huán)境
從Node.js官方網(wǎng)站下載對(duì)應(yīng)版本的Node.js壓縮包。這里使用的版本是V18.0.0。如果用戶想運(yùn)行發(fā)布的這個(gè)開(kāi)源網(wǎng)絡(luò)項(xiàng)目,最好也使用V18.0.0。由于Node.js版本的迭代速度相對(duì)較快,如果使用其他版本,這一開(kāi)源Web項(xiàng)目中使用的本地JavaScript庫(kù)可能不兼容,可能無(wú)法運(yùn)行。
在下載Node.js壓縮包之后,將壓縮包解壓到英文目錄下。因?yàn)槭褂肐ntelliJ IDEA開(kāi)發(fā)前端React,所以需要在IntelliJ IDEA中配置Node.js和NPM,并指定Node.js的安裝目錄。
在Intelli J IDEA中配置Node.js和NPM后,用戶可以通過(guò)IntelliJ IDEA工具創(chuàng)建ReactApp項(xiàng)目。
步驟2:在FaceIO中申請(qǐng)公共ID
FaceIO提供了一個(gè)在線JavaScript庫(kù)。如果想使用FaceIO提供的人工智能服務(wù),則需要為其APP申請(qǐng)一個(gè)公共ID。在登錄之前,首先注冊(cè)一個(gè)帳戶,然后根據(jù)項(xiàng)目創(chuàng)建一個(gè)應(yīng)用程序以獲得公共ID。這個(gè)公共ID需要寫(xiě)在ReactApp項(xiàng)目的代碼中。在申請(qǐng)公共ID時(shí),F(xiàn)aceIO將為應(yīng)用程序提供一個(gè)免費(fèi)的公共ID版本,并對(duì)使用次數(shù)進(jìn)行限制。
步驟3:在React應(yīng)用程序項(xiàng)目中使用FaceIO
外部JavaScript庫(kù)地址由FaceIO提供。
JavaScript
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = 'https://cdn.faceio.net/fio.js';
document.head.appendChild(script);
在引入fio.js之后,定義常量就可以使用了。代碼如下所示:
JavaScript
let myFaceIO;
useEffect(()=>{
//eslint-disable-next-line
myFaceIO = new faceIO("fioab497");
},[])
fioab497是應(yīng)用注冊(cè)后的公共ID。用戶需要替換應(yīng)用程序的公共ID。
需要注意的是,在上面代碼React的鉤子函數(shù)useEffect()中,有一行代碼//eslint-disable-next-line。如果用戶已經(jīng)在開(kāi)發(fā)環(huán)境中安裝了eslint插件,則需要添加這行代碼。如果沒(méi)有這行代碼,eslint檢測(cè)將認(rèn)為存在錯(cuò)誤。錯(cuò)誤提示如下:
ERROR in [eslint]
src\views\pages\login\Login.js
Line 52:20: 'faceIO' is not defined no-undef
在開(kāi)發(fā)環(huán)境中,Web項(xiàng)目將無(wú)法運(yùn)行。因此,需要添加這一行注釋代碼來(lái)讓eslint跳過(guò)下一行代碼的檢測(cè)。
FaceIO提供了人臉注冊(cè)函數(shù)enroll()。代碼的使用方式如下:
JavaScript
const faceSignUp = async ()=>{
myFaceIO.enroll({
"locale": "auto"
}).then(userInfo => {
console.log("facialId: " + userInfo.facialId)
console.log(userInfo);
handleLogin(userInfo,"login/signUp");
}).catch(errCode => {
console.log(errCode);
})
}
在上面的代碼中,函數(shù)enroll()以JSON字符串格式輸入數(shù)據(jù),并傳遞給FaceIO的人工智能接口。UserInfo是FaceIO人臉驗(yàn)證后返回的數(shù)據(jù)對(duì)象。handllogin()是一個(gè)用戶定義的函數(shù),用于在接收到FaceIO返回的數(shù)據(jù)對(duì)象后與后端通信。當(dāng)然,用戶也可以根據(jù)自己的開(kāi)發(fā)情況,設(shè)置其他自定義函數(shù)來(lái)處理FaceIO返回的數(shù)據(jù)對(duì)象。
在函數(shù)enroll()輸入的JSON字符串格式數(shù)據(jù)中,還可以添加自定義JSON字符串?dāng)?shù)據(jù)。代碼如下所示:
JavaScript
const faceSignUp = async ()=>{
myFaceIO.enroll({
"locale": "auto", // Default user locale
"payload": {
"user": 123456,
"email": "name@example.com"
}
}).then(userInfo => {
console.log("facialId: " + userInfo.facialId)
console.log(userInfo);
handleLogin(userInfo,"login/signUp");
}).catch(errCode => {
console.log(errCode);
})
}
payload是FaceIO可以返回的數(shù)據(jù)節(jié)點(diǎn)。在“有效負(fù)載”中,可以添加需要返回的JSON格式數(shù)據(jù),并根據(jù)開(kāi)發(fā)需求定制數(shù)據(jù)內(nèi)容。
調(diào)用函數(shù)enroll()后,瀏覽器將顯示FaceIO提供的人機(jī)交互用戶界面,并在瀏覽器提示符下啟動(dòng)攝像頭。需要點(diǎn)擊“是”。FaceIO提供的用戶界面會(huì)自動(dòng)在當(dāng)前攝像頭前確認(rèn)用戶的臉兩次,并要求當(dāng)前用戶確認(rèn)兩次PIN碼的設(shè)置。輸入的PIN碼將用于面部認(rèn)證。
FaceIO提供了人臉認(rèn)證函數(shù)authenticate()。代碼的使用方式如下:
JavaScript
const faceSignIn = async ()=>{
myFaceIO.authenticate({
"locale": "auto"
}).then(userInfo => {
console.log("facialId: " + userInfo.facialId)
console.log(userInfo);
handleLogin(userInfo,"login/signIn");
}).catch(errCode => {
console.log(errCode);
})
}
在調(diào)用authenticate()函數(shù)之后,瀏覽器將顯示FaceIO提供的人機(jī)交互用戶界面,啟動(dòng)攝像頭,并要求當(dāng)前登錄用戶輸入人臉注冊(cè)時(shí)設(shè)置的PIN碼。
根據(jù)FaceIO返回的數(shù)據(jù),自定義函數(shù)handleLogin()被傳遞到后端。后端接收數(shù)據(jù),分析數(shù)據(jù),并將結(jié)果返回給前端。前端執(zhí)行后續(xù)業(yè)務(wù)邏輯。如果用戶身份驗(yàn)證成功,后端將向前端返回令牌數(shù)據(jù)。在前端確認(rèn)登錄成功后,頁(yè)面跳轉(zhuǎn)到Dashboard完成整個(gè)用戶身份驗(yàn)證過(guò)程。
前端接收到令牌數(shù)據(jù)后,將令牌數(shù)據(jù)保存在用戶當(dāng)前瀏覽器的會(huì)話存儲(chǔ)中。在會(huì)話存儲(chǔ)中自定義了一個(gè)名為“Authorization”的項(xiàng)來(lái)存儲(chǔ)令牌數(shù)據(jù)。保存令牌數(shù)據(jù)的函數(shù)代碼如下:
JavaScript
1 const setAuthorization = (Auth) => {
2 sessionStorage.setItem('Authorization',Auth)
3 }
獲取Token數(shù)據(jù)的函數(shù)代碼如下:
JavaScript
1function getAuthorization () {
2 let Author = sessionStorage.getItem('Authorization')
3 if (Author === null) return ''
4 return Author
5 }
已經(jīng)在系統(tǒng)中設(shè)置了自動(dòng)加載令牌數(shù)據(jù)。在后續(xù)的業(yè)務(wù)處理中,當(dāng)訪問(wèn)后端API時(shí),令牌數(shù)據(jù)將自動(dòng)放置在請(qǐng)求頭的授權(quán)中。代碼如下所示:
JavaScript
instanceForm.interceptors.request.use(
(config) => {
config.headers.authorization = getAuthorization()
return config
}
)
如何在后端完成用戶認(rèn)證
在Web項(xiàng)目中,使用SpringBoot作為后臺(tái),開(kāi)發(fā)語(yǔ)言為Java 1.8。在Spring框架中,創(chuàng)建一個(gè)處理用戶登錄身份驗(yàn)證的控制層類。這個(gè)LoginController類也是一個(gè)用于處理用戶登錄身份驗(yàn)證的API接口。代碼如下:
Java
package com.auto17.base.controller;
import ...
import ...
@RestController
@RequestMapping("/login")
@CrossOrigin
public class LoginController{
protected final Logger logger = LoggerFactory.getLogger(LoginController.class);
@Autowired
private IAppUserService appUserService;
@PostMapping("/signUp")
public AjaxResult signUp(@RequestBody JSONObject userInfo) {
...
}
15 @PostMapping("/signIn")
16 public AjaxResult signIn(@RequestBody JSONObject userInfo) {
17 ...
18 }
19 }
在上面的代碼中,@RequestMapping("/login")表示整個(gè)LoginController類的API路徑。
@PostMapping("/signUp")表示用戶注冊(cè)的API路徑,接收POST數(shù)據(jù)請(qǐng)求,使用函數(shù)signUp()處理用戶注冊(cè)。完整的API路徑是/login/signUp。
@PostMapping("/signIn")表示用戶身份驗(yàn)證的API路徑。它接收POST數(shù)據(jù)請(qǐng)求。函數(shù)signIn()用于處理用戶身份驗(yàn)證。完整的API路徑是/login/signIn。
在函數(shù)signUp()或signIn()中,解析前端React傳遞的JSON格式數(shù)據(jù)。從JSON數(shù)據(jù)中提取nodefacialId的值。FacialId是FaceIO成功驗(yàn)證用戶時(shí)返回的唯一標(biāo)識(shí)符。該ID是唯一的。在這個(gè)Web項(xiàng)目中,通過(guò)“facialId”判斷用戶,并通過(guò)facialId識(shí)別用戶。
需要注意的是,在這個(gè)Web項(xiàng)目中,JSON格式的數(shù)據(jù)從前端傳輸?shù)胶蠖耸敲魑牡?,沒(méi)有進(jìn)行數(shù)據(jù)加密。如果使用它,可以添加安全函數(shù)來(lái)加密JSON格式的數(shù)據(jù)。前端加密完成后,消息從前端傳輸?shù)胶蠖恕T诤蠖私饷芎?,將解析JSON格式數(shù)據(jù)。加密方法有很多種。在此推薦RSA算法。RSA算法是一種非對(duì)稱加解密算法。在后端,也就是在用戶訪問(wèn)Web時(shí),服務(wù)器端生成一對(duì)RSA密鑰,包括公鑰和私鑰,并將公鑰提供給前端。前端使用公鑰對(duì)JSON格式的數(shù)據(jù)進(jìn)行加密,然后傳輸?shù)胶蠖?。后端根?jù)私鑰對(duì)其進(jìn)行解密。在解密之后,解析JSON格式數(shù)據(jù)。這可以最大限度地保護(hù)“facialId”的值不被輕易攔截。
在后端,在用戶被facialId成功識(shí)別之后。使用JWT方法保存當(dāng)前登錄用戶的信息。JWT Utils類中的函數(shù)getToken(AppUser用戶)獲取當(dāng)前用戶的令牌。使用facialId的值作為簽名密鑰。代碼如下所示:
Java
public class JWTUtils {
public static String getToken(AppUser user) {
Calendar instance = Calendar.getInstance();
//Token expiration time
instance.add(Calendar.DATE, 1);
JWTCreator.Builder builder = JWT.create();
return builder.withAudience(String.valueOf(user.getUserId()))
.withClaim("userId", user.getUserId())
.withClaim("facialId", user.getFacialId())
.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(user.getFacialId()));
}
}
在后端獲取當(dāng)前登錄用戶的令牌后,將令牌數(shù)據(jù)返回給前端。
用戶身份驗(yàn)證成功后,后端將在每個(gè)后續(xù)API請(qǐng)求中接收用于用戶身份驗(yàn)證的Token數(shù)據(jù)。已經(jīng)創(chuàng)建了一個(gè)攔截器類JWTInterceptor,用于在每個(gè)API請(qǐng)求之前驗(yàn)證令牌數(shù)據(jù)。代碼如下所示:
Java
package com.auto17.base.security;
import com.auto17.base.domain.AjaxResult;
import com.auto17.faceLogin.domain.AppUser;
import com.auto17.faceLogin.service.IAppUserService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
public class JWTInterceptor extends HandlerInterceptorAdapter {
protected final Logger log = LoggerFactory.getLogger(JWTInterceptor.class);
@Autowired
private IAppUserService appUserService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("authorization");
log.info("token="+token);
if(StringUtils.isEmpty(token)){
log.error("authorization is Empty");
errorOut(response,"authorization is Empty");
return false;
}
try {
//get login user info
String userNoString=JWTUtils.getAudience(token);
log.info("userNoString="+userNoString);
AppUser loginUser=appUserService.selectAppUserById(Long.valueOf(userNoString));
if(loginUser!=null){
JWTUtils.verify(token,loginUser.getFacialId());
request.setAttribute("loginUser", loginUser);
}else{
errorOut(response,"check user fail");
}
} catch (Exception e) {
errorOut(response,"check verify fail");
e.printStackTrace();
return false;
}
return true;
}
private void errorOut(HttpServletResponse response,String msg){
try {
response.setHeader("Access-Control-Allow-Origin","*");
response.setContentType("text/json; charset=utf-8");
response.setHeader("Access-Control-Allow-Methods","GET,POST,OPTIONS,DELETE");
response.setHeader("Access-Control-Max-Age","3600");
response.addHeader("Access-Control-Allow-Headers", "*");
PrintWriter writer=response.getWriter();
writer.print(AjaxResult.error(msg));
writer.flush();
writer.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
除了在前端提供易于使用的功能之外,F(xiàn)aceIO還提供了REST API,允許從后端管理應(yīng)用程序。每個(gè)FaceIO應(yīng)用程序都會(huì)自動(dòng)分配一個(gè)API密鑰,可以從FaceIO控制臺(tái)的應(yīng)用程序管理器中檢索這一密鑰。API鍵允許用戶從專用的后端環(huán)境以編程方式管理應(yīng)用程序,可以使用SpringBoot的RestTemplate輕松實(shí)現(xiàn)它。因?yàn)檫@一項(xiàng)目是一個(gè)簡(jiǎn)單的應(yīng)用程序,所以沒(méi)有在這方面使用它。如果用戶感興趣,可以訪問(wèn)FaceIO其余API的在線文檔。
如何設(shè)計(jì)數(shù)據(jù)庫(kù)
在這個(gè)Web項(xiàng)目中,數(shù)據(jù)庫(kù)使用MySQL。為了配合人臉識(shí)別,設(shè)計(jì)了一個(gè)簡(jiǎn)單的用戶表。創(chuàng)建表SQL語(yǔ)句:
SQL
CREATE TABLE `app_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`facial_id` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`nick_name` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`true_name` varchar(160) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`age` smallint(6) DEFAULT NULL,
`gender` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`reg_time` datetime(0) DEFAULT NULL,
`last_login_time` datetime(0) DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE,
UNIQUE INDEX `idx_facialid`(`facial_id`) USING BTREE
)
數(shù)據(jù)表中的字段facal_id用于存儲(chǔ)FaceIO返回的唯一用戶ID。這是一個(gè)簡(jiǎn)單的用戶數(shù)據(jù)表。沒(méi)有用戶密碼字段或用戶手機(jī)字段。該用戶數(shù)據(jù)表僅使用字段“facal_id”完成登錄用戶的認(rèn)證和識(shí)別。
用戶在FaceIO完成注冊(cè)后,F(xiàn)aceIO返回的JSON字符串?dāng)?shù)據(jù)中包含“details”節(jié)點(diǎn),即FaceIO人工智能識(shí)別的當(dāng)前用戶的年齡和性別。將它存儲(chǔ)在用戶數(shù)據(jù)表中的“年齡”和“性別”字段中,這是目前的開(kāi)發(fā)方法。
當(dāng)然,也可以保留以前的用戶認(rèn)證的開(kāi)發(fā)方法,同時(shí)仍然保留登錄密碼和SMS認(rèn)證。在原有開(kāi)發(fā)方法的基礎(chǔ)上,增加了FaceIO的人臉認(rèn)證方法。在原有的用戶數(shù)據(jù)表中,添加字段facal_id綁定人工智能的人臉識(shí)別。
原文標(biāo)題:Using the JavaScript library of FaceIO to implement user authentication in a web application project,作者:jianxiang sun