這裏有一篇文章描述的已經很是詳盡,闡述了 JWT
驗證相比較傳統的持久化 session
驗證的優點,以及基於 angular
和 express
驗證的簡單流程。前端
Passport 提供了多種的驗證策略,如:webpack
passport-http-bearer - 使用 Bearer tokens
對 HTTP
請求作權限驗證。這個最適合咱們的項目不過了。git
passport-local - 本地驗證,普通的登錄驗證,數據庫密碼驗證成功便可。angularjs
此外還有 passport-github
, passport-weixin
, passport-qq
, passport-weibo
… ,這些你均可以在 官網 上找到。 github
咱們就採用這種方式來進行權限驗證。web
首先須要注意的是使用 Koa@2,Node的版本須要 7.X的版本以上,並且啓動時須要加上
--harmony
或者—harmony-async-await
最近Node 8.0
已經上線,我直接採用的是Node v8.0.0
nvm install 8.0.0
nvm alias default 8.0.0
mongodb
blog/server
基本的目錄結構數據庫
server ├─ bin / www # 入口文件 ├─ config # server配置文件 ├─ controller # 控制器文件夾 | └─ user.js ├─ lib | ├─ auth.js # 認證邏輯 | └─ db.js # 數據庫 鏈接等 ├─ models # Mongoose Models ├─ routes # Koa router ├─ utils # 工具方法 ├─ index.js └─ package.json
咱們在入口文件處 server/bin/www
來鏈接 MongoDB
express
(async () => { // 測試鏈接 MongoDB try { const info = await connect(dbConfig) console.log(`Success to connect to MongoDB at ${info.host}:${info.port}/${info.name}`) } catch (err) { console.error(err) process.exit() } // 開啓服務進程 try { app.listen(port) console.log(`Server is running at http://localhost:${port}`) } catch (err) { console.error(err) } })()
server/lib/db.js
下對應的 connect
方法
exports.connect = function (config) { return new Promise((resolve, reject) => { mongoose.connection .on('error', err => reject(err)) .on('close', () => console.log('MongoDB connection closed! ')) .on('open', () => resolve(mongoose.connections[0])) mongoose.connect(`mongodb://${config.host}:${config.port}/${config.database}`, config.options) }) }
在 server/config/index.js
增長 MongoDB
的配置
const base = { admin: { username: 'whistleyz', password: 'admin123', email: 'whistleyz@163.com', level: 51 // >50 超管 } } const dev = Object.assign(base, { db: { host: '127.0.0.1', port: 27017, database: 'fullblog', options: { user: '', pass: '' } } }) const prod = Object.assign(base, {}) const env = process.env.NODE_ENV || 'development' const _config = { development: dev, production: prod } // 數據庫配置 module.exports = _config[env]
因爲線上和咱們開發甚至是測試環境,配置都會有些許不一樣,咱們能夠用
process.env.NODE_ENV
來區分這些配置
新建 server/lib/auth.js
// serialize deserialize user objects into the session passport.serializeUser((user, done) => done(null, user.username)) passport.deserializeUser(async (username, done) => { const user = await UserModel.findOne({username}) done(null, user) }) /** * 基於 Bearer、Local 的認證方式 * 下面導出的路由中間件走的就是這裏的邏輯 * passport-http-bearer 會自動解析出 headers 中的 token * https://github.com/jaredhanson/passport-http-bearer/blob/master/lib/strategy.js#L89 */ passport.use(new BearerStrategy(async (token, done) => { try { console.log(token) const accessToken = await AccessToken.findOne({token}).populate('user') accessToken ? done(null, accessToken.user) : done(null, false, {type: 'error', message: '受權失敗!'}) } catch (err) { done(err) } })) /** * 默認從 req.body 或者 req.query 中取出 username, password 字段 * https://github.com/jaredhanson/passport-local/blob/master/lib/strategy.js#L49 */ passport.use(new LocalStrategy(async (username, password, done) => { try { const user = await UserModel.findOne({username}) if (user && user.validPassword(password)) { done(null, user) } else { done(null, false) } } catch (err) { done(err) } })) // 導出中間件 exports.isBearerAuthenticated = function () { return passport.authenticate('bearer', {session: false}) } exports.isLocalAuthenticated = function () { return passport.authenticate('local', {session: false}) } exports.passport = passport
新建 server/routes/api.js
:
const Router = require('koa-router') const User = require('../controllers/user') const { isBearerAuthenticated, isLocalAuthenticated } = require('../lib/auth') const router = new Router() router.use(async (ctx, next) => { try { await next() } catch (error) { console.error(error) ctx.status = 400 ctx.body = { code: error.code, message: error.message || error.errmsg || error.msg || 'unknown_error', error } } }) // 初始化用戶數據 User.seed() // Auth 認證 router.post('/auth', isLocalAuthenticated(), User.signToken) router.get('/auth', isBearerAuthenticated(), User.getUserByToken) module.exports = router.routes()
那麼咱們在 server/controller/user.js
下的處理邏輯久變得簡單:
// LocalStrategy 的中間件驗證經過,會把 user 儲存在 req 中 exports.signToken = async function (ctx, next) { const { user } = ctx.req // 從新請求 token 須要刪除上一次生成的 token await TokenModel.findOneAndRemove({user: user._id}) const result = await TokenModel.create({ // md5加密 token: genHash(user.username + Date.now()), user: user._id }) ctx.status = 200 ctx.body = { success: true, data: result } } // LocalStrategy 的中間件驗證Token有效,會把 user 儲存在 req 中 exports.getUserByToken = async function (ctx, next) { ctx.status = 200 ctx.body = { success: true, data: ctx.req.user } } // 當數據庫中user表示空的時候,建立超級管理員 exports.seed = async function (ctx, next) { const users = await UserModel.find({}) const adminInfo = config.admin if (users.length === 0) { const _admin = new UserModel(adminInfo) const adminUser = await _admin.save() } }
咱們能夠藉助
mongoose
還控制Token
的壽命
好比設置 7 天后過時,expires: 60 * 60 * 24 * 7
到這裏咱們的後端邏輯基本實現,爲了和前端 webpack-dev-server
本地服務器進行數據模擬,咱們能夠開啓 devServer
的 proyx
,以及開啓 koa
的跨域支持
task/config
:
config.devServer = { hot: true, contentBase: path.resolve(__dirname, '../dist'), publicPath: '/', proxy: { "/api/v1": "http://localhost:8082" } }
這樣,前端的任何 /api/v1
下的請求,都會被代理到 http://localhost:8082
而 8082
就是 koa
服務器的監聽端口。
// koa 跨域 const logger = require('koa-logger') const app = new koa() app.use(kcors())
在下一篇文章中,咱們會深刻 dva
的框架核心實現。咱們先來看看 dav
的基本使用
新建 src/model/app.js
import { doLogin, getUserByToken } from '../service/app' import { LocalStorage } from '../utils' import { message } from 'antd' export default { namespace: 'app', state: { isLogin: false, user: null }, subscriptions: {}, effects: { *checkToken({next}, {call, put}){ const Token = LocalStorage.getItem('token') if (Token) { yield put({type: 'loginSuccess'}) } else { message.error('你尚未登錄哦!') } }, *doLogin({payload}, {call, put}){ try { const { success, data } = yield call(doLogin, payload) if (success) { LocalStorage.setItem('token', data.token) yield put({type: 'requireAuth'}) } } catch (err) { message.error('受權失敗!') yield put({type: 'authErr'}) } }, *getUserByToken({}, {call, put}){ try { const { success, data } = yield call(getUserByToken) if (success) { yield put({type: 'authSuccess', payload: data}) } } catch (err) { message.error(err.message) yield put({type: 'authErr'}) } } }, reducers: { loginSuccess(state){ return { ...state, isLogin: true } }, authErr(state){ return { ...state, isLogin: false, user: null } }, authSuccess(state, {payload}){ return { ...state, user: payload } } } }
對於
redux-saga
的effect
等的用法,能夠參考 文檔
這裏咱們對 localStorage
作了一次封裝,看了源碼相信你就知道目的是什麼了:
/** * src/utils/localStorage.js * Custom window.localStorage */ const STORE_PREFIX = 'blog' export function getItem (key) { return window.localStorage.getItem(STORE_PREFIX + '-' + key) } export function setItem (key, value) { window.localStorage.setItem(STORE_PREFIX + '-' + key, value) } export function removeItem (key) { window.localStorage.removeItem(STORE_PREFIX + '-' + key) }
封裝 src/utils/request.js
import fetch from 'dva/fetch' import * as LocalStorage from './localStorage' const URL_PREFIX = '/api/v1' const TOKEN_NAME = 'token' function checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response; } const error = new Error(response.statusText); error.response = response; throw error; } /** * Requests a URL, returning a promise. * * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to "fetch" * @return {object} An object containing either "data" or "err" */ export default function request(url, options) { options = Object.assign({ headers: new Headers({ 'Content-Type': 'application/json' }) }, options) return fetch(URL_PREFIX + url, options) .then(checkStatus) .then(res => res.json()) .then(data => data) } /** * Request width token * @param {[type]} url * @param {[type]} options * @return {[type]} */ export function requestWidthToken (url, options) { const TOKEN = LocalStorage.getItem(TOKEN_NAME) options = Object.assign({ headers: new Headers({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}` }) }, options) return request(url, options) }
dva/fetch
直接導出了fetch
fetch
的用法很簡單,參考 github地址
這裏咱們把 url 的 prefix、token name 提取出來用做常量保存,以便於咱們修改,最好的方法是提取出來用一個文件保存
還記得咱們的展現組件嗎,如今咱們讓它 connect
到咱們的 model
import { connect } from 'dva' const { Header, Content, Footer } = Layout const { HeaderRight } = HeaderComponent const App = ({children, routes, app, doLogin}) => { const { isLogin, user } = app return ( <Layout> <Header> <HeaderComponent routes={routes}> {isLogin ? <HeaderRight user={user} /> : <LoginComponent doLogin={doLogin} app={app} /> } </HeaderComponent> </Header> ... ) } function mapStateToProps ({app}, ownProps) { return { app } } function mapDispatchToProps (dispatch) { return { doLogin({username, password}){ dispatch({type: 'app/doLogin', payload: {username, password}}) } } } export default connect(mapStateToProps, mapDispatchToProps)(App)
惟一須要注意的就是action
的 type
屬性了,如 app/doLogin
前綴 app
就是 dva.model
的 namespace
從 dva/createDva.js at master · dvajs/dva · GitHub 中能夠看到,
dva
會把model.namespace
最爲reducer
,effects
的prefix
拼接
而後咱們就能夠在 LoginComponent
中,監聽登錄的相應事件來調用對應的方法了。
在寫後端的時,不免遇到不少錯誤,咱們可使用 supervisor
、pm2
來監聽文件變更來自動重啓 nodejs
。鑑於後期咱們會使用 pm2
部署項目。這裏我仍是使用 pm2
在 server/package.json
中的 scripts
下新增:"start": "pm2 start bin/www --watch --name blog && pm2 log blog",