接口使用jwt返回token_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\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 客户端上实现对这些接口的访问。

本文采用的 Android 代码下载地址:

https://github.com/zhongchenyu/jokes

由于后续可能会重构代码,本文使用的代码保存在 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"/>

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

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

登录之前效果如下,界面显示登录和注册按钮,获取通知按钮为不可点击状态。

登录后效果如下,登录和注册按钮隐藏,变为显示用户名和邮箱,退出按钮也被显示出来,并且获取通知按钮变为可以点击。

2. 实现注册功能

在 ServiceAPI 下添加网络接口:

@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 架构,网络请求是在 Presenter 中完成的,那么在 MorePresenter 的 onCreate 函数中注册请求:

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 网络接口,在请求成功调用moreFragment 的 onRegisterSuccess 函数。

同时在 MorePresenter 中公开一个 register 函数,供 View 层来调用,发起网络请求:

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 层的实现,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;

}

}1

2

3

4

5

6

7

81

2

3

4

5

6

7

8

在看下 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();

}

});

}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个文本编辑框。我们给确定按钮注册了一个空的监听器,这是因为在点击确定时要验证密码和确认密码是否相同,如果不同,弹出提示消息,对话框不会消失,这样用户才有机会进行修改,如果监听器不是 null,那用户点击确定后对话框必定会消失。所以这里给确定按钮注册一个空的 DialogInterface.OnClickListener,并在对话框显示出来给,查找到确认按钮,并注册一个 View.OnClickListener,来实现上述需求。

如果两次密码确认一致,则调用 Presenter 中的 register 函数,并取消对话框。

注册成功后的相应比较简单,直接弹出提示:

public void onRegisterSuccess(Token token) {

Toast.makeText(getContext(), "注册成功,请登录", Toast.LENGTH_SHORT).show();

}1

2

31

2

3

看下效果,点击注册,弹出对话框:

输入密码不一致时,点击确定,弹出提示,对话框不消失:

密码输入一致,点击确定,发起注册请求,对话框消失,提示注册成功:

3. 实现登录功能

先看一下登录接口返回的数据,其中包含了用户信息和 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

登录功能和注册功能很相似,但是登录成功后我们要根据服务器返回的用户信息更新UI,并对 token进行存储。

首先在 Model 路径下创建 User 类和 Account 类用于解析和存储网络数据:

@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 增加 网络接口,我们用 Account类来解析接口返回的 Json数据:

@GET("login") Observable login(

@Query("email") String email,

@Query("password") String password

);1

2

3

41

2

3

4

接下来在 MorePresenter 的 onCreate 中注册网络请求:

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

网络请求成功后会调用 MoreFragment 的 onLoginSuccess 函数。

同时在 MorePresenter 中公开 login 函数供外部调用:

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 函数,发送登录请求。

再看一下登录成功后的处理:

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 是我们自定义的账号管理类,可以在应用的任何地方存储和获取用户信息,具体在下一节中介绍。

然后就是 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;

}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

代码比较简单,提供一个静态函数 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;

}1

2

31

2

3

@GET("notices") Observable getNotice(

@Header("Authorization") String token

);1

2

31

2

3

注意和之前的接口不同,这里添加了 @Header 注解,这样发送网络请求时会添加认证头。

接下来 MorePresenter 注册请求,公开函数,和之前的基本类似,不同的是在访问 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函数,请求成功后,显示获取的通知消息:

@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 已经过期,我们再取点击按钮,则无法通过认证:

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;

}

}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 检测

上面的代码已经实现了登录成功后用户信息和 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;

}1

2

3

4

5

6

7

8

9

10

111

2

3

4

5

6

7

8

9

10

11

首先通过 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);

}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 页面,自动进入已登录状态:

再次退出应用,等 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,不过这在并发请求比较多的时候,也会涉及到异步冲突的问题,需要谨慎考虑。

后续如果有时间,再深入研究下这些问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/511816.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

技术分享 | 使用 mPaaS 配置 SM2 国密加密指南

简介:随着移动智能终端的广泛应用,敏感信息极易被监控或盗取,给国家、企事业及个人带来极大政治、经济损失。金融和重要领域的各个企业正在逐步落实并完成国产密码改造工作。为解决客户侧因更换加密算法造成的种种不便,mPaaS 现已…

我的世界1.8.9无需正版的服务器,我的世界1period;8period;9服务器纯洁服地址 | 手游网游页游攻略大全...

发布时间:2015-09-26怎么创建属于自己的服务器那?开服教程为大家准备好了.如果我们想和小伙伴们联机进行玩耍的话就必须要建立一个服务器,要不然就是加入别人的服务器,那么服务器的建立方法是什么呢?我 ...标签:我的世界攻略 我的世界 我的世界开服发布…

报表功能升级|新增的这4项图表组件太好用了吧

简介:你们要的交叉透视表、词云、日历热力图、雷达图安排上啦~ 宜搭3.0上线已满一月,大家体验如何呢? 为了让大家更好地实现一站式数据加工处理及展示,我们近期针对报表板块做了升级 我们新上线了4项大家在社区呼声…

进程切换的本质是什么?

作者 | 陆小凤来源 | 码农的荒岛求生我们都知道操作系统最重要的功能之一是多任务能力,也就是可以运行超过CPU数量的程序——即进程,要想实现这一功能就必须具备将有限的CPU资源在多个进程之间分配的能力,在程序员看来,我们的程序…

python脚本限制_解决python 上传图片限制格式问题

终于忙完有空更新了,这次说下一个比较简单的东西,限制上传图片格式问题。先上代码!img_file D:\\image\\test.jpg# uuid生成文件名key shortuuid.ShortUUID().random(length9)if img_file.size > 2097152:raise exceptions.APIException…

lol1.7更新服务器维护,lol今天停机维护到几点11日7.1版本停机更新公告

lol今天停机维护到几点,lol1月11日停机维护更新公告,lol今天怎么进不去2017?下面小编将英雄联盟发布的停机公告详细给大家介绍。lol今天停机维护到几点1月11日早7点30分全区停机维护,预计停机时间为07:30-12:0011日7.1版本停机更…

Log4j漏洞不仅仅是修复,更需要构建有效预警机制

简介:软件的漏洞有时不可避免,根据Gartner的相关统计,到 2025 年,30% 的关键信息基础设施组织将遇到安全漏洞。日志服务SLS,可帮助快速部署一个预警机制,使得漏洞被利用时可以快速发现并及时响应。通过使用…

python取的键不存在_Python3基础 dict get 在查询不存在的键时,返回指定的内容

Python : 3.7.0OS : Ubuntu 18.04.1 LTSIDE : PyCharm 2018.2.4Conda : 4.5.11typesetting : Markdowncode"""Author : 行初心Date : 18-9-23Blog : www.cnblogs.com/xingchuxinGitee : gitee.com/zhichengjiu"""def main():my_dict {子: 鼠, 丑…

【ClickHouse 技术系列】- ClickHouse 聚合函数和聚合状态

简介:本文翻译自 Altinity 针对 ClickHouse 的系列技术文章。面向联机分析处理(OLAP)的开源分析引擎 ClickHouse,因其优良的查询性能,PB级的数据规模,简单的架构,被国内外公司广泛采用。本系列技…

【ClickHouse 技术系列】- ClickHouse 中的嵌套数据结构

简介:本文翻译自 Altinity 针对 ClickHouse 的系列技术文章。面向联机分析处理(OLAP)的开源分析引擎 ClickHouse,因其优良的查询性能,PB级的数据规模,简单的架构,被国内外公司广泛采用。本系列技…

太强了!这款开源终端工具可查询 IP 信息~

作者 | JackTian来源 | 杰哥的IT之旅在 Linux 下,有dig、nslookup、traceroute等多种非常实用的网络调试工具。dig:是常用的域名查询工具,可以用来测试域名是否正常。nslookup:是常用的域名查询工具,也就是查 DNS 信息…

顺序写磁盘比随机写内存_深入理解 linux磁盘顺序写、随机写

一、前言随机写会导致磁头不停地换道,造成效率的极大降低;顺序写磁头几乎不用换道,或者换道的时间很短。本文来讨论一下两者具体的差别以及相应的内核调用。二、环境准备三、fio介绍通过fio测试,能够反映在读写中的状态&#xff0…

为余势负天工背,云原生内存数据库Tair助力用户体验优化

简介:作为双11大促承载流量洪峰的利器,Tair支撑了电商交易核心体验场景。不仅在数十亿QPS的峰值下保持着亚毫秒级别的顺滑延迟,同时在电商交易核心体验场景上也做出了技术创新。 作者 | 漠冰 来源 | 阿里技术公众号 作为双11大促承载流量洪峰…

【视频特辑】数据分析师必备,快速制作一张强大好用的大宽表

简介:随着企业数字化进程的逐步推进,在日常经营过程当中会沉淀下越来越多的数据信息。 每当想做数据分析的时候,就会发现想要的指标分散在不同的数据源、数据集、数据表当中。 Quick BI的数据关联功能,可以帮助数据分析师快速将指…

读取硬盘前的准备工作有哪些?

作者 | 闪客sun来源 | 低并发编程读取硬盘数据到内存中,是操作系统的一个基础功能。读取硬盘需要有块设备驱动程序,而以文件的方式来读取则还有要再上面包一层文件系统。把读出来的数据放到内存,就涉及到内存中缓冲区的管理。上面说的每一件事…

【视频特辑】提效神器,如何用Quick BI高效配置员工的用数权限

简介:随着企业数字化进程逐步加速,企业所产生和积累的数据资源日益增多。每当员工的用数权限发生变动,管理员都需要进行复杂繁琐的重复性配置流程,不仅耗时耗力还容易出错。 如何能便捷地对员工用数权限进行高效管理?试…

python苦逼_自学Python编程的第六天(最后代码有更好的请告诉我)----------来自苦逼的转行人...

2019-09-16-23:09:06自学Python的第六天,也是写博客的第六天今天学的内容是有关dict字典的用法看视频加上练习,目前还没遇到有难点,但是感觉很不好的样子没有难点以后突然出现一个有关字典的程序感觉要炸,还是得继续掌握看最后的代码吧,有更好的请告诉我我是一条快乐的分割线一…

让容器跑得更快:CPU Burst 技术实践

简介:让人讨厌的 CPU 限流影响容器运行,有时人们不得不牺牲容器部署密度来避免 CPU 限流出现。我们设计的 CPU Burst 技术既能保证容器运行服务质量,又不降低容器部署密度。CPU Burst 特性已合入 Linux 5.14,Anolis OS 8.2、Aliba…

实时数仓Hologres首次走进阿里淘特双11

简介:这是淘特在阿里巴巴参与的第二个双11大促,大促期间累计超过上千万消费者在此买到心仪的商品,数百万家商家因为淘特而变得不同,未来,淘特也将会继续更好的服务于下沉市场,让惠民走近千万家。 2021年11…

调用某个按钮事件_Event 对象之事件句柄 (Event Handlers)

所谓的Event 对象代表事件的状态,比如事件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态。事件通常与函数结合使用,函数不会在事件发生前被执行!事件句柄HTML 4.0 的新特性之一是能够使 HTML 事件触发浏览器中的行为&#x…