Authentication 是許多 Web 服務都會使用到的基本功能,但每個服務的需求可能又不太一樣,有的人只需要 Basic Auth,有的人需要 Local Auth,也有人需要 Social Auth,Boilerplate 其實不可能做到滿足所有需求,所以使用了 Passportopen_in_new 這套 Library 來統一這一個 Infrastructure。
雖說大家需要的認證需求都不太一樣,但大部分 Web 服務都還是會需要基本的站內註冊及登入登出,所以 Boilerplate 有幫各位實作了 Local Authentication 的功能。
許多實作 Local Authentication 的入門教學都是 Session-Based,但我個人非常排斥使用 Session,因為 HTTP 是一個 Stateless 的協定,而 Session 是個 Stateful 的東西,違反了 HTTP 本身的設計原則,而且當 Server Scale Up 時,還要有管理 Session 的機制,徒增營運上的困擾,所以不如打從一開始就棄用。
我們的 Boilerplate 採用的是 Token-Based 的 Authentication,這裡所謂的 Token 指的是 JWT(JSON Web Tokens),這是目前 Modern Web 中很熱門的認證方式,而且是 Stateless。因此實作上搭配 passport-jwtopen_in_new 這個 Passport Strategy,撰寫了 這個 Middleware:
let cookieExtractor = (req) => { return req.store.getState().cookies.token;};
export default (req, res, next) => { passport.use(new JwtStrategy({ jwtFromRequest: cookieExtractor, secretOrKey: configs.jwt.authentication.secret, }, (jwtPayload, done) => { // this callback is invoked only when jwt token is correctly decoded User.findById(jwtPayload._id, handleDbError(res)((user) => { done(null, user); })); }));
passport.initialize()(req, res, next);};
由於 JWT 是夾帶在 Cookie 中,所以可以看到程式碼中使用了 ,從 Store 中的 cookies 讀取 Token。
另外我們還必須要有 這樣的 Middleware 讓我們確保某些 API Path 只有已登入的使用者可以存取:
export default (req, res, next) => { passport.authenticate( 'jwt', { session: false }, handleError(res)((user, info) => { handlePassportError(res)((user) => { if (!user) { res.pushError(Errors.USER_UNAUTHORIZED); return res.errors(); } req.user = user; next(); })(info, user); }) )(req, res, next);};
如此就能透過 Middleware 輕鬆維護 API 的存取權限:
import authRequired from '../middlewares/authRequired';
export default ({ app }) => { // ... app.get('/api/users/me', authRequired, userController.readSelf); // ...};
我認為使用 Passport 的最大好處就在於 Social Authentication 有一套標準的實作寫法,無論 Provider 是 Facebook、Twitter 還是 LinkedIn,只要套用對應的 Passport Strategy 就可以很容易實作 Social Authentication。
我們的 Boilerplate 實作了 Facebook 和 LinkedIn 兩個 Providers 的 Social Authentication,但寫法與概念都和 JWT 類似,就不在此細講程式碼,詳見 src/server/middlewares/passportInit.jsopen_in_new。