JSON-WEB-TOKEN
用户的登录验证我们采用目前比较火的JSON-WEB-TOKEN
验证方式,相比于基于 cookie 或者 session 的登录验证 jwt 是真正的无状态请求,不占用服务器的资源,不存在跨域的问题,简单说说
jwt 的原理:
- 用户输入账号密码登录,会把账户密码(密码用 md5 加密)发送给后端
- 后端验证用户的账号和密码信息,如果匹配,就返回一个 TOKEN 给客户端;如果验证失败,就返回验证错误信息
- 登录成功成功后,客户端会将服务器返回的 TOKE 保存下来(SessionStorage、LocalStorage),之后要请求其他资源的时候,在请求头(Header)里带上这个 TOKEN 进行请求
- 后面服务器在收到客户端的请求时,会先验证一下 TOKEN 是否有效,有效则返回请求的资源,无效则返回验证错误
更详细的关于
JSON-WEB-TOKEN
的介绍,可参考阮一峰的博客 通过这个 TOKEN 的方式,客户端和服务端之间的访问,是无状态的:也就是服务端不知道你这个用户到底还在不在线,只要你发送的请求头里的 TOKEN 是正确的我就给你返回你想要的资源。这样能够不占用服务端宝贵的空间资源,而且如果涉及到服务器集群,如果服务器进行维护或者迁移或者需要 CDN 节点的分配的话,无状态的设计显然维护成本更低。 安装jwt
:
yarn add koa-jwt
在models
里的user.js
新加一个通过用户名查找用户的方法:
// models/user.js
// ......
// 前面的省略了
// 新增一个方法,通过用户名查找
const getUserByName = async function(name) {
const userInfo = await User.findOne({
where: {
username: name
}
})
return userInfo
}
export default {
getUserById, // 导出getUserById的方法,将会在controller里调用
getUserByName
}
接着修改controllers
里的user.js
:
// controllers/user.js
// ......
// 前面的省略了
// 新增一个方法,通过用户名查找
const getUserAuth = async function(ctx, next) {
const data = ctx.request.body // post过来的数据存在request.body里
const userInfo = await user.getUserByName(data.name)
if (userInfo != null) {
// 如果查无此用户会返回null
if (userInfo.password != data.password) {
ctx.response.body = {
success: false, // success标志位是方便前端判断返回是正确与否
info: '密码错误!'
}
} else {
// 如果密码正确
const userToken = {
name: userInfo.username,
id: userInfo.id
}
const secret = 'vue-koa-demo' // 指定密钥,这是之后用来判断token合法性的标志
const token = jwt.sign(userToken, secret) // 签发token
ctx.response.body = {
success: true,
token: token // 返回token
}
}
} else {
ctx.response.body = {
success: false,
info: '用户不存在!' // 如果用户不存在返回用户不存在
}
}
}
提示
userToken
里面的内容是可以自定义的,加密后存储在 token 里返回给前端,前端如果想拿到 token 里面的内容,需要解码,安装koa-jwt
,从koa-jwt
导入jwt
,然后调用jwt.decode(token)
拿到userToken
里面的内容。
更新一下router.js
规则:
//router.js
import KoaRouter from 'koa-router'
import UserController from './../controllers/user.js'
const router = KoaRouter()
export default function(app) {
router.post('/user/:id', UserController.getUserInfo)
router.post('/api/user', UserController.getUserAuth)
app.use(router.routes()).use(router.allowedMethods())
}
后端 API 写完了,我们开始写前端请求
Axios
axios是一个基于 Promise 用于浏览器和 nodejs 的 HTTP 客户端,非常火,在 github 上已经有了将近 50000 个 star 了,具体的使用方法大家可以参考官网,文档写的非常详细。先安装一下axios
:
yarn add axios
在src\main.js
里引入 axios,
// scr/main.js
// ...
import Axios from 'axios'
Vue.prototype.$http = Axios // 绑定到Vue的$http实例上
// ...
修改Login.vue
,编写登录方法handleSubmit
:
// Login.vue
// 省略前面的部分
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
let obj = {
name: values.username,
password: values.password
}
this.$http.post('/api/user', obj) // 将信息发送给后端
.then((res) => { // axios返回的数据都在res.data里
if(res.data.success){ // 如果成功
sessionStorage.setItem('demo-token',res.data.token) // 用sessionStorage把token存下来
message.success('登录成功')
this.$router.push('/') // 进入后台管理页面,登录成功
} else {
this.$message.error(res.data.info) // 登录失败,显示提示语
sessionStorage.setItem('demo-token',null) // 将token清空
}
}, (err) => {
message.error('请求错误!')
sessionStorage.setItem('demo-token',null)// 将token清空
})
}
})
}
}
}
</script>
登录框里的username
和password
的值,antd
这个框架都帮我们封装在了this.form.validateFields((err, values)
里的values
这个参数里面了。
bcryptjs-密码加密
以前,我们习惯前端用 md5 给信息加密再提交给后端,但是后来发现这种方式并不安全,因为 html 是明文传输,即使是加密后的数据也能被第三方截取,他们可以直接拿着加密后的信息去后端请求数据;所以前端加密并不太靠谱,最安全的方式是采用https传输协议
,加密的步骤放在后端。为什么需要后端加密呢?假如你登录过的某个网站数据库被攻击导致数据泄露,而你的密码是以明文保存在数据库的,那么黑客就可以拿你的密码去撞库,因为我们经常好几个网站的密码都是相同的,那意味着一个网站被攻击,你其他的网站的数据也不安全了。
安装 bcryptjs:
yarn add bcryptjs
在controllers/user.js
引入bcryptjs
//controllers/user.js
import bcrypt from 'bcryptjs'
// 省略前面的部分
if (userInfo != null) { // 如果查无此用户会返回null
if (!bcrypt.compareSync(data.password, userInfo.dataValues.password)) { // 验证密码是否正确
ctx.response.body = {
success: false, // success标志位是方便前端判断返回是正确与否
info: '密码错误!'
}
} else { // 如果密码正确
// 省略后面的部分
然后我们需要把我们数据库里123
这个明文密码 bcrypt 化,加密后变为:$2a$10$x3f0Y2SNAmyAfqhKVAV.7uE7RHs3FDGuSYw.LlZhOFoyK7cjfZ.Q6
,替换数据库里的123
.
提示
如果我们做注册功能,用户注册成功后提交到后端的密码,需要我们用 bcrypt 加密后再存到数据库里,不要把密码等敏感信息用明文的方式存到数据库里
跨域
在进行前后端联调的时候,我们还得解决跨域的问题。像现在,我们的 koa 服务器跑在3000
端口,而前端是跑在webpack
为我们提供的8080
端口,端口不一样,存在跨域的情况,没法直接传输数据。解决跨域通信的方法常用的两种方法:
- 服务端在请求头上加上
CORS
允许跨域,客户端即可用类似axios
这样的工具跨域发送请求 - 变成同域
如果使用第一种方法的话,可以借助koajs这个中间件
如果前端人员也能自己解决跨域的问题,那我们就没必要麻烦后端了,这里我们选用第二种办法。
打开根目录下
config/index.js
文件,找到dev
下的proxyTable
,利用这个proxyTable
这个代理工具,把外部的请求通过 webpack 转发给本地,也就把跨域请求变成同域请求了:
//config/index.js
前面省略
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
},
省略...
proxyTable
的作用是:当我们在组件里发出的请求地址是/api/xxxx 的时候,实际上请求的是http://localhost:3000/api/xxxx,由于webpack帮我们代理了localhost的3000端口的服务,所以我们可以把实际是跨域的请求当做是同域下的接口来调用。
然后重新启动一下前端webpack
服务,先ctrl+c
退出当前进程,然后yarn dev
或者npm run dev
,输入网址http://localhost:8080/#/login
,输入账号test
,密码123
,然后登录:

登录成功,我们打开浏览器的后台查看Session Storage
:

后端返回给我们的token
也已经被保存下来了。
路由拦截
在前端服务刚启动的时候或者手动修改地址栏的地址改成http://localhost:8080/
也会跳转到管理页面,这与我们的需求违背,我们需要用户登录成功后才能跳转到管理后台:

这里就要用到后端给我们传回来的token
,有 token 就说明我们的身份是经过验证的,可以跳转到后台管理页面,否则就是非法的。我们用vue-router
做一下前端的全局拦截,打开src/main.js
,加入以下内容:
//src/main.js
前面省略
Vue.config.productionTip = false
router.beforeEach((to, from, next) => {
const token = sessionStorage.getItem('demo-token')
if (to.meta.requiresAuth) {
if (token !== 'null' && token != null) {
//判断是否存在 token
next()
} else {
message.warning('请先登录')
next('/login')
}
} else {
next()
}
})
打开src/router/index.js
,改成如下内容:
//src/router/index.js
前面省略
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld,
meta: {requiresAuth: true} //表示进入HelloWorld页面是需要验证的
},
{
path: '/login',
name: 'Login',
component: Login
}
重启服务器npm run dev
,或者直接清除浏览器缓存刷新网页,保证token
被清除掉就行,会发现在未登录的情况下,无法跳转到后台管理页面,说明拦截功能生效了。
