Skip to content

User Management with JWT

Chen Junda edited this page Jul 24, 2018 · 3 revisions

使用JWT进行用户管理

用户登录、注册和鉴权管理是个非常常见的需求,而且不同系统的鉴权系统都有相似性。于是,这个套件提供了一套基于JWT进行权限管理的代码示例,实现了以下功能:

  1. 用户登录和信息保存
  2. 将用户信息保存至localStorage,页面刷新后可记住用户,直接登录
  3. 登录后,发送的API请求将会带有鉴权信息

RESTful API with JWT

RESTful API前后端交互和传统网页前后端交互接口最大的不同,即是RESTful是无状态的。每次RESTful请求都是独立的。于是,Cookie, Session等概念在RESTful API设计里都没有了作用,HTTP回到了它最初始、最简单的Request->Response模式。可是,Cookie, Session在以前都是鉴权所必要的工具,没有了它们,那怎么才能做到鉴权呢?JWT就是用来做这个事的。

JWT的具体定义这里就不多讲,简单的说,JWT鉴权分为以下几个步骤:

  1. 用户登录时,后端生成一个**无法伪造的token(使用后端独有的私钥加密过)**并发送给前端,token中包含后端用来唯一标识登录成功的用户的信息(例如用户名)。
  2. 之后,每当前端对一个需要用户信息的API发起请求时,需在header中增加一项Authorization: Bearer {token}。后端可以通过此header项里的token信息,解密出持有token的用户(用户名),这样就完成了用户身份鉴权;不携带此header信息,后端将无法知道用户信息(因为每次请求是独立无关的)。

这样做有什么好处呢?

  1. 后端省去了管理session的开销,每次请求都是独立的,可以提高后端的吞吐率
  2. “前端无关性”,后端不需要对不同的前端设备(Web或者手机)进行不同的处理
  3. 前端鉴权方法统一,有利于重用代码

实现

根据上文所说,要实现JWT,需要做到以下两点:

  1. 保存用户状态
  2. 在每次发起请求时带上token信息

幸运的是,在独立API层(与API交互)和MobX的帮助下,实现这两点非常容易。

保存用户状态

我们使用一个专门的Store保存用户状态:UserStore。这个UserStore是可注入的,所以可以在其他Store或者组件中注入UserStore以访问用户信息。(DIP, IoC and DI)

src/app/stores/UserStore.ts

const USER_LOCALSTORAGE_KEY = "user"; // 用户信息存在localStorage里的key

@Injectable //可被注入
export class UserStore {
  @observable user: User = null; // 用户信息
 
  @computed get loggedIn() { // 是否登录
    return !!this.user;
  }

  get token() { // 获得token
    return this.user ? this.user.token : null;
  }

  @action logout() { // 登出,清空用户信息和localStorage的用户信息,清空HttpService里的token信息。
    this.user = null;
    this.httpService.token = "";
    this.userService.logout();
    this.clearUser();
  };

  @action async login(username: string, password: string) { // 登录
    const { response, ok, error, statusCode } = await this.userService.login(username, password); // 发起登录API请求

    if (!ok) {
      throw { response, error, statusCode};
    }
    runInAction(() => {
      this.user = new User({
        username: username,
        token: response.token,
        role: response.jwtRoles[0].roleName as UserRole,
        email: response.email,
        avatarUrl: response.avatarUrl
      });
    }); // 登录成功,生成用户信息
 
  };

  remember() { // 记住当前用户,即在localStorage里增加一项。
    localStorage.setItem(USER_LOCALSTORAGE_KEY, JSON.stringify(this.user));
  } 

  clearUser() { // 清空当前记住的用户,即删除localStorage的用户项
    localStorage.removeItem(USER_LOCALSTORAGE_KEY);
  }

  constructor(@Inject private userService: UserService, @Inject private httpService: HttpService, @Inject private routerStore: RouterStore) {
    const user = localStorage.getItem(USER_LOCALSTORAGE_KEY); // 尝试取出localStorage里已经记住的用户项
    if (user) {
      try {
        this.user = new User(JSON.parse(user)); // 若有,则生成用户信息,并登录
        httpService.token = this.user.token;
      } catch (ignored) {
        console.log(ignored);
      }
    }
  }
}

在每次发起请求时带上token信息

在UserStore语句中出现了给httpService赋值的操作。HttpService中保存了当前登录用户的token信息,并且将会在每一次调用fetch时,将token加入header。在用户登录和退出时,UserStore除了清空自己保存的用户,还需要写入/清除HttpService中的token信息。

src/app/api/HttpService.tsx

@Injectable
export class HttpService {

  token: string  = ""; // 保存用户信息

  async fetch<T = any>(fetchInfo: FetchInfo = {}): Promise<NetworkResponse<T>> {
    const authHeader = this.token
      ? {"Authorization": `Bearer ${this.token}`}
      : {}; // 将token信息加入header
    try {
      const response = await this.fetchRaw({
        ...fetchInfo,
        body: JSON.stringify(fetchInfo.body),
        path: urlJoin(APIROOTURL, fetchInfo.path),
        headers: {
          ...authHeader,
          'Content-Type': 'application/json',
          ...fetchInfo.headers,
        }
      });
      return createNetworkResponse(response.status, (await response.json()));
    } catch (e) {
      return createNetworkResponse(NetworkErrorCode, null, e);
    }

帮助类

为了方便用户的使用,不需要让需要用户登录的组件每次都要手动写判断用户是否已经登录的判断代码,我们还提供了一个方便的HOC(Higher-Order Component)。

定义

src/pages/hoc/RequireLogin.tsx

import React from 'react';
import { observer } from "mobx-react";
import { UserRole } from "../../models/user/User";
import { Inject } from "react.di";
import { UserStore } from "../../stores/UserStore";
import { LocaleMessage } from "../../internationalization/components";

const ID_PREFIX = "common.login.";

/**
 * Marks that the component needs authentication. Authentication will be run before the component is rendered. If authentication succeeded, returns the wrapped component with { token: string, currentRole: UserRole } injected into props. Otherwise, error will be rendered.
 * @param roles Accepted Roles. Empty is considered to accepted all roles.
 */
export function requireLogin(...roles: UserRole[]) { // 参数为允许的权限。若不传入,则允许所有权限
  return function(WrappedComponent): any {
    class Component extends React.Component {

      @Inject userStore: UserStore;

      render() {

        if (!this.userStore.loggedIn) {
          return <LocaleMessage id={ID_PREFIX+"needLogin"}/>
        }
        if (roles.length >0 && roles.indexOf(this.userStore.user.role) == -1) {
          return <LocaleMessage id={ID_PREFIX+"roleNotMatch"} replacements={{

          }}/>;
        }
        return <WrappedComponent {...this.props}
                                 token={this.userStore.token}
                                 currentRole={this.userStore.user.role}
        />;
      }

    }

    return observer(Component);
  }
}

export interface RequireLoginProps {
  token: string;
  currentRole: UserRole;
}

使用方法

需要用户登录

@requireLogin()
class Component extends React.Component<RequireLoginProps> {
  render() {
    const {token, currentRole} = this.props;
    return JSON.stringify({ token, currentRole });
  }
}
用户登录状态 输出
用户没有登录 common.login.needLogin定义所对应的值(国际化
当用户已经登录,账户角色为ROLE_ADMIN,token为123.456.789 { token: "123.456.789", currentRole: "ROLE_ADMIN" }

需要特定权限

@requireLogin(ROLE_ADMIN, ROLE_REQUESTER)
class Component extends React.Component<RequireLoginProps> {
  render() {
    const {token, currentRole} = this.props;
    return JSON.stringify({ token, currentRole });
  }
}
用户登录状态 输出
用户没有登录 common.login.needLogin定义所对应的值(国际化
当用户已经登录,账户角色为ROLE_ADMIN(包含在参数里),token为123.456.789 { token: "123.456.789", currentRole: "ROLE_ADMIN" }
当用户已经登录,账户角色为ROLE_USER(未包含在参数里) common.login.roleNotMatch定义所对应的值

扩展阅读

JWT

https://www.jianshu.com/p/576dbf44b2ae

https://jwt.io/

Higher-Order Component高阶组件

https://doc.react-china.org/docs/higher-order-components.html

https://juejin.im/post/5914fb4a0ce4630069d1f3f6