User Authentication, JWTs, and Session Management
01_JSON Web Tokens
npm install jsonwebtoken --save
- 增加dev-server/services/auth-service.js
import jwt from 'jsonwebtoken';
export function generateJWT(user){
const tokenData = { username: user.username, id: user._id };
return jwt.sign({ user: tokenData }, process.env.TOKEN_SECRET );
}
- 修改dev-server/config/env.js,增加process.env.TOKEN_SECRET
import express from "express";
import morgan from "morgan";
import cors from "cors";
import bodyParser from "body-parser";
export function setEnvironment(app) {
if (process.env.NODE_ENV !== "production") {
setDevEnv(app);
} else {
setProdEnv(app);
}
}
function setDevEnv(app) {
// console.log("setting development environment");
process.env.NODE_ENV = "development";
process.env.DB_URL = "mongodb://localhost:27017/mevn-dev-db";
process.env.TOKEN_SECRET = "my-development-secret";
app.use(bodyParser.json());
app.use(morgan("dev"));
app.use(cors());
}
function setProdEnv(app) {
process.env.DB_URL = "mongodb://localhost:27017/mevn-prod-db";
process.env.TOKEN_SECRET = "my-production-secret";
app.use(bodyParser.json());
app.use(express.static(__dirname + "/../dist"));
console.log("setting production environment");
}
修改dev-server\api\auth\auth-controller.js,为认证的用户返回token
import * as StringUtil from "../../utilities/string-util";
import User from "../../model/user-model";
import bcrypt from "bcrypt-nodejs";
import { generateJWT } from "../../services/auth-service";
export function index(req, res) {
// console.log(req.body, req.body.username.toLowerCase());
const validation = validateIndex(req.body);
if (!validation.isValid) {
return res.status(400).json({ message: validation.message });
}
User.findOne({ username: req.body.username.toLowerCase() }, (error, user) => {
console.log(user);
if (error) {
return res.status(500).json();
}
if (!user) {
return res.status(401).json();
}
/* const passwordMatch = true;
babel编译调用错误
const passwordMatch = User.passwordMatches( req.body.password, user.password );
验证密码是否正确 */
const passwordMatch = bcrypt.compareSync(req.body.password, user.password);
if (!passwordMatch) {
return res.status(401).json({ message: "User not exist!" });
}
// 为认证的用户返回token
const token = generateJWT(user);
return res.status(200).json({ token: token });
});
}
function validateIndex(body) {
let errors = "";
if (StringUtil.isEmpty(body.username)) {
errors += "Username is required. ";
}
if (StringUtil.isEmpty(body.password)) {
errors += "Password is required. ";
}
return {
isValid: StringUtil.isEmpty(errors),
message: errors,
};
}
使用Postman,测试认证的用户返回token
02 Vue.js and JSON Web Tokens
修改src\services\AuthService.js, 替换原来的fakeToken,使用服务端返回的token
import store from "../store";
import http from "./HttpService";
export function isLoggedIn() {
const token = localStorage.getItem("token");
return token != null;
}
/* export function login() {
const token = {
username: "morpheus",
};
setToken(token);
} */
export function login(user) {
return http()
.post("/auth", user)
.then((res) => {
if (res) {
/* const fakeToken = {
token: "my-token",
};
setToken(fakeToken);
*/
setToken(res.data.token);
}
});
}
function setToken(token) {
// localStorage.setItem("token", JSON.stringify(token));
localStorage.setItem("token", token); // 已经是文本,无需JSON.stringify
store.dispatch("authenticate");
}
export function getUsername() {
return "morpheus";
}
export function getUserId() {
return 1;
}
export function registerUser(user) {
return http().post("/register", user);
}
浏览器测试
03 User Authentication
- 修改src/services/AuthService.js, 增加logout方法
export function logout() {
localStorage.clear();
store.dispatch("authenticate");
}
- 修改src\components\Navbar.vue, 增加条件判断显示导航菜单
<template>
<header>
<nav class="navbar navbar-expand-md navbar-dark fixed-top custom-bg-dark">
<router-link to="/" class="navbar-brand">
<img style="max-height:25px" src="../assets/logo.png"/>
Task Manager
</router-link>
<!-- <a class="navbar-brand" href="#">
<img style="max-height:25px" />
Task Manager
</a> -->
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<!-- <ul class="navbar-nav mr-auto"> -->
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<router-link to="/" class="nav-link" exact>Home</router-link>
</li>
<li v-if="$store.state.isLoggedIn" class="nav-item">
<router-link to="/tasks" class="nav-link" exact>Tasks</router-link>
<!-- <a class="nav-link" href="#">Link</a> -->
</li>
<li v-if="!$store.state.isLoggedIn" class="nav-item">
<router-link to="/register" class="nav-link" exact>Register</router-link>
</li>
<li v-if="!$store.state.isLoggedIn" class="nav-item">
<router-link to="/login" class="nav-link" exact>Login</router-link>
</li>
<li v-if="$store.state.isLoggedIn" class="nav-item">
<a v-on:click.prevent="logout()" class="nav-link" href="#">Logout</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
{{ this.$store.state.username ?
this.$store.state.username : 'User' }}
</a>
</li>
</ul>
</div>
</nav>
</header>
</template>
<script>
import * as auth from '../services/AuthService';
export default {
name: 'Navbar',
methods: {
logout: function(){
auth.logout();
this.$router.push({ name: 'home' });
}
}
}
</script>
修改src\router\index.js,替换原来的fake字段,const isLoggedIn = true;
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import Login from "../views/authentication/Login.vue";
import Register from "../views/authentication/Register.vue";
import TasksAll from "../views/tasks/TasksAll.vue";
import TasksCreate from "../views/tasks/TasksCreate.vue";
import TasksEdit from "../views/tasks/TasksEdit.vue";
import * as auth from "../services/AuthService";
Vue.use(VueRouter);
// const isLoggedIn = true;
const routes = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home,
},
{
path: "/tasks",
name: "tasks-all",
component: TasksAll,
beforeEnter: (to, from, next) => {
// if (isLoggedIn) {
if (auth.isLoggedIn()) {
next();
} else {
next("/login");
}
},
},
{
path: "/tasks/new",
name: "tasks-create",
component: TasksCreate,
beforeEnter: (to, from, next) => {
if (auth.isLoggedIn()) {
next();
} else {
next("/login");
}
},
},
{
path: "/tasks/:id",
name: "tasks-edit",
component: TasksEdit,
beforeEnter: (to, from, next) => {
if (auth.isLoggedIn()) {
next();
} else {
// 没有登录跳转到登录页面
next("/login");
}
},
},
{
path: "/register",
name: "register",
component: Register,
beforeEnter: (to, from, next) => {
if (!auth.isLoggedIn()) {
// 没有登录跳转到注册页面
next();
} else {
next("/");
}
},
},
{
path: "/login",
name: "login",
component: Login,
beforeEnter: (to, from, next) => {
if (!auth.isLoggedIn()) {
next();
} else {
next("/");
}
},
},
{
path: "*",
redirect: "/",
},
],
linkActiveClass: "active",
});
// router guards
/*
routes.beforeEach((to, from, next) => {
if (auth.isLoggedIn()) {
next();
} else {
next("/login");
}
});
*/
export default routes;
04 Validating a User's Session
-
修改dev-server/services/auth-service.js, 增加方法requireLogin,decodeToken
import jwt from "jsonwebtoken";
export function generateJWT(user) {
const tokenData = { username: user.username, id: user._id };
return jwt.sign({ user: tokenData }, process.env.TOKEN_SECRET);
}
export function requireLogin(req, res, next) {
const token = decodeToken(req);
if (!token) {
return res.status(401).json({ message: "You mast be logged in." });
}
next();
}
export function decodeToken(req) {
const token = req.headers.authorization || req.headers["authorization"];
if (!token) {
return null;
}
try {
return jwt.verify(token, process.env.TOKEN_SECRET);
} catch (error) {
return null;
}
}
- 修改dev-server\api\task\tasks-routes.js, 增加登录验证
import express from "express";
const router = express.Router();
import * as controller from "./tasks-controller";
import * as auth from "../../services/auth-service";
router.post("/task", auth.requireLogin, controller.create);
router.get("/task", auth.requireLogin, controller.index);
router.get("/task/:id", auth.requireLogin, controller.show);
router.put("/task", auth.requireLogin, controller.update);
router.delete("/task", auth.requireLogin, controller.remove);
export default router;
- 使用postman测试get http://localhost:3000/api/task
没有token,请求会失败,Headers中设置token才能请求到task
- 修改/src/services/AuthService.js, 增加方法getToken
export function getToken(){
return localStorage.getItem('token');
}
- 修改src\services\HttpService.js,增加保存token到header中
import store from "../store";
import axios from "axios";
import * as auth from "./AuthService";
export default function http() {
return axios.create({
baseURL: store.state.apiUrl,
headers: {
Authorization: auth.getToken(),
},
});
}
05 Managing User's Session
- 修改dev-server/services/auth-service.js,增加方法getUsername,getUserId
import jwt from "jsonwebtoken";
export function generateJWT(user) {
const tokenData = { username: user.username, id: user._id };
return jwt.sign({ user: tokenData }, process.env.TOKEN_SECRET);
}
export function requireLogin(req, res, next) {
const token = decodeToken(req);
if (!token) {
return res.status(401).json({ message: "You mast be logged in." });
}
next();
}
export function decodeToken(req) {
const token = req.headers.authorization || req.headers["authorization"];
if (!token) {
return null;
}
try {
return jwt.verify(token, process.env.TOKEN_SECRET);
} catch (error) {
return null;
}
}
export function getUsername(req) {
const token = decodeToken(req);
if (!token) {
return null;
}
return token.user.username;
}
export function getUserId(req) {
const token = decodeToken(req);
if (!token) {
return null;
}
return token.user.id;
}
- 修改dev-server/api/task/tasks-controller.js, 把测试的id,替换成从token中获取的id
import User from "../../model/user-model";
import Task from "../../model/task-model";
import moment from "moment";
import * as auth from "../../services/auth-service";
export function index(req, res) {
// FIND ALL TASKS
Task.find({}, (error, tasks) => {
if (error) {
return res.status(500).json();
}
return res.status(200).json({ tasks: tasks });
}).populate("author", "username", "user");
}
export function create(req, res) {
/* CREATE TASK
const id = 10; */
const userId = auth.getUserId(req);
User.findOne({ _id: userId }, (error, user) => {
if (error && !user) {
return res.status(500).json();
}
const task = new Task(req.body.task);
task.author = user._id;
task.dueDate = moment(task.dueDate);
task.save((error) => {
if (error) {
return res.status(500).json();
}
return res.status(201).json();
});
});
}
export function update(req, res) {
/* UPDATE TASKS
const id = 10; */
const userId = auth.getUserId(req);
User.findOne({ _id: userId }, (error, user) => {
if (error) {
return res.status(500).json();
}
if (!user) {
return res.status(404).json();
}
const task = req.body.task;
task.author = user._id;
task.dueDate = moment(task.dueDate);
Task.findByIdAndUpdate({ _id: task._id }, task, (error) => {
if (error) {
return res.status(500).json();
}
return res.status(204).json();
});
});
}
export function remove(req, res) {
/* Delete a task
const id = 5; */
const userId = auth.getUserId(req);
Task.findOne({ _id: req.params.id }, (error, task) => {
if (error) {
return res.status(500).json();
}
if (!task) {
return res.status(404).json();
}
if (task.author._id.toString() !== userId) {
return res
.status(403)
.json({ message: "Not allowed to delete another user's post" });
}
Task.deleteOne({ _id: req.params.id }, (error) => {
if (error) {
return res.status(500).json();
}
return res.status(204).json();
});
});
}
export function show(req, res) {
// GET TASK BY ID
Task.findOne({ _id: req.params.id }, (error, task) => {
if (error) {
return res.status(500).json();
}
if (!task) {
return res.status(404).json();
}
return res.status(200).json({ task: task });
});
}
- 修改src/services/AuthService.js,增加decodeToken方法,替换getUsername,getUserId中的假数据
import store from "../store";
import http from "./HttpService";
import jwt from "jsonwebtoken";
export function isLoggedIn() {
const token = localStorage.getItem("token");
return token != null;
}
/* export function login() {
const token = {
username: "morpheus",
};
setToken(token);
} */
export function login(user) {
return http()
.post("/auth", user)
.then((res) => {
if (res) {
/* const fakeToken = {
token: "my-token",
};
setToken(fakeToken);
*/
setToken(res.data.token);
}
});
}
function setToken(token) {
// localStorage.setItem("token", JSON.stringify(token));
localStorage.setItem("token", token); // 已经是文本,无需JSON.stringify
store.dispatch("authenticate");
}
export function logout() {
localStorage.clear();
store.dispatch("authenticate");
}
export function getUsername() {
// return 'morpheus';
const token = decodeToken();
if (!token) {
return null;
}
return token.user.username;
}
export function getUserId() {
// return 1;
const token = decodeToken();
if (!token) {
return null;
}
return token.user.id;
}
export function registerUser(user) {
return http().post("/register", user);
}
export function getToken() {
return localStorage.getItem("token");
}
function decodeToken() {
const token = getToken();
if (!token) {
return null;
}
return jwt.decode(token);
}