graphql-server node版(五)
认证,注册和登陆功能
1、在data model中添加User 类型
在Link和User中建立关系,用户可以发表多个Link,每个Link都有对应的用户。这是SDL表达一对多关系的方式,如下:
type Link {
id: ID! @unique
description: String!
url: String!
postedBy: User
}
type User {
id: ID! @unique
name: String!
email: String! @unique
password: String!
links: [Link!]!
}
在更改数据模型后,我们需要重新部署以应用更改。
prisma deploy
Prisma数据库schema src/generated/prisma.graphql以及Prisma服务的API已经更新。API现在还根据指定的关系公开User类型的CRUD操作以及连接和断开连接User和元素的操作Link
2、扩展app应用程序层的schema
“schema驱动开发”,我们都是首先以增加客户端api为出发点的。
# import Link from "./generated/prisma.graphql"
type Query{
info:String!
feed:[Link!]!
# Fetch a single link by its `id`
link(id: ID!): Link
}
type Mutation{
#add a link
post(url:String!,description:String!):Link!
# Update a link
updateLink(id: ID!, url: String, description: String): Link
# Delete a link
deleteLink(id: ID!): Link
signup(name:String!,email:String!,password:String!):AuthPayload!
login(email:String!,password:String!):AuthPayLoad
}
type AuthPayLoad{
token:String!
user:User
}
type User{
id:ID!
name:String!
email:String!
links:[Link!]!
}
这里我们没有从"./generated/prisma.graphql"引入User,因为我们想隐藏password信息。不想前端可以直接看到密码明文信息。所以我们这里重新定义了User.(当然了,数据库中的密码都应该是加密存储的,即使真的将password字段放在这里,请求到的也是一串加密后的密码。)
这里我们还定义了一个AuthPayLoad,即授权用户。当用户注册登陆之后,将会得到授权用户的信息.
3、实现解析器功能
为了更加的模块化,我们可以将所有的解析器放在一个单独的文件下:resolvers
在src下创建一个resolvers文件夹,并新建三个文件Query.js,Mutation.js,和AuthPayLoad.js
在Query.js中
function feed(parent, args, context, info) {
return context.db.query.links({}, info)
}
function link(root,args,context,info)=>{
return context.db.query.link({id:args.id},info)
}
module.export={
feed,
link,
}
在Mutation.js中
增加login解析器和signup解析器
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const { APP_SECRET } = require('../utils')
function post(root,args,context,info)=>{
return context.db.mutation.createLink({url:args.url,description:args.description},info)
}
function updateLink(root,args,context,info)=>{
return context.db.mutation.updateLink({url:args.url,description:args.description,id:args.id},info)
}
function deleteLink(root,args)=>{
return context.db.mutation.deleteLink({id:args.id},info)
}
async function signup(root,args,context,info)=>{
const password = await bcrypt(args.password,10)
const user = await context.db.mutation.createUser({
data:{...args,password},
},`{ id }`)
const token = jwt.sign({userId:user.id},APP_SECRET)
return {
token,
user
}
}
async function login(root,args,context,info)=>{
const user=await context.db.query.user({where:{email:args.email}},` { id password } `)
if (!user) {
throw new Error('No such user found')
}
const valid = bcrypt.compare(args.password,user.password)
if(!valid){
throw new Error('Invalid password')
}
const token=jwt.sign({userId:user.id},APP_SECRET)
return{
token,
user
}
}
module.export={
post,
updateLink,
deleteLink,
signup,
login,
}
新增signup和login方法
注册signup:
(1)我们使用bcrypt对密码进行加密,bcrpt是一种慢加密方法,使用穷举解密的时候需要很长的时间。在使用之前我们需要先安装bcrypt库。
(2)调用数据库层的api,将修改后的加密过后的密码同用户信息一起存到数据库中,使用createUser方法,并且在创建成功后返回一个用户ID字段、注意这里没有使用info而是采用了硬编码‘{id}’,,即用户注册成功后仅可以得到ID.
因为这里的info代表的是signup的info,即signup中的字段,而不是createUser中的字段。因此传入signup中的字段给creatUser肯定是错误的。
(3)这里我们使用jsonwebtoken包来生成token,使用了APP_SECRET签名加密。
(4)最后返回token和user
login解析器:
(1)根据email向database服务端api请求获取用户,获取成功后返回id和password字段,这里也是采用硬编码方式。没有引用info。也是同理,login的字段信息不能直接传给User.
(2)如果没有找到用户,则提示用户名不存在
(3)如果用户名存在的话验证密码是否正确,使用bcrypt.compare(args.password,user.password),比较用户传入的密码和服务器存储的密码是否一致。
(4)如果不一致,提示密码无效
(5)如果登陆成功,则获取token
(6)返回登陆成功的token和user
我们需要在根目录添加相应的包bcrypt和jsonwebtoken
yarn add jsonwebtoken bcryptjs
继续在mutation.js中增加AuthPayload解析器
function user(root, args, context, info) {
return context.db.query.user({ where: { id: root.user.id } }, info)
}
module.exports = { user }
在AuthPayload解析器中解析user,他接受的是login或signup传递来的user.id,因此我们使用root.user.id来查找user,并且返回user.这样在对于login和signup中返回的AuthPayload中的user可以正常的返回user对象了。因为info的现值,在login和signup中的user都是使用了硬编码的方式来获取字段id,在AuthPayload中在login,signup中获取的id字段再次转换为了user对象。
现在创建一个utils.js,
在utils.js中创建一些会在其他地方重用的信息。
const jwt = require('jsonwebtoken')
const APP_SECRET = 'GraphQL-is-aw3some'
function getUserId(context) {
const Authorization = context.request.get('Authorization')
if (Authorization) {
const token = Authorization.replace('Bearer ', '')
const { userId } = jwt.verify(token, APP_SECRET)
return userId
}
throw new Error('Not authenticated')
}
module.exports = {
APP_SECRET,
getUserId,
}
APP_SECRET是用来签署发布给用户的JWT token的加密签名信息。它和prisma.yml中的secret没有任何关系。即如果你要更换数据库层的实现,那么APP_SECRET将继续以完全相同的方式工作,没有影响。prisma.yml中的secret是用来限制访问prisma GraphQLAPI的;而APP_SECRET是发给用于的身份信息,用户每次发送请求携带token便可以验证用户的身份。用户身份验证,及访问都是在App层,不会涉及到prisma database层。
getUserId函数是一个辅助函数,您将在需要验证用户的解析器中调用。它首先从传入的HTTP请求中检索Authorization标头(包含User's JWT)。然后它验证JWT并从中检索用户的ID。请注意,如果该进程因任何原因未成功,该函数将抛出异常。因此,您可以使用它来“保护”需要身份验证的解析程序。
最后,您需要将所有内容导入Mutation.js
const { APP_SECRET } = require('../utils')
然后修改post,必须要验证的用户才能增加link,并且在增加的link中写入postBy的相关信息
function post(root,args,context,info)=>{
const userId = getUserId(context)
return context.db.mutation.createLink({
url:args.url,
description:args.description,
postBy:{
connect:{id:userId}
}
},info)
}
首先:使用getUserId从context中解析出userID.如果没有成功解析出id,就会爆出异常。
其次:我们使用connect userId来连接一个已经存在的User对象。
查看connect mutation
https://www.prisma.io/docs/reference/prisma-api/mutations-ol0yuoz6go/#overview
4、在index.js中使用新的解析器
const Query = require('./resolvers/Query')
const Mutation = require('./resolvers/Mutation')
const AuthPayload = require('./resolvers/AuthPayload')
const resolvers={
Query,
Mutation,
AuthPayLoad
}
重启服务器:
node src/index.js
重新打开playground
graphql playground
5、在playground中测试一下
新增一个用户
mutation {
signup(
name: "Alice"
email: "alice@graph.cool"
password: "graphql"
) {
token
user {
id
}
}
}
从服务器的响应中,复制身份验证token并在Playground中打开另一个选项卡。在新选项卡中,打开左下角的HTTP HEADERS窗格并指定Authorization标题 - 类似于之前对Prisma Playground所做的操作。再次,用实际令牌替换__TOKEN__。每个客户端请求都必须带上token.
mutation {
post(
url: "www.graphql-europe.org"
description: "Europe's biggest GraphQL conference"
) {
id
}
}
模拟客户端登陆后可以发表新的Link了。
要验证一切正常,可以使用login验证:
mutation {
login(
email: "alice@graph.cool"
password: "graphql"
) {
token
user {
email
links {
url
description
}
}
}
}
好了,现在登陆和验证功能就全部实现了。
浙公网安备 33010602011771号