后端登录
搭建http服务
搭建https服务的话:搭建https服务
http服务我们前面其实已经搭好了
现在把端口换一下 18082
因为访问47.103.29.206:18082的话会报错,因为已经注册过了?? 所以我后面加了个a
const express=require('express')
const router=require('./router')
const app=express()
app.use('/',router)
app.listen(18082,()=>{
console.log("http://localhost:18082");
})
登录api开发
在user里创建一个api
随便写点测试一下
router.post('/login',(req,res)=>{
console.log(req.body);
res.json({
code:0,
msg:'登录成功'
})
})
因为浏览器不好做post请求测试
这里可以用postman
也可以下载一个curl在dos窗口
或者git bash里面输入也可以
curl http://47.103.29.206a:18082/user/login -X POST
输出req.body为undifined 说明并没有传入参数
可以通过-d来指定body里的参数
curl http://47.103.29.206a:18082/user/login -X POST -d 'username=admin&password=1234'
但是body里面的参数仍然没有被解析 需要用到body-parser中间件来解决这个问题
安装body-parser cnpm i -S body-parser
关于body-parser的用法 :https://juejin.cn/post/6844903478830055431
不用下这个包了 已经被弃用了
直接用express调用bodyParser的方法就可以了
发的请求可以简化一下 ,加上-d以后他默认是post
curl http://47.103.29.206a:18082/user/login -d 'username=admin&password=1234'
记得把前端的baseAPI也改了
然后运行一下前端项目
到时候上线了就把a给去掉
跨域问题
所以在服务端写个解决跨域的方案
可以看到参数已经传进来了
同时还收到了返回值
这时候前端后端已经发生了联动了 可以互通了
这里我们在 Network 中会发现发起了两次 https 请求,这是因为由于触发跨域,所以会首先进行 OPTIONS 请求,判断服务端是否允许跨域请求,如果允许才能实际进行请求
也就是说跨域请求的访问是由服务端控制的,服务端下发了这样的response header之后,
浏览器就知道是可以发送请求的,如果跨域条件不满足的时候,浏览器就直接抛出异常
响应结果封装
const {
CODE_ERROR,
CODE_SUCCESS
} = require('../utils/constant')
// new Result
class Result {
// data:向前端返回的数据 msg:向前端返回的信息
constructor(data, msg = '操作成功', options) {
this.data = null
// 一个都没传
if (arguments.length === 0) {
this.msg = '操作成功'
// 传入一个参数 认为他是个msg
} else if (arguments.length === 1) {
this.msg = data
} else {
// 传入2/3个参数
this.data = data
this.msg = msg
if (options) {
this.options = options
}
}
}
createResult() {
//没有code默认code为success
if (!this.code) {
this.code = CODE_SUCCESS
}
// 基础的结构是一个code 一个msg
let base = {
code: this.code,
msg: this.msg
}
// data存在就增加一个data
if (this.data) {
base.data = this.data
}
// 如果还有额外的options 就加到base里
if (this.options) {
base = { ...base, ...this.options }
}
console.log(base)
return base
}
json(res) {
//通过res.json返回给前端
res.json(this.createResult())
}
success(res) {
this.code = CODE_SUCCESS
this.json(res)
}
fail(res) {
this.code = CODE_ERROR
this.json(res)
}
}
module.exports = Result
还要来改造一下user.js
router.post('/login',(req,res)=>{
console.log(req.body);
new Result('登录成功').success(res)
})
做失效功能的时候只需要增加一个方法就行 扩展能力很好
如果success里面出现了异常,可以在index.js的异常处理语句中被处理
mysql
密码是用的md5加盐
我们需要在node中安装mysql数据库
建一个db文件夹 专门存储db的相关操作
里面包含两个文件 config.js index.js
config.js:
module.exports={
host:'localhost',
user:'root',
password:'111111',
database:'book'
}
index.js
const mysql = require('mysql')
const config = require('./config')
function connect() {
return mysql.createConnection({
...config
})
}
function querySql(sql) {
const conn = connect()
return new Promise((resolve, reject) => {
try {
conn.query(sql, (err, results) => {
if (err) {
reject(err)
}
resolve(results)
})
}
catch (e) {
reject(e)
}
finally {
conn.end()
}
})
}
module.exports=querySql
user.js引入
const querySql=require('../db/index')
querySql('select * from admin_user').then((res)=>{
console.log(res);
}).catch((err)=>{
console.log(err);
})
有个技巧,我们可以在关键的地方打印一些参数, 在constant里面写个debug为true
然后在需要输出的页面里引入debug
debug&&console.log(xxxx)
在上线的时候我们就可以把debug改成false 就不会再打印了
这是在db/index.js里面写的, 就不用再业务逻辑里面打印了
这个配置文件可以从代码中抽离出来,变成一个单独的配置文件,这样就可以在不启动代码的情况下实现debug的切换
有了这个查询语句 ,就可以对用户查询进一步封装
我们是直接将查询语句写在user.js里了
更好的做法是再建一个service层 把业务逻辑全放到service层
创建service 里面包含了个user.js
创建一个function 叫login
const querySql=require('../db/index')
function login(username,password){
return querySql(`select * from admin_user where username='${username}' and password='${password}'`)
}
module.exports=login
在router/user.js
const { username, password } = req.body
login(username, password).then((user) => {
if (!user || user.length == 0) {
new Result('登录失败').fail(res)
} else {
new Result('登录成功').success(res)
}
})
这时,即使我们输入正确的用户名密码也会出现无法登陆的情况
这是因为密码采用了MD5+SALT加密,所以需要对密码进行对等加密。
去constant文件
加上这句话
PWD_SALT:'admin_imooc_node'
这个就相当于一个秘钥,通过秘钥和密码混合之后生成新的密码
不知道秘钥的话外界要破解这个密码是很难破解的
接着对密码进行MD5加密
cnpm i -S crypto
新建文件utils/index.js 并写入
const crypto=require('crypto')
function md5(s){
return crypto.createHash('md5').update(String(s)).digest('hex')
}
module.exports=md5
接着要对password进行改造
password=md5(` p a s s w o r d {password} password{PWD_SALT}`)
express-validator
https://express-validator.github.io/docs/validation-chain-api.html
https://blog.csdn.net/cuk0051/article/details/108343329
用处:可以简化对post请求的校验
可以帮助我们快速的验证表单当中的参数
安装依赖
使用它的body方法,在post请求当中,需要在第二个参数当中传入一个数组,通过数组来进行校验,
如何接受到参数呢
这里的err有个isEmpty方法,这个方法可以去判断数组是否为空
如果isEmpty为false表示出现了验证错误,我们就可以从errors里面拿到msg然后返回给前端抛出异常
然后通过第三个参数next来继续传递,传递给下一个中间件来执行
这里的[ ]=err.errors代表取出第一个元素
去前端测试一下
注意next 传递给下一个中间件,下一个中间件是
express-validator 使用技巧:
- 在
router.post
方法中使用 body 方法判断参数类型,并指定出错时的提示信息 - 使用
const err = validationResult(req)
获取错误信息,err.errors
是一个数组,包含所有错误信息,如果err.errors
为空则表示校验成功,没有参数错误 - 如果发现错误我们可以使用
next(boom.badRequest(msg))
抛出异常,交给我们自定义的异常处理方法进行处理
jwt
https://www.youbaobao.xyz/admin-docs/guide/extra/jwt.html
https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
https://github.com/dwyl/learn-json-web-tokens/blob/master/README.md
基本概念
Token 的用途主要有三点:
- 拦截无效请求,降低服务器处理压力;
- 实现第三方 API 授权,无需每次都输入用户名密码鉴权;
- 身份校验,防止 CSRF 攻击
可以在https://jwt.io/ 调试jwt字符串
有三段,第一段是加密的算法和token的类型,第二段是具体数据,第三段是签名部分,使用加密算法
私钥是存在服务端的,别人不知道私钥的时候是无法破解里面的信息的,最后解开的时候是要用到私钥的。
生成jwt token
npm i -S jsonwebtoken
需要一个私钥和一个过期时间,过期时间不宜过短,也不宜过长,课程里设置为 1 小时,实际业务中可根据场景来判断,通常建议不超过 24 小时,保密性要求高的业务可以设置为 1-2 小时:
const jwt = require('jsonwebtoken')
const { PRIVATE_KEY, JWT_EXPIRED } = require('../utils/constant')
login(username, password).then(user => {
if (!user || user.length === 0) {
new Result('登录失败').fail(res)
} else {
const token = jwt.sign(
{ username }, //传入的数据
PRIVATE_KEY, //秘钥
{ expiresIn: JWT_EXPIRED } //过期时间
)
new Result({ token }, '登录成功').success(res)
}
})
前端代码改造
utils/require.js
对响应拦截器做修改
这里的message也改成msg return Promise.reject(new Error(res.msg || 'Error'))
然后是views/login/index.vue
找到具体做登录动作的方法handleLogin
他会进入到user/login这个action
看一下这里的data 后面serToken让token保存起来了
token是保存起来了,但是运行起来报错,这都是因为/info接口还没有开发
来分析一下
进入到permission.js 全局守卫
因为去的是dashboard所以走else
之后进入到user/getInfo
又调用了一个getInfo方法
请求了后端的/user/info接口
我们还没有开发,所以后面的路都走不通了。 我们看看他的/info的框架是怎么实现的
他是直接带上token参数,我们后面要改成request里面包含token信息,拿到token信息,服务端就可以解析出用户信息,因为token信息当中有个username,我们把username解出来之后根据username查到用户信息,再返回给前端,整个实现流程是这样的状态,所以我们下面要在服务端添加一个jwt的认证,我们刚刚是生成了token,现在要来验证一下token是否在有效期范围内。
jwt认证
https://www.cnblogs.com/zkqiang/p/11810203.html
安装:npm i -S express-jwt
他的主要功能是检查所有的路由,判断当前时间是否在过期时间内,当路由中包含了没有过期的token,就可以判定为通过,
新建一个中间件router/jwt.js
这个中间件的主要用途就是做验证
这个地方卡了我好久 不是user/login而是/user/login
const expressJwt = require('express-jwt');
const { PRIVATE_KEY } = require('../utils/constant');
const jwtAuth = expressJwt({
secret: PRIVATE_KEY,
credentialsRequired: true // 设置为false就不进行校验了,游客也可以访问
}).unless({
path: [
'/',
'/user/login'
], // 设置 jwt 认证白名单
});
module.exports = jwtAuth;
在index.js中加入这个中间件
const jwtAuth = require('./jwt')
// 注册路由
const router = express.Router()
// 对所有路由进行 jwt 认证
router.use(jwtAuth)
之后去浏览器看看
有个报错 algorithms should be set
解决:https://iseeu.blog.csdn.net/article/details/108641110
algorithms:[‘HS256’]
抛出500错误,说明拦截已经生效了 ,因为在header里面他没有找到token,所以报错了
现在去服务端做一些处理 现在发现错误以后返回的都是-1,但是我们希望token的错误会有另外的值比如-2
在constant.js 增加:CODE_TOKEN_EXPIRED:-2
当出现token错误的时候会打印出这个
老师的是这样的,他是用name标识token错误的,我这儿没有,就用code标识吧
还有,我们以前封装了Result,可以改造一下,通过Result快速生成错误信息
在Result里新建一个方法
点击登录
对前端代码再进行改造
request-相应拦截器-error
这里有个技巧 如何看到error的详细信息
这样就能弹出自定义的msg
服务端else这里的代码可以用Result改造一下
目前,会出现token验证的错误,为了解决这个错误,我们需要将header里增加一个authorization(jwt规定的)
去request的请求拦截器:
这里的user/info其实不需要带参数
开发user/info接口
在服务端部分,在数据库index.js里增加一个queryOne的方法
function queryOne(sql){
return new Promise((resolve,reject)=>{
querySql(sql).then((res)=>{
if(res&&res.length>0){
resolve(res[0])
}else{
resolve(null)
}
}).catch(err=>{
reject(err)
})
})
}
service/user.js(把login.js重命名了 变成user.js):
function findUser(username){
return queryOne(`select * from admin_user where username=${username}`)
}
router/user.js
router.get('/info', (req, res, next) => {
// res.send('user/info')
findUser('admin').then((user) => {
console.log(user); //不需要处理异常,因为默认情况会抛给自定义异常处理
if (user) {
new Result(user, '用户信息查询成功').success(res)
} else {
new Result('用户信息查询失败').fail(res)
}
})
})
因为不想让他出现password 所以查询语句要修改一下
function findUser(username){
return queryOne(`select id,username,nickname,role,avatar from admin_user where username='${username}'`)
}
因为前端获取的是roles不是role
所以在查询成功前面需要加上user.roles=[user.role]
router.get('/info', (req, res, next) => {
// res.send('user/info')
findUser('admin').then((user) => {
console.log(user); //不需要处理异常,因为默认情况会抛给自定义异常处理
if (user) {
user.roles=[user.role]
new Result(user, '用户信息查询成功').success(res)
} else {
new Result('用户信息查询失败').fail(res)
}
})
})
这样就可以进去了
继续对user/info接口进行改造 username不应该写死
username需要从token里拿,这就用到了对jwt进行解析
所以,我们要在header中拿到jwt并且对他进行解析
前端仅在 Http Header 中传入了 Token,如果通过 Token 获取 username 呢?这里就需要通过对 JWT Token 进行解析了,
需要用到jsonwebtoken里的verify方法
需要从http header里拿到authorization
在 /utils/index.js
中添加 decode 方法:
function decode(req){
const token=req.get('authorization')
return token
}
function decode(req){
let token=req.get('authorization')
if(token.indexOf('Bearer')===0){
token=token.replace('Bearer ','')
}
return jwt.verify(token,PRIVATE_KEY)
}
router.get('/info', (req, res, next) => {
const decoded=decode(req)
console.log({decoded});
// res.send('user/info')
if(decoded&&decoded.username){
findUser(decoded.username).then((user) => {
console.log(user); //不需要处理异常,因为默认情况会抛给自定义异常处理
if (user) {
user.roles=[user.role]
new Result(user, '用户信息查询成功').success(res)
} else {
new Result('用户信息查询失败').fail(res)
}
})
}else{
new Result('用户信息查询失败').fail(res)
}
})
登出
登出的时候显示接口不存在,其实登出的时候是不需要调用任何接口的
只需要将token清空,然后重定向一下
如何解决
用try-catch改造 这里不走他的接口了
logout({ commit, state, dispatch }) {
return new Promise((resolve, reject) => {
try {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resetRouter()
// reset visited views and cached views
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
dispatch('tagsView/delAllViews', null, { root: true })
resolve()
} catch (error) {
reject(error)
}
})
},
查询一下,发现是在NavBar里面被用到了
logout执行完以后他会跳到login页面
回顾
主要开发了两个api一个是登录api ,一个是获取用户信息api
前端请求api到服务端以后,服务端会检查jwt的白名单,在白名单的话直接调用controller,如果不在,进行jwt token的认证,是通过 jwt库 express-jwt来进行实现的,如果验证失败将返回401错误,验证成功将调用controller
在登录的时候会判断body参数,通过express-validator来进行判断,如果验证失败会调用boom.badRequest返回验证失败
之后会调用mysql数据库,写了两个方法,一个是login服务,一个是findUser服务,他们都是查询admin_User这张表去判定用户是否存在,如果是登录场景的话,会通过jwt生成一个token,将token返回给前端用户