接口使用jwt返回token_API接口JWT方式的Token认证(下),客户端(Android)的实现
上篇文章已經(jīng)介紹了 JWT 認證在 Laravel 框架服務器上的實現(xiàn)。這篇文章繼續(xù)介紹 Android 客戶端的實現(xiàn)。回顧下 JWT 認證的流程,客戶端先提交賬號密碼進行登錄,賬號密碼驗證成功后,服務器會生成一個 token,其中包含了用戶信息,token 到期時間等信息,服務器將 token 返回給客戶端后不會保存此 token。客戶端接受到 token 后,需要對 token進行存儲,在以后訪問需要認證的 API 接口是,在 HTTP 請求通過認證頭提交 token,服務器校驗 token 的合法性,是否過期,攜帶的用戶信息是否匹配,全部通過后,完成驗證,之后才能完成后續(xù)操作。
先看一下已經(jīng)實現(xiàn)的 API 接口的路由:
$api = app('Dingo\Api\Routing\Router');
$api->version('v1', ['namespace' => 'App\Http\Controllers'], function ($api) {
$api->get('login', 'Auth\AuthenticateController@authenticate');
$api->post('register', 'Auth\RegisterController@register');
$api->group(['middleware' => 'jwt.auth', 'providers' => 'jwt'], function ($api) {
$api->get('user', 'UserController@getUserInfo');
$api->get('notices', 'NoticeController@index');
});
});1
2
3
4
5
6
7
8
9
10
111
2
3
4
5
6
7
8
9
10
11
其中 login 和 register 是用來獲取 token 的,而 user 和 notices 則需要客戶端提供 token 。下面我們就在 android 客戶端上實現(xiàn)對這些接口的訪問。
本文采用的 Android 代碼下載地址:
https://github.com/zhongchenyu/jokes
由于后續(xù)可能會重構代碼,本文使用的代碼保存在 demo2 分支。
1.構建 UI
在主頁新增一頁 MoreFragment,布局文件代碼如下:
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">
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:background="@android:color/white">
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"/>
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"/>
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"/>
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"/>
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"/>
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"/>
android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_marginTop="8dp"
android:background="@android:color/white">
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"/>
android:layout_width="match_parent" android:layout_height="wrap_content"
android:textSize="16sp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp"/>
12
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
681
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
登錄之前效果如下,界面顯示登錄和注冊按鈕,獲取通知按鈕為不可點擊狀態(tài)。
登錄后效果如下,登錄和注冊按鈕隱藏,變?yōu)轱@示用戶名和郵箱,退出按鈕也被顯示出來,并且獲取通知按鈕變?yōu)榭梢渣c擊。
2. 實現(xiàn)注冊功能
在 ServiceAPI 下添加網(wǎng)絡接口:
@FormUrlEncoded @POST("register") Observable register(
@Field("name") String name,
@Field("email") String email,
@Field("password") String password
);1
2
3
4
51
2
3
4
5
我們用的是 MVP 架構,網(wǎng)絡請求是在 Presenter 中完成的,那么在 MorePresenter 的 onCreate 函數(shù)中注冊請求:
restartableFirst(REGISTER,
new Func0>() {
@Override public Observable call() {
return App.getServerAPI().register(mName, mEmail, mPassword) .subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread());
}
},
new Action2() {
@Override public void call(MoreFragment moreFragment, Token token) {
moreFragment.onRegisterSuccess(token);
}
}, new Action2() {
@Override public void call(MoreFragment moreFragment, Throwable throwable) {
moreFragment.onError(throwable);
}
}
);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
161
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
調用 register 網(wǎng)絡接口,在請求成功調用moreFragment 的 onRegisterSuccess 函數(shù)。
同時在 MorePresenter 中公開一個 register 函數(shù),供 View 層來調用,發(fā)起網(wǎng)絡請求:
public void register(String name, String email, String password) {
mName = name;
mEmail = email;
mPassword = password;
start(REGISTER);
}1
2
3
4
5
61
2
3
4
5
6
然后是 View 層的實現(xiàn),MoreFragment 中對注冊按鈕添加監(jiān)聽,點擊后彈出對話框進行注冊:
@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;
}
}1
2
3
4
5
6
7
81
2
3
4
5
6
7
8
在看下 showRegisterDialog() 函數(shù):
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();
}
});
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
381
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
AlertDialog 采用了自定義的 layout,包含 用戶名、郵箱、密碼、確認密碼 這4個文本編輯框。我們給確定按鈕注冊了一個空的監(jiān)聽器,這是因為在點擊確定時要驗證密碼和確認密碼是否相同,如果不同,彈出提示消息,對話框不會消失,這樣用戶才有機會進行修改,如果監(jiān)聽器不是 null,那用戶點擊確定后對話框必定會消失。所以這里給確定按鈕注冊一個空的 DialogInterface.OnClickListener,并在對話框顯示出來給,查找到確認按鈕,并注冊一個 View.OnClickListener,來實現(xiàn)上述需求。
如果兩次密碼確認一致,則調用 Presenter 中的 register 函數(shù),并取消對話框。
注冊成功后的相應比較簡單,直接彈出提示:
public void onRegisterSuccess(Token token) {
Toast.makeText(getContext(), "注冊成功,請登錄", Toast.LENGTH_SHORT).show();
}1
2
31
2
3
看下效果,點擊注冊,彈出對話框:
輸入密碼不一致時,點擊確定,彈出提示,對話框不消失:
密碼輸入一致,點擊確定,發(fā)起注冊請求,對話框消失,提示注冊成功:
3. 實現(xiàn)登錄功能
先看一下登錄接口返回的數(shù)據(jù),其中包含了用戶信息和 token:
{
"user": {
"id": 9,
"name": "user666",
"email": "user6@user.com"
},
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjksImlzcyI6Imh0dHA6XC9cL2hvbWVzdGVhZC5hcHBcL2FwaVwvbG9naW4iLCJpYXQiOjE0OTM3NTQ0NjUsImV4cCI6MTQ5Mzc1ODA2NSwibmJmIjoxNDkzNzU0NDY1LCJqdGkiOiJGeTRmb2FYeWI5Q2RZTGlXIn0.Isu2XpPypZIMjB8P8Fis-qLknij6hdWfaQ_Jl1Gzo-o"
}1
2
3
4
5
6
7
81
2
3
4
5
6
7
8
登錄功能和注冊功能很相似,但是登錄成功后我們要根據(jù)服務器返回的用戶信息更新UI,并對 token進行存儲。
首先在 Model 路徑下創(chuàng)建 User 類和 Account 類用于解析和存儲網(wǎng)絡數(shù)據(jù):
@JsonIgnoreProperties(ignoreUnknown = true) public class User {
public String id;
public String name;
public String email;
}1
2
3
4
51
2
3
4
5
@JsonIgnoreProperties(ignoreUnknown = true) public class Account {
public User user;
public String token;
}1
2
3
41
2
3
4
ServiceAPI 增加 網(wǎng)絡接口,我們用 Account類來解析接口返回的 Json數(shù)據(jù):
@GET("login") Observable login(
@Query("email") String email,
@Query("password") String password
);1
2
3
41
2
3
4
接下來在 MorePresenter 的 onCreate 中注冊網(wǎng)絡請求:
restartableFirst(LOGIN,
new Func0>() {
@Override public Observable call() {
return App.getServerAPI().login(mEmail, mPassword)
.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread());
}
},
new Action2() {
@Override public void call(MoreFragment moreFragment, Account account) {
moreFragment.onLoginSuccess(account);
}
},
new Action2() {
@Override public void call(MoreFragment moreFragment, Throwable throwable) {
moreFragment.onError(throwable);
}
}
);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
181
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
網(wǎng)絡請求成功后會調用 MoreFragment 的 onLoginSuccess 函數(shù)。
同時在 MorePresenter 中公開 login 函數(shù)供外部調用:
public void login(String email, String password) {
mEmail = email;
mPassword = password;
start(LOGIN);
}1
2
3
4
51
2
3
4
5
接下來是 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();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
351
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
因為不需要做校驗,登錄對話框比注冊時簡單,點擊確定后就調用 MorePresenter 的 login 函數(shù),發(fā)送登錄請求。
再看一下登錄成功后的處理:
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);
}1
2
3
4
5
6
7
8
9
10
111
2
3
4
5
6
7
8
9
10
11
首先對賬號信息進行存儲,包含用戶的 ID、name、email,以及此次的 token,這些信息會被保存到 SharedPreferences 里,AccountManager 是我們自定義的賬號管理類,可以在應用的任何地方存儲和獲取用戶信息,具體在下一節(jié)中介紹。
然后就是 UI 的變更了,登錄成功后將登錄和注冊按鈕隱藏,顯示用戶的 name 和email,顯示退出按鈕,將獲取通知按鈕設置為可點擊。
最后看下實現(xiàn)效果,點擊登錄按鈕,彈出對話框:
登錄成功后界面變化:
4. 實現(xiàn)全局賬號信息存取
JWT 的 token 的有效期一般設置為數(shù)小時,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;
}1
2
3
4
5
6
7
8
9
101
2
3
4
5
6
7
8
9
10
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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
531
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
代碼比較簡單,提供一個靜態(tài)函數(shù) create 來創(chuàng)建并返回 AccountManager,同時做好 SharedPreferences 存取的準備工作,這里用到了 App 里的getAppContext() 函數(shù)來獲取全局 context。之后提供了對賬號Account
的存儲、讀取和清除函數(shù),也可以單獨存取 User 和 token。
4. 訪問需要認證的 API
獲取到 token 之后就可以訪問 需要認證的 API 了,服務器已經(jīng)準備好了兩個 API,一個是簡單測試用的 notices API,認證成功就返回一段話,還有一個就是 user API,認證成功后返回 User 信息,user API 當前用來做校驗 token 是否有效使用,在后面的章節(jié)介紹,這一節(jié)只介紹 notices API。
首先添加 Model,在 ServiceAPI 下創(chuàng)建網(wǎng)絡接口:
public class Notice {
public String content;
}1
2
31
2
3
@GET("notices") Observable getNotice(
@Header("Authorization") String token
);1
2
31
2
3
注意和之前的接口不同,這里添加了 @Header 注解,這樣發(fā)送網(wǎng)絡請求時會添加認證頭。
接下來 MorePresenter 注冊請求,公開函數(shù),和之前的基本類似,不同的是在訪問 API 接口時,調用了 AccountManager 來獲取 token,注意 token 前加了 Bearer:
restartableFirst(NOTICE,
new Func0>() {
@Override public Observable call() {
return App.getServerAPI().getNotice("Bearer " + AccountManager.create().getToken())
.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread());
}
},
new Action2() {
@Override public void call(MoreFragment moreFragment, Notice notice) {
moreFragment.onGetNoticeSuccess(notice);
}
},
new Action2() {
@Override public void call(MoreFragment moreFragment, Throwable throwable) {
moreFragment.onError(throwable);
}
}
);
public void getNotice() {
start(NOTICE);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
211
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
然后是 View 層處理,也很簡單,點擊獲取通知按鈕,調用 MorePresenter 的 getNotice函數(shù),請求成功后,顯示獲取的通知消息:
@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);
}1
2
3
4
5
6
7
8
9
10
11
121
2
3
4
5
6
7
8
9
10
11
12
最后看下效果,登錄成功后獲取通知:
假如 token 已經(jīng)過期,我們再取點擊按鈕,則無法通過認證:
5. 實現(xiàn)退出賬號
因為 JWT 是無狀態(tài)無連接的認證方式,服務器上不需要保存 token 狀態(tài),因此退出時只需要清除掉客戶端本地的賬號信息就行了,不需要和服務器作交互。
看下實現(xiàn)代碼,調用 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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
161
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
6. UI 恢復 和 token 檢測
上面的代碼已經(jīng)實現(xiàn)了登錄成功后用戶信息和 token 的存儲,那么我們希望在應用或者特定的 View 啟動的時候,能夠將存儲的用戶信息恢復到 UI 上,并且檢測下存儲的 token 是否有效,是否過期,如果未過期,則自動恢復 UI 到已登錄的狀態(tài),不需要用戶再登錄。綜上,我們在 MoreFragment 啟動的時候,訪問 user API 接口,攜帶存儲的 token,給服務器驗證,如果驗證成功,則恢復 UI 到登錄成功后的樣子,如果驗證失敗,則保留未登錄的狀態(tài),等待用戶再次輸入賬號密碼進行登錄。
要實現(xiàn)上述功能,和之前的代碼一樣的,首先創(chuàng)建好 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;
}1
2
3
4
5
6
7
8
9
10
111
2
3
4
5
6
7
8
9
10
11
首先通過 AccountManager 獲取存儲的 token,如果 token 是空的,說明之前就是未登錄狀態(tài),不需要處理,UI 或保持初始的未登錄狀態(tài),如果 token 非空,則調用 MorePresenter 來訪問 user API。
如果認證失敗,則彈出提示,UI 不會有變化,保持未登錄狀態(tài)。如果認證成功,則調用 MoreFragment 的 onGetUserSuccess 函數(shù)來更新UI,這里恢復 UI 時用戶信息的來源可以是本地 SharedPreferences,也可以是服務器剛返回的數(shù)據(jù),正常情況下兩者應該是一樣的,但是我們認為服務器的數(shù)據(jù)更可信,因而采用服務器的數(shù)據(jù)更新 UI,并將服務器的 User 數(shù)據(jù)進行存儲。
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);
}1
2
3
4
5
6
7
8
9
10
111
2
3
4
5
6
7
8
9
10
11
為了測試效果,我們特意將服務器上的 token 有效期配置為1分鐘,修改服務器的 .env 文件,設置 JWT_TTL=1 。
看下效果,登錄成功或退出應用,在 token 過期前重新啟動應用,進入 MoreFragment 頁面,自動進入已登錄狀態(tài):
再次退出應用,等 token 過期后,啟動應用,提示未認證,進入 MoreFragment 頁面,處于未登錄狀態(tài):
后記
JWT 方式的 API 基本功能,以及 Laravel 服務器和 Android 客戶端的實現(xiàn)方式就介紹完了,JWT 這種無狀態(tài)的方式還是很適合 API 認證的,客戶端只需要生成和驗證 token,客戶端只需要存儲 token 就行,token 有效期就存儲在 token 自身,不需要服務器為每個登錄的用戶去存儲 token 狀態(tài),這樣大大減小了開銷。并且 token 本身就包含了用戶 ID 等一些非敏感信息,因此在很多網(wǎng)絡請求的時候,甚至可以只傳輸 token,不需要再有單獨的用戶信息參數(shù),也是減少了一筆開銷。
上面介紹的內容可以完成 JWT 認證的基本功能了,但還是有很多可以改善的地方,比如 password 是明文傳輸?shù)?#xff0c;很不安全,這個作為一個 Demo 項目,就沒考慮這么周全。另外由于 JWT 方式一個天生的缺點,服務器無法控制 token 的有效期,只要你發(fā)出了一個 token,它的有效期就定死了,因為服務器不存儲 token 狀態(tài),所有就無法提前結束 token 生命周期。
因此在配置 token 有效期是要比較謹慎,不能太長了。但是太短也不行,因為 token 方式,包括除了 JWT 外的其他 token方式,其實就是用 token 代替賬號密碼作為用戶驗證的憑證,只要一次賬號密碼驗證通過,后續(xù)一段時間內只需要 token 就可以驗證,不需要密碼,降低風險,有效期太短必然導致密碼頻繁發(fā)送,且用戶需要頻繁地登錄,影響用戶體驗。所有要根據(jù)實際情況選擇一個合適的有效期。
另外 token 到期后如何處理也是個問題。如果用戶沒使用應用的時候 token 過期了,那還好點,想想用戶正在操作應用的時候,突然 token 就到期了,操作被中斷,需要重新登錄,那一定是一件很不爽的事情。JWT 本身也提供了一種解決方法,設置了一個 token 刷新時間,在 token 過期但是沒超過刷新時間的情況下,用舊的 token 可以獲取到新的 token。另外也可以考慮在每次發(fā)送 API 請求的時候都去刷新 token,或者周期性發(fā)送心跳包來更新 token,不過這在并發(fā)請求比較多的時候,也會涉及到異步?jīng)_突的問題,需要謹慎考慮。
后續(xù)如果有時間,再深入研究下這些問題。
總結
以上是生活随笔為你收集整理的接口使用jwt返回token_API接口JWT方式的Token认证(下),客户端(Android)的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 委比是怎么算出来的
- 下一篇: 云计算到底是怎么玩的?