1. 程式人生 > >API介面JWT方式的Token認證(下),客戶端(Android)的實現

API介面JWT方式的Token認證(下),客戶端(Android)的實現

上篇文章已經介紹了 JWT 認證在 Laravel 框架伺服器上的實現。這篇文章繼續介紹 Android 客戶端的實現。回顧下 JWT 認證的流程,客戶端先提交賬號密碼進行登入,賬號密碼驗證成功後,伺服器會生成一個 token,其中包含了使用者資訊,token 到期時間等資訊,伺服器將 token 返回給客戶端後不會儲存此 token。客戶端接受到 token 後,需要對 token進行儲存,在以後訪問需要認證的 API 介面是,在 HTTP 請求通過認證頭提交 token,伺服器校驗 token 的合法性,是否過期,攜帶的使用者資訊是否匹配,全部通過後,完成驗證,之後才能完成後續操作。

先看一下已經實現的 API 介面的路由:

$api = app('Dingo\Api\Routing\Router');

$api->version('v1', ['namespace' => 'App\Http\Controllers'], function ($api) { 
  $api->get('login', 'Auth\[email protected]');
   $api->post('register', 'Auth\[email protected]');

  $api->group(['middleware' => 'jwt.auth', 'providers'
=> 'jwt'], function ($api) { $api->get('user', '[email protected]'); $api->get('notices', '[email protected]'); }); });

其中 login 和 register 是用來獲取 token 的,而 user 和 notices 則需要客戶端提供 token 。下面我們就在 Android 客戶端上實現對這些介面的訪問。

1.構建 UI

在主頁新增一頁 MoreFragment,佈局檔案程式碼如下:

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="chenyu.jokes.feature.more.MoreFragment" android:orientation="vertical" android:background="@color/bgGrey">
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:background="@android:color/white"> <ImageView android:id="@+id/avatar" android:layout_width="80dp" android:layout_height="80dp" android:layout_alignParentStart="true" app:srcCompat="@drawable/ic_36" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_centerVertical="true" android:adjustViewBounds="false"/> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="32dp" android:visibility="invisible" android:textSize="24sp" android:layout_toEndOf="@+id/avatar" android:layout_marginStart="16dp" android:layout_alignParentTop="true" android:layout_marginTop="8dp"/> <TextView android:id="@+id/email" android:layout_width="wrap_content" android:layout_height="32dp" android:textSize="16sp" android:visibility="invisible" android:layout_toEndOf="@+id/avatar" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:layout_below="@+id/name"/> <Button android:id="@+id/login" android:text="登入" android:layout_width="72dp" android:layout_height="32dp" android:layout_toEndOf="@+id/avatar" android:layout_centerVertical="true" android:layout_marginStart="32dp" android:padding="0dp" android:textColor="@android:color/white" android:textSize="16sp" android:background="@drawable/selector_bg_corner"/> <Button android:id="@+id/register" android:text="註冊" android:padding="0dp" android:layout_width="72dp" android:layout_height="32dp" android:background="@drawable/selector_bg_corner" android:layout_toEndOf="@+id/login" android:textColor="@android:color/white" android:textSize="16sp" android:layout_centerVertical="true" android:layout_marginStart="16dp"/> <Button android:id="@+id/logout" android:text="退出" android:padding="0dp" android:layout_width="72dp" android:layout_height="32dp" android:layout_marginEnd="16dp" android:background="@drawable/selector_bg_corner" android:visibility="invisible" android:textColor="@android:color/white" android:textSize="16sp" android:layout_alignParentEnd="true" android:layout_centerVertical="true"/> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="8dp" android:background="@android:color/white"> <Button android:id="@+id/notice" android:text="獲取通知" android:layout_width="wrap_content" android:layout_height="wrap_content" android:enabled="false" android:layout_gravity="top" android:background="@drawable/selector_bg_corner" android:layout_marginTop="16dp" android:textColor="@android:color/white" android:textSize="16sp" android:layout_marginStart="16dp" android:layout_marginBottom="16dp"/> <TextView android:id="@+id/notice_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="16sp" android:layout_marginStart="16dp" android:layout_marginTop="16dp"/> </LinearLayout> </LinearLayout>

登入之前效果如下,介面顯示登入和註冊按鈕,獲取通知按鈕為不可點選狀態。
這裡寫圖片描述

登入後效果如下,登入和註冊按鈕隱藏,變為顯示使用者名稱和郵箱,退出按鈕也被顯示出來,並且獲取通知按鈕變為可以點選。
這裡寫圖片描述

2. 實現註冊功能

在 ServiceAPI 下新增網路介面:

  @FormUrlEncoded @POST("register") Observable<Token> register(
      @Field("name") String name,
      @Field("email") String email,
      @Field("password") String password
  );

我們用的是 MVP 架構,網路請求是在 Presenter 中完成的,那麼在 MorePresenter 的 onCreate 函式中註冊請求:

restartableFirst(REGISTER,
        new Func0<Observable<Token>>() {
          @Override public Observable<Token> call() {
            return App.getServerAPI().register(mName, mEmail, mPassword)                .subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread());
          }
        },
        new Action2<MoreFragment, Token>() {
          @Override public void call(MoreFragment moreFragment, Token token) {
            moreFragment.onRegisterSuccess(token);
          }
        }, new Action2<MoreFragment, Throwable>() {
          @Override public void call(MoreFragment moreFragment, Throwable throwable) {
            moreFragment.onError(throwable);
          }
        }
    );

呼叫 register 網路介面,在請求成功呼叫moreFragment 的 onRegisterSuccess 函式。
同時在 MorePresenter 中公開一個 register 函式,供 View 層來呼叫,發起網路請求:

public void register(String name, String email, String password) {
    mName = name;
    mEmail = email;
    mPassword = password;
    start(REGISTER);
  }

然後是 View 層的實現,MoreFragment 中對註冊按鈕新增監聽,點選後彈出對話方塊進行註冊:

@OnClick({R.id.login, R.id.logout, R.id.register, R.id.notice}) public void click(View view) {
    switch (view.getId()) {
      ...
      case R.id.register:     
        showRegisterDialog();
        break;
    }
  }

在看下 showRegisterDialog() 函式:

private void showRegisterDialog() {
    AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
    builder.setIcon(R.mipmap.ic_launcher).setTitle("註冊");

    View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_register, null);
    builder.setView(view);

    final EditText edtUserName = (EditText) view.findViewById(R.id.username);
    final EditText edtPassword = (EditText) view.findViewById(R.id.password);
    final EditText edtEmail = (EditText) view.findViewById(R.id.email);
    final EditText edtPasswordConfirm = (EditText) view.findViewById(R.id.password_confirmation);

    builder.setPositiveButton("確定", null);

    builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
      @Override public void onClick(DialogInterface dialog, int which) {

      }
    });

    final AlertDialog alertDialog = builder.create();
    alertDialog.show();
    alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
        new View.OnClickListener() {
          @Override public void onClick(View v) {
            String userName = edtUserName.getText().toString().trim();
            String password = edtPassword.getText().toString().trim();
            String email = edtEmail.getText().toString().trim();
            String password_confirm = edtPasswordConfirm.getText().toString().trim();
            if(! password.equals(password_confirm) ) {
              Toast.makeText(getContext(), "兩次輸入密碼不一致", Toast.LENGTH_SHORT).show();
              return;
            }
            getPresenter().register(userName, email, password);
            alertDialog.dismiss();
          }
        });
  }

AlertDialog 採用了自定義的 layout,包含 使用者名稱、郵箱、密碼、確認密碼 這4個文字編輯框。我們給確定按鈕註冊了一個空的監聽器,這是因為在點選確定時要驗證密碼和確認密碼是否相同,如果不同,彈出提示訊息,對話方塊不會消失,這樣使用者才有機會進行修改,如果監聽器不是 null,那使用者點選確定後對話方塊必定會消失。所以這裡給確定按鈕註冊一個空的 DialogInterface.OnClickListener,並在對話方塊顯示出來給,查詢到確認按鈕,並註冊一個 View.OnClickListener,來實現上述需求。
如果兩次密碼確認一致,則呼叫 Presenter 中的 register 函式,並取消對話方塊。

註冊成功後的相應比較簡單,直接彈出提示:

  public void onRegisterSuccess(Token token) {
    Toast.makeText(getContext(), "註冊成功,請登入", Toast.LENGTH_SHORT).show();
  }

看下效果,點選註冊,彈出對話方塊:
這裡寫圖片描述

輸入密碼不一致時,點選確定,彈出提示,對話方塊不消失:
這裡寫圖片描述

密碼輸入一致,點選確定,發起註冊請求,對話方塊消失,提示註冊成功:
這裡寫圖片描述

3. 實現登入功能

先看一下登入介面返回的資料,其中包含了使用者資訊和 token:

{
  "user": {
    "id": 9,
    "name": "user666",
    "email": "[email protected]"
  },
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjksImlzcyI6Imh0dHA6XC9cL2hvbWVzdGVhZC5hcHBcL2FwaVwvbG9naW4iLCJpYXQiOjE0OTM3NTQ0NjUsImV4cCI6MTQ5Mzc1ODA2NSwibmJmIjoxNDkzNzU0NDY1LCJqdGkiOiJGeTRmb2FYeWI5Q2RZTGlXIn0.Isu2XpPypZIMjB8P8Fis-qLknij6hdWfaQ_Jl1Gzo-o"
}

登入功能和註冊功能很相似,但是登入成功後我們要根據伺服器返回的使用者資訊更新UI,並對 token進行儲存。
首先在 Model 路徑下建立 User 類和 Account 類用於解析和儲存網路資料:

@JsonIgnoreProperties(ignoreUnknown = true) public class User {
  public String id;
  public String name;
  public String email;
}
@JsonIgnoreProperties(ignoreUnknown = true) public class Account {
  public User user;
  public String token;
}

ServiceAPI 增加 網路介面,我們用 Account類來解析介面返回的 Json資料:

@GET("login") Observable<Account> login(
      @Query("email") String email,
      @Query("password") String password
  );

接下來在 MorePresenter 的 onCreate 中註冊網路請求:

restartableFirst(LOGIN,
        new Func0<Observable<Account>>() {
          @Override public Observable<Account> call() {
            return App.getServerAPI().login(mEmail, mPassword)
                .subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread());
          }
        },
        new Action2<MoreFragment, Account>() {
          @Override public void call(MoreFragment moreFragment, Account account) {
            moreFragment.onLoginSuccess(account);
          }
        },
        new Action2<MoreFragment, Throwable>() {
          @Override public void call(MoreFragment moreFragment, Throwable throwable) {
            moreFragment.onError(throwable);
          }
        }
    );

網路請求成功後會呼叫 MoreFragment 的 onLoginSuccess 函式。

同時在 MorePresenter 中公開 login 函式供外部呼叫:

  public void login(String email, String password) {
    mEmail = email;
    mPassword = password;
    start(LOGIN);
  }

接下來是 View 層處理,在 MoreFragment 中,點選登入按鈕後,彈出登入對話方塊:

@OnClick({R.id.login, R.id.logout, R.id.register, R.id.notice}) public void click(View view) {
    switch (view.getId()) {
      case R.id.login:
        showLoginDialog();
        break;
        ...
    }
  }

  private void showLoginDialog() {
    AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
    builder.setIcon(R.mipmap.ic_launcher).setTitle("登入");

    View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_login, null);
    builder.setView(view);

    final EditText edtPassword = (EditText) view.findViewById(R.id.password);
    final EditText edtEmail = (EditText) view.findViewById(R.id.email);

    builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
      @Override public void onClick(DialogInterface dialog, int which) {
        String password = edtPassword.getText().toString().trim();
        String email = edtEmail.getText().toString().trim();
        getPresenter().login( email, password);
      }
    });

    builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
      @Override public void onClick(DialogInterface dialog, int whick) {

      }
    });

    builder.show();
  }

因為不需要做校驗,登入對話方塊比註冊時簡單,點選確定後就呼叫 MorePresenter 的 login 函式,傳送登入請求。

再看一下登入成功後的處理:

  public void onLoginSuccess(Account account) {
    AccountManager.create().setAccount(account);
    mTxtName.setVisibility(View.VISIBLE);
    mTxtName.setText(account.user.name);
    mTxtEmail.setVisibility(View.VISIBLE);
    mTxtEmail.setText(account.user.email);
    mBtnLogin.setVisibility(View.INVISIBLE);
    mBtnLogout.setVisibility(View.VISIBLE);
    mBtnRegister.setVisibility(View.INVISIBLE);
    mBtnNotice.setEnabled(true);
  }

首先對賬號資訊進行儲存,包含使用者的 ID、name、email,以及此次的 token,這些資訊會被儲存到 SharedPreferences 裡,AccountManager 是我們自定義的賬號管理類,可以在應用的任何地方儲存和獲取使用者資訊,具體在下一節中介紹。

然後就是 UI 的變更了,登入成功後將登入和註冊按鈕隱藏,顯示使用者的 name 和email,顯示退出按鈕,將獲取通知按鈕設定為可點選。

最後看下實現效果,點選登入按鈕,彈出對話方塊:
這裡寫圖片描述

登入成功後介面變化:
這裡寫圖片描述

4. 實現全域性賬號資訊存取

JWT 的 token 的有效期一般設定為數小時,Laravel 下的 JWT 預設有效期為60分鐘。在這期間客戶端需要對 token 進行儲存,那麼儲存在什麼位置合適呢?因為 應用中任何位置都有可能訪問需要認證的 API,這個 token 需要在應用全域性可用,不會隨著 Fragment 或者 Activity 的生命週期而消亡,並且在應用退出後也需要保留。

綜合考慮上面的需求,決定將賬戶資訊儲存到 SharedPreferences 中,由於使用 SharedPreferences 需要用到 context,因此在 Application 類中提供一個獲取全域性 context 的方法,以便在任何地方都可以呼叫 AccountManager 類。

在 App 類下:

  private static Context context;

  @Override public void onCreate(){
    super.onCreate();
    context = getApplicationContext();
    ...
  }
  public static ServerAPI getServerAPI() {
    return serverAPI;
  }
public class AccountManager {
  private static SharedPreferences  sp;
  private static SharedPreferences.Editor editor;

  public static AccountManager create() {
    AccountManager accountManager = new AccountManager();
    accountManager.sp = App.getAppContext().getSharedPreferences("account", 0);
    accountManager.editor = sp.edit();
    return accountManager;
  }

  public void setToken(String token) {
    editor.putString("token", token);
    editor.commit();
  }

  public String getToken() {
    String token = sp.getString("token", "");
    return token;
  }

  public void setAccount(Account account) {
    editor.putString("token", account.token);
    editor.putString("userId", account.user.id);
    editor.putString("userEmail", account.user.email);
    editor.putString("userName", account.user.name);
    editor.commit();
  }

  public Account getAccount() {
    Account account = new Account();
    account.token = sp.getString("token", "");
    account.user.id = sp.getString("userId", "");
    account.user.name = sp.getString("userEmail", "");
    account.user.email = sp.getString("userEmail", "");
    return account;
  }

  public void clearAccount() {
    editor.putString("token", "");
    editor.putString("userId", "");
    editor.putString("userEmail", "");
    editor.putString("userName", "");
    editor.commit();
  }

  public void setUser(User user) {
    editor.putString("userId", user.id);
    editor.putString("userEmail", user.email);
    editor.putString("userName", user.name);
    editor.commit();
  }
}

程式碼比較簡單,提供一個靜態函式 create 來建立並返回 AccountManager,同時做好 SharedPreferences 存取的準備工作,這裡用到了 App 裡的getAppContext() 函式來獲取全域性 context。之後提供了對賬號Account
的儲存、讀取和清除函式,也可以單獨存取 User 和 token。

4. 訪問需要認證的 API

獲取到 token 之後就可以訪問 需要認證的 API 了,伺服器已經準備好了兩個 API,一個是簡單測試用的 notices API,認證成功就返回一段話,還有一個就是 user API,認證成功後返回 User 資訊,user API 當前用來做校驗 token 是否有效使用,在後面的章節介紹,這一節只介紹 notices API。

首先新增 Model,在 ServiceAPI 下建立網路介面:

public class Notice {
  public String content;
}
  @GET("notices") Observable<Notice> getNotice(
      @Header("Authorization") String token
  );

注意和之前的介面不同,這裡添加了 @Header 註解,這樣傳送網路請求時會新增認證頭。

接下來 MorePresenter 註冊請求,公開函式,和之前的基本類似,不同的是在訪問 API 介面時,呼叫了 AccountManager 來獲取 token,注意 token 前加了 Bearer:

    restartableFirst(NOTICE,
        new Func0<Observable<Notice>>() {
          @Override public Observable<Notice> call() {
            return App.getServerAPI().getNotice("Bearer " + AccountManager.create().getToken())
                .subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread());
          }
        },
        new Action2<MoreFragment, Notice>() {
          @Override public void call(MoreFragment moreFragment, Notice notice) {
            moreFragment.onGetNoticeSuccess(notice);
          }
        },
        new Action2<MoreFragment, Throwable>() {
          @Override public void call(MoreFragment moreFragment, Throwable throwable) {
            moreFragment.onError(throwable);
          }
        }
    );
public void getNotice() {
    start(NOTICE);
}

然後是 View 層處理,也很簡單,點選獲取通知按鈕,呼叫 MorePresenter 的 getNotice函式,請求成功後,顯示獲取的通知訊息:

@OnClick({R.id.login, R.id.logout, R.id.register, R.id.notice}) public void click(View view) {
    switch (view.getId()) {
      ...
      case R.id.notice:
        getPresenter().getNotice();
        break;
    }
  }

public void onGetNoticeSuccess(Notice notice) {
    mTxtNotice.setText(notice.content);
}

最後看下效果,登入成功後獲取通知:
這裡寫圖片描述
假如 token 已經過期,我們再取點選按鈕,則無法通過認證:
這裡寫圖片描述

5. 實現退出賬號

因為 JWT 是無狀態無連線的認證方式,伺服器上不需要儲存 token 狀態,因此退出時只需要清除掉客戶端本地的賬號資訊就行了,不需要和伺服器作互動。
看下實現程式碼,呼叫 AccountManager 清除掉儲存的賬號資訊,並恢復 UI 到登入前的樣子就行了。

@OnClick({R.id.login, R.id.logout, R.id.register, R.id.notice}) public void click(View view) {
    switch (view.getId()) {

      case R.id.logout:
        AccountManager.create().clearAccount();

        mBtnLogin.setVisibility(View.VISIBLE);
        mBtnLogout.setVisibility(View.INVISIBLE);
        mBtnRegister.setVisibility(View.VISIBLE);
        mBtnNotice.setEnabled(false);
        mTxtName.setVisibility(View.INVISIBLE);
        mTxtEmail.setVisibility(View.INVISIBLE);
        mTxtNotice.setText("");
        break;
    }
  }

這裡寫圖片描述

6. UI 恢復 和 token 檢測

上面的程式碼已經實現了登入成功後用戶資訊和 token 的儲存,那麼我們希望在應用或者特定的 View 啟動的時候,能夠將儲存的使用者資訊恢復到 UI 上,並且檢測下儲存的 token 是否有效,是否過期,如果未過期,則自動恢復 UI 到已登入的狀態,不需要使用者再登入。綜上,我們在 MoreFragment 啟動的時候,訪問 user API 介面,攜帶儲存的 token,給伺服器驗證,如果驗證成功,則恢復 UI 到登入成功後的樣子,如果驗證失敗,則保留未登入的狀態,等待使用者再次輸入賬號密碼進行登入。

要實現上述功能,和之前的程式碼一樣的,首先建立好 Model、ServiceAPI 介面、MorePresenter中註冊好請求,具體程式碼就不貼了,都是類似的。主要看下 MoreFragment 的程式碼,我們在 onCreateView 裡處理:

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_more, container, false);
    ButterKnife.bind(this, view);

    if(AccountManager.create().getToken() != "") {
      getPresenter().getUserInfo();
    }
    return view;
  }

首先通過 AccountManager 獲取儲存的 token,如果 token 是空的,說明之前就是未登入狀態,不需要處理,UI 或保持初始的未登入狀態,如果 token 非空,則呼叫 MorePresenter 來訪問 user API。

如果認證失敗,則彈出提示,UI 不會有變化,保持未登入狀態。如果認證成功,則呼叫 MoreFragment 的 onGetUserSuccess 函式來更新UI,這裡恢復 UI 時使用者資訊的來源可以是本地 SharedPreferences,也可以是伺服器剛返回的資料,正常情況下兩者應該是一樣的,但是我們認為伺服器的資料更可信,因而採用伺服器的資料更新 UI,並將伺服器的 User 資料進行儲存。

public void onGetUserSuccess(User user) {
    AccountManager.create().setUser(user);
    mTxtName.setVisibility(View.VISIBLE);
    mTxtName.setText(user.name);
    mTxtEmail.setVisibility(View.VISIBLE);
    mTxtEmail.setText(user.email);
    mBtnLogin.setVisibility(View.INVISIBLE);
    mBtnLogout.setVisibility(View.VISIBLE);
    mBtnRegister.setVisibility(View.INVISIBLE);
    mBtnNotice.setEnabled(true);
  }

為了測試效果,我們特意將伺服器上的 token 有效期配置為1分鐘,修改伺服器的 .env 檔案,設定 JWT_TTL=1 。
看下效果,登入成功或退出應用,在 token 過期前重新啟動應用,進入 MoreFragment 頁面,自動進入已登入狀態:
這裡寫圖片描述

再次退出應用,等 token 過期後,啟動應用,提示未認證,進入 MoreFragment 頁面,處於未登入狀態:
這裡寫圖片描述

這裡寫圖片描述

後記

JWT 方式的 API 基本功能,以及 Laravel 伺服器和 Android 客戶端的實現方式就介紹完了,JWT 這種無狀態的方式還是很適合 API 認證的,客戶端只需要生成和驗證 token,客戶端只需要儲存 token 就行,token 有效期就儲存在 token 自身,不需要伺服器為每個登入的使用者去儲存 token 狀態,這樣大大減小了開銷。並且 token 本身就包含了使用者 ID 等一些非敏感資訊,因此在很多網路請求的時候,甚至可以只傳輸 token,不需要再有單獨的使用者資訊引數,也是減少了一筆開銷。

上面介紹的內容可以完成 JWT 認證的基本功能了,但還是有很多可以改善的地方,比如 password 是明文傳輸的,很不安全,這個作為一個 Demo 專案,就沒考慮這麼周全。另外由於 JWT 方式一個天生的缺點,伺服器無法控制 token 的有效期,只要你發出了一個 token,它的有效期就定死了,因為伺服器不儲存 token 狀態,所有就無法提前結束 token 生命週期。

因此在配置 token 有效期是要比較謹慎,不能太長了。但是太短也不行,因為 token 方式,包括除了 JWT 外的其他 token方式,其實就是用 token 代替賬號密碼作為使用者驗證的憑證,只要一次賬號密碼驗證通過,後續一段時間內只需要 token 就可以驗證,不需要密碼,降低風險,有效期太短必然導致密碼頻繁傳送,且使用者需要頻繁地登入,影響使用者體驗。所有要根據實際情況選擇一個合適的有效期。

另外 token 到期後如何處理也是個問題。如果使用者沒使用應用的時候 token 過期了,那還好點,想想使用者正在操作應用的時候,突然 token 就到期了,操作被中斷,需要重新登入,那一定是一件很不爽的事情。JWT 本身也提供了一種解決方法,設定了一個 token 重新整理時間,在 token 過期但是沒超過重新整理時間的情況下,用舊的 token 可以獲取到新的 token。另外也可以考慮在每次傳送 API 請求的時候都去重新整理 token,或者週期性傳送心跳包來更新 token,不過這在併發請求比較多的時候,也會涉及到非同步衝突的問題,需要謹慎考慮。

後續如果有時間,再深入研究下這些問題。

相關推薦

API介面JWT方式Token認證客戶(Android)的實現

上篇文章已經介紹了 JWT 認證在 Laravel 框架伺服器上的實現。這篇文章繼續介紹 Android 客戶端的實現。回顧下 JWT 認證的流程,客戶端先提交賬號密碼進行登入,賬號密碼驗證成功後,伺服器會生成一個 token,其中包含了使用者資訊,token

API介面JWT方式Token認證伺服器(Laravel)的實現

簡介 最近在開發一個 Android 程式,需要做使用者登入和認證功能,另外伺服器用的是 Laravel 框架搭建的。最終決定用 JWT 實現API介面的認證。 JWT 是 Json Web Tokens 的縮寫,與傳統 Web 的 Cookies 或者 S

使用者Ip地址和百度地圖api介面獲取使用者地理位置經緯度座標城市

<?php   //獲取使用者ip(外網ip 伺服器上可以獲取使用者外網Ip 本機ip地址只

IdentityServer47- 使用客戶認證控制API訪問客戶授權模式

一.前言 目前官方的文件和Demo以及一些相關元件全部是.net core 1.1的,應該是因為目前IdentityServer4目前最新版本只是2.0.0 rc1的原因,官方文件和Demo還沒來更新。我準備使用的是.net core 2.0 所支援的IdentityServer4 2.0.0,官方文件及De

HTTPS介面加密和身份認證

HTTPS介面加密和身份認證 對HTTPS研究有一段時間了,在這裡寫下一些收集的資料和自己的理解。有不對的地方希望斧正。 1.為什麼要使用HTTPS代替HTTP 1.1HTTPS和HTTP的區別 https協議需要到CA申請證書,一

跟我學設計模式視頻教程——管擦者模式責任鏈模式

tar eight color font content 設計模式 name -m ack 課程視頻 觀察者模式(下) 責任鏈模式(上) 課程筆記 課程筆記 課程代碼 課程代碼 新課程火熱報名中 課程介紹

深度學習中的三種梯度下降方式:批量batch隨機stochastic小批量mini-batch

  1,批量梯度下降法(Batch Gradient Descent) :在更新引數時都使用所有的樣本來進行更新。   優點:全域性最優解,能保證每一次更新權值,都能降低損失函式;易於並行實現。   缺點:當樣本數目很多時,訓練過程會很慢。   2,隨機梯度下降法(Stoch

App後臺開發運維和架構實踐學習總結6——App客戶與後臺互動方式總結

1、HTTP簡單基本認證方式 這個是早期互動用得比較多的一種方式,主要是使用使用者名稱和密碼來互動,由於在每次的互動中,使用者名稱和密碼都會暴露給第三方,那麼這麼做是不可取的,風險十分大,所以這種

TortoiseSVN 客戶

svn服務端 系統 方便 mman bsp 命令操作 登陸 and 右鍵 svn客戶端類型 svn客戶端需要通過網絡訪問svn服務端提交文件、查詢文件等,可通過以下客戶端類型訪問svn服務端: 使用Subversion提供的客戶端命令,使用方式:在命令行下輸入命令操作。

數據庫字符集AL32UTF8客戶字符集2%是不同的

服務端 oracl ges 11g 解決 tle client 相同 解碼 登錄oracle數據庫時我們會遇到這樣的提示信息:“數據庫字符集(AL32UTF8)和客戶端字符集(2%)是不同的”。 這是由於數據庫服務端和客戶端的字符集不一致所造成的,服務端字符集和客戶端字符集

服務器渲染SSR客戶渲染(CSR)

服務器 技術分享 wid post eight 渲染 log 服務器端 pos 服務器端渲染(SSR)和客戶端渲染(CSR)

【WCF系列】WCF客戶怎麽消費服務

class fig 完全 文件 自動 客戶 回收 ins 必須 WCF客戶端怎麽消費服務 獲取服務綁定協議、綁定和地址:實現方式 SvcUtil方式:SvcUtil.exe是一個命令行工具,位於:C:\Program Files (x86)\Microsoft SDKs

大數據環境搭建2- hive客戶安裝

where ecif tro 結構化 AR JD serve HERE lec 一、簡介 hive是基於hadoop的一種數據倉庫工具,可以將結構化的文件映射成為數據庫的一張表,並提供簡單sql查詢功能,底層實現是轉化為MapReduce任務計算。 二、安裝 (1)下載 從

GitIDEA客戶應用Git

span 工程 version 如果 ubd 圖片 nbsp 提交 add 一.IDEA客戶端git 1.提交代碼到本地倉庫 1. 設置使用Git 關聯git 設置本地倉庫目錄,一般是IDEA工作空間,項目存放的目錄。 配置git環境變量 2.

ZooKeeper-- 第三方客戶 ZkClient的使用

實現 kafka int java pid () config obj 9.1 前言   zkClient主要做了兩件事情:     一件是在session loss和session expire時自動創建新的ZooKeeper實例進行重連。     另一件是將一次性wat

Zookeeper 原始碼Zookeeper 客戶原始碼

Zookeeper 原始碼(三)Zookeeper 客戶端原始碼 Zookeeper 客戶端由以下幾個核心元件組成: 類 說明 Zookeeper Zookeeper 客戶端入口 ClientWatch

大資料入門7RPC客戶和RPC服務通訊

RPC客戶端和RPC服務端通訊: 客戶端:(匯入jar:hdfs,common相關的) LoginControl: public class LoginControl {     public static void main(String[] args) th

資料庫字符集AL32UTF8客戶字符集2%不同

       今天在安裝Oracle資料庫時出現的一個問題,“資料庫字符集(AL32UTF8)和客戶端字符集(2%)是不同的。字符集轉化可能會造成不可預期的後果”。         上網查了一些資料得知Oracle資

SpringCloud 2.0——Eureka客戶搭建

上一篇:SpringCloud 2.0(一)——註冊中心Eureka搭建 這一節,我們基於SpringBoot搭建一個服務的提供方,然後註冊到上一節中我們搭建的Eureka註冊中心。還是跟上一篇一樣,去Spring的官網搜尋對應的Eureka Discovery依賴,如下圖:因

spring cloud 入門客戶往註冊中心Eureka 註冊服務】

客戶端 往Eureka 註冊服務,註冊成功之後,其他的服務,才可以對本服務進行呼叫 程式碼結構如下:   UserApplication 程式碼如下: package com.study.user; import org.springframework.boot.