react基础4授权与部署
最终章。可能有番外吧。。但是也要在完成大作业之后了
前三章:
Authentication and Authorization
Introduction
Registering a New User
利用postman 进行post,成功了
Submitting the Registration Form
首先新建一个userService.js 这个文件封装了调用后端api的函数,沟通了前后端。
1 | import http from "./httpService"; |
然后再registerForm中导入,我们可以用这种语法
1 | import * as userService from "../services/userService"; |
这样的语法 ,会在registerForm中创建一个userServices对象,然后从userServices导入的所有函数都会成为他的方法
然后修改doSubmit方法,调用userService.register() 来注册
1 | doSubmit = async () => { |
我们看到在前端输入信息提交之后,在dev-tools中可以看到这条注册信息,然后后端会在MongoDb上载入信息
Handling Registration Errors
现在来处理注册时出现的错误,利用try,catch
如果出现错误且错误码为400,那么就是说用户的操作出现了问题。我们需要在表单中提示一些错误的信息。
这里我们只能得到用户已经被注册了的错误。所以我们先克隆errors。然后让errors的username属性设置成ex.response.data,然后再实现更新。ex.response.data是服务器给我们报的错误
1 | doSubmit = async () => { |
Logging in a User
我们先做登录页,登录页做好了我们做注册成功跳转页面这个功能。我们在postman中实现登录。发现返回了一堆代码,这其实是一种 ID card
实现逻辑是这样的:所以如果用户发送了合法的用户名和密码的组合给了服务器,服务器给用户一个认证卡。以后无论何时,用户都需要请求apiEndpoint,apiEndpoint需要认证这个用户,客户端会把这个token发给server,如果他是一个合法的token,服务器就会执行用户请求
Submitting the Login Form
首先新建一个authService.js 这里面封装了调用后端的post函数,并把promise返回
1 | import http from "./httpService"; |
在loginForm.jsx中导入以后,我们重写doSubmit函数
1 | doSubmit = async () => { |
那么我如果输入刚才登陆的账号密码,那么我们看到返回了一个token
Handling Login Errors
现在来处理登陆时发生的错误,利用try catch,逻辑和注册表单一样
1 | doSubmit = async () => { |
Storing the JWT(JSON Token)
我们得到了Token之后,现在我们应该把token保留在客户端。每个浏览器都有一个小型的本地数据库。这个数据库可以保存键值对
因为login函数(http.post)会返回一个promise对象,其中的data存放着返回的jwt,也就是我们想要的token,那么我们重命名它,然后存储到浏览器本地的小型数据库中。然后,点击提交的以后自动转回到首页
1 | doSubmit = async () => { |
Logging in the User upon Registration
现在我们想从 Headers(服务器发给我的包的头部信息)中获取tokens,学会了这个就可以如何使用头部来传递信息了
为实现这个功能,我们对后端user.js进行修改
1 | res |
我们修改registerForm.js中的doSubmit,让他从headers中获取token并且存入localStorage。最后跳转到首页
1 | doSubmit = async () => { |
下面是console.log(response)打印出来的内容,我们就是从这个headers中获取token
JSON Web Tokens (JWT)
在这个网站我们可以验证我们得到的Token是否合法
我们看到和我注册的信息是一致的
这相当于一个JSON对象,利用Base64URL加密算法进行编码得到的结果。我们看到左边有刚刚的id,name,email,最后一行是创建的事件。这就是为什么JWT是一个ID card的原因了,他就像驾照活着护照。存放了很多用户的信息
蓝色部分只有服务器才有。除非服务器被hack了,他们是不能通过修改token来伪装的。
Getting the Current User
下面我们来说说如何在本地得到token,解码它,然后读取装在信息,并且显示在导航栏上
npm i jwt-decode@2.2.0
在app.js中导入 jwt-decode
这样说吧,jwt-decode 导出了一个默认函数,然后我们把他叫做jwtDecode,我们随便叫他什么都可以。只要有辨识度即可
1 | import jwtDecode from "jwt-decode"; |
Displaying the Current User on NavBar
接下来我们实现:没有登陆就只显示Login和Register,登陆了就显示用户名和logout链接
首先我们在NavBar的()中把user解构出来,如果user非空,那么就显示user的名字和Logout按钮,否则就显示Login和Register
那我们就这样渲染,我们知道
1 |
|
但是这么做有个问题。我登陆以后返回首页,看到的还是login和register,只有当我重新刷新的时候,才会显示名字和logout。
这是因为在App组件当中,我们从本地获得JWT并且在cdm中解码了它,这个方法在程序的生命周期中只被调用了一次。为了解决这个问题,我们在登录和注册表单中,不要使用history的push方法来重定向
1 | this.props.history.push("/"); |
我们要完全的重载应用.window.location强制重载。这样的结果就是App对象又会被Mount一次。这时候,我们的本地存储已经有JWT了,这样就可以解码它并且得到当前的user了
1 | window.location = '/'; |
Logging out a User
logging out 就是把本地的token删除。
首先我们注册这个路由,然后渲染一个logout组件。当这个组件被装载时,我们删掉本地的token,然后将用户重定向到主页
logout.jsx
1 | import React, { Component } from "react"; |
然后注册路由之后。我们就可以正常退出了
Refactoring
现在我们有很多的文件出现了token,以后我们如果想换其他键值,就会比较麻烦,所以我们只需要一个模块来完成授权工作,所有授权相关的都在那个模块当中。所以我们希望把保存和删除token的方法,放到authService当中。这样这个服务就是应用中唯一负责处理授权的地方。
authService.jsx
1 | import http from "./httpService"; |
App,loginForm,registerForm,logout中对于token的操作,都换成
import auth from “../services/authService”; 然后相对应引用auth.loginWithJwt, auth.login, auth.logout, auth.getCurrentUser,…
首先来看看App.js: 在cdm中调用getCurrentUser方法,从本地数据库中取出token并用user接受这个对象
然后更新user,如果当前没有token,那么就返回一个null值, user为null
1 | class App extends Component { |
然后来看loginForm:调用auth.login,authService会帮他提取出JWT并存储到本地数据库中
1 | //... |
registerForm: 调用loginWithJwt,authService接收headers中提取出来的JWT并存储到本地数据库中
1 | //... |
最后来看看logout: authService会删除本地的token
1 | //... |
Calling Protected API Endpoints
受保护的终端要求用户认证或者登录,并且有明确的授权
后端 的config中的default.json中修改 “requiresAuth”: true 这样的话,如果是没有授权的用户就无法修改或者删除我后端数据库的内容了。
然后,我们需要给有token的用户授权。这个工作需要在httpServer中实现。因为这个模块的职责就是和终端进行http对话。现在我们添加一个配置一个信息,目的是让axios无论发送什么给服务器都带上这个头部
首先在authservice中写一个返回 JWT 的函数
1 | export function getJwt() { |
然后再httpService中添加以下配置,这样就实现了对注册用户的认证
1 | axios.defaults.headers.common['x-auth-token'] = auth.getJwt(); |
Fixing Bi-directional Dependencies
我们在authService和httpService之间缔造了一个双向依赖关系,这是很危险的
在这里,http更为重要,auth是建立在http之上的,所以我们既要在http中抹去对authService的依赖,又要在http中获取JWT。所以我们与其调用auth中的函数获取token,不如就直接跟auth说把token给我拿来,就像下面这张图
我们在httpService中写一个这个set函数,目的是获取到jwt并赋值给请求头
1 | function setJwt(jwt){ |
然后把这个函数导出
在authService中我们调用setJwt,把getJwt得到的结果来传入到这个函数。这样,httpService中就会得到我们的jwt了。解决了双向依赖
1 | http.setJwt(getJwt); |
Authorization
在后端,我们实现了只有管理员才能删除电影的功能.这就相当于当后端收到一个删除电影的请求的时候,请确保用户被授权且是管理员,auth和admin是中间件函数
1 | router.delete("/:id", [auth, admin], async (req, res) => { |
auth函数的功能:首先他确认服务器的requiresAuth打开了没有,如果是关闭的,他会将控制权移交到下一个中间件函数,否则就读取头部信息中的 x-auth-token .如果没有token的话,我们就返回Access denied. No token provided。意思是没有办法验证。如果有token的话,就先验证这个token是不是合法,是就传递给另外一个,不是就返回Invalid token.
auth函数的实现如下
1 | const jwt = require("jsonwebtoken"); |
admin函数的其实要检测设置,如果配置关闭,就传递控制权,不做任何事情;否则就检测是否用户为管理员,如果不是管理员,那么就返回 Access denied
1 | const config = require("config"); |
如果授权的话我们呢需要在数据库中修改
设置好后,重新登入,那么就可以删除movie了
Showing or Hiding Elements based on the User
现在我们想让没有登陆的用户看不到Delete,New Movie 按钮
在App中设置movie的路由.我们不能直接传入user进去,为了给子组件传递属性,我们要用到render
我们用render替换component,用箭头函数传入props,返回Movies组件,在movies组件当中加入所有的props还有当前的user属性
1 | <Route |
然后在movies的 new movies中,我们这样修改(在上面已经解构了).只有当user存在,我们才可以说把这个NewMovie渲染出来
1 | {user && ( |
同理,我们可以隐藏Delete
1 | content: movie => ( |
效果图
Protecting Routes
但直接隐藏并不能阻止新建电影,因为可以直接通过输入网站来访问路由
所以我们来看看怎么保护这个路由(初步实现),没有办法再每个route中都重复这个逻辑
逻辑和刚才的一样(我们在上面已经结构了user),传入props,判断是否存在user,如果不存在,那么就重定向为login页面,存在那么渲染 MovieForm组件
1 | <Route |
Extracting PrivateRoute
我们这里想把前面的逻辑封装成一个组件
PrivateRoute.jsx:返回一个标准的React Route。
原来的path现在已经囊括到{…rest}当中去了,render中我们要把!user 改成!auth.getCurrentUser()
除此之外,我们还需要判断当前的Component是否存在。因为在App.js当中,要么设置component要么设置render,所以如果有component,那么就返回继承了所有props的component,否则就继承所有props的render
1 | import auth from "../services/authService"; |
然后我们在app.js中引入的时候,对之前的进行修改.
1 | <PrivateRoute path="/movies/:id" component={MovieForm} /> |
现在如果我们点击movie的名字,那么我们不会跳到MovieForm表单,我们会跳到Login表单
Redirecting after Login
我们现在点击movie名字,登录好以后,不想再回到主页了,我想到这个movie的表单当中去,怎么办?
我们首先打印一下这个组件的props
我们发现在location中的pathname是有效的,我们就要重定向到这条路径。Redirect中我们把to设置成一个对象
我们首先传入一个pathname,这是默认的,我们也要设置state属性,这是用来传递附加数据的。我再这个state当中传入一个from属性把它设置为props.location 因为这个location对象保留了用户跳转前的 位置信息
1 | return ( |
然后我们转到loginForm去看看是否state已经设置了,如果设置了我们就取location中的pathname;如果state没有被设置,那么就回到主页当中.
比如上图,我是直接点击login进入LoginForm的,那么这时候state就不会被设置,那么登陆过后就直接跳转主页
1 | doSubmit = async () => { |
现在还需要注意到,在登陆状态下如果我们直接输入login,还是会跳转到login表单。所以在login表单我们要做一个重定向
1 | if (auth.getCurrentUser()) return <Redirect to="/" />; |
Exercise
现在我们要实现只有admin才能显示delete按钮(刚才是只要是用户就能显示)
Hiding the Delete Column
有几种方法隐藏这个Delete 列,一种方法是添加一个属性如visible或者hidden来动态控制它,这个方法需要修改好几个组件;还有一种方法是将这个Delete列直接删除,只在当前用户是管理员的情况下才添加进来,这个方法相对简单
我们为了不污染constructor,把delete列单独设置成了deleteColumn对象。然后写一个constructor,利用auth.getCurrentUser()获取当前的user
1 | deleteColumn = { |
我们设置mosh为管理员:再mongodb中添加 isAdmin: true
Deployment
Introduction
Environment Variables
Production Builds
Getting Started with Heroku
MongoDB in the Cloud
云数据库