一、序言
现在正处于前后端分离时代,而解决前后端分离衍生出各种各样的技术和技巧,以前没分离的时候可以通过cookie或session的方式来进行前后端交互,当然,前后端分离后,这两种方式仍然适用,但是却比之前更为复杂。而我这里采用更为简便的token机制,现在很多公司采用的也是token机制,可以看出token机制的优越性。这篇文章主要讲解在Android中如何使用token与后台进行交互,关于后台部分有兴趣的可以看我的另一篇博客 SpringBoot+Shiro+JWT实现权限管理 。废话不多说,接下来进入正题。
二、什么是token?
首先,从字面上理解,token 是令牌的意思。在开发中,token 则是由服务器通过一些特定的算法生成的字符串,是客户端和服务端交互的令牌。token 生成之后会发送给客户端,客户端收到 token 之后,它每次与服务器交互都需要携带 token,否则服务器就会返回错误码。
题外话:在实战中,返回的错误码都是由后端编程人员决定的,而我们 Android 开发要做的就是根据服务器返回的错误码告诉用户这里出了什么错误。
三、为什么要使用token?
一般情况下,客户端都需要向服务端获取数据和上传数据,那么服务器就需要知道具体的每一个请求是由哪一个用户发起的,服务端才好操作相对应用户的数据。在初始阶段是通过每次请求都带上用户名和密码来唯一确认一个请求的,但是这样会使服务器的压力增大,会增加服务器的查询操作。因此 token 应运而生,在每次登录成功之后,都保存服务端生成的唯一 token 到本地,就可以唯一确认一个请求了,我们就可以顺畅地访问服务端了。
四、在Android中使用token
接下来我将通过一个案例,讲解token在Android中如何使用。
案列介绍:在未登录时,向后端请求不到数据,只有在登录之后才可以向后端访问数据。
这个案例看起来相当简单,但如果要做的好的话,却没有你想的那么容易。
案例分析:当我们为登录时,本地是没有存储token的,而登录之后,我们需要将token保存在本地,以后每次向后台发送请求时,将在请求头带上token。之前我在写vue的时候用的是拦截器机制,就是对每一个网络请求进行拦截,便可以实现全局管理token,所以我在这里用的也是拦截器机制。
五、技术栈
Retrofit:用来进行网络请求的框架,这东西话说是在Github上Android框架排行榜第一,果然不是吹的,用起来感觉叼叼的,现在 很多东西也是用的这东西,其实这东西就是对OkHttp再次进行了封装。
Okhttp:我主要是用来做网络拦截器。
Butterknife:这个大家应该都懂,就是用来进行控件绑定,使用这个东西就可以使用注解的方式对控件进行绑定。
Fastjson:阿里巴巴的开源框架,用它可以很简单的对json数据进行解析。
六、数据准备
以下是我后台返回的json数据:
登录成功之后返回的json:
{
"status": 200,
"message": "success",
"data": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjU3MDA1OTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.AJ1P6VYW1vH10iFQxwrVKihbaxqDTuVu2K8p-drh0KY"
}
}
请求数据成功的json:
{
"status": 200,
"message": "success",
"data": {
"info": "成功获得信息!"
}
}
请求数据失败的json:
{
"status": 400,
"message": "fail",
"data": {
"info": "token认证失败!"
}
}
七、添加所需依赖
在Module的build.gradle中添加
//retrofit2网络请求
implementation 'com.squareup.retrofit2:retrofit:2.1.0'
//解析器
api 'com.squareup.retrofit2:converter-gson:2.1.0'
//快速解析json
implementation 'com.alibaba:fastjson:1.1.54.android'
//通过注解绑定控件
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
在AndroidManifest.xml中添加网络权限
<uses-permission android:name="android.permission.INTERNET" />
八、编写布局界面
主页 activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<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"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="登录"/>
<Button
android:id="@+id/getMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送请求"/>
</LinearLayout>
登录页 activity_login.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00FFFFFF">
<LinearLayout
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户登录"
android:textSize="18dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:layout_marginTop="10dp"
android:gravity="center"
android:orientation="vertical"
android:padding="5dp">
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:hint="请输入用户名" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical">
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:hint="请输入密码"
android:password="true" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/buttom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/edittext"
android:layout_marginTop="20dp"
android:orientation="vertical"
android:padding="15dp">
<Button
android:id="@+id/user_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="登录" />
</LinearLayout>
<LinearLayout
android:id="@+id/bottom1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/buttom"
android:layout_marginTop="20dp"
android:gravity="center"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="我要注册" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text=" | " />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="忘记密码?" />
</LinearLayout>
</RelativeLayout>
九、编写Acyivity
主页Activity:
package com.chen.retrofittest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@BindView(R.id.login)
public Button loginButton;
@BindView(R.id.getMessage)
public Button getMessageButton;
private Retrofit retrofit;
private RequestInterface request;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
retrofit = new Retrofit.Builder()
.baseUrl("http://10.0.2.2/")
.client(OkHttpUtil.getOkHttpClient())
.addConverterFactory(GsonConverterFactory.create())
.build();
}
@OnClick(R.id.login)
public void login(){
Intent intent = new Intent(MainActivity.this , LoginActivity.class);
startActivity(intent);
}
@OnClick(R.id.getMessage)
public void getMessage() {
request = retrofit.create(RequestInterface.class);
Call<ResponseBody> call = request.getMessage();
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
String s = null;
try {
s = new String(response.body().bytes());
} catch (IOException e) {
e.printStackTrace();
}
Log.e(TAG, s);
JsonElement je = new JsonParser().parse(s);
String data = je.getAsJsonObject().get("status").toString();
String info=je.getAsJsonObject().get("data").getAsJsonObject().get("info").toString().replace("\"", "");
Log.e(TAG, "状态码: " + data);
Toast.makeText(MainActivity.this, info, Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
}
}
OkHttpUtil是我封装的一个工具类,用来得到OkHttpClient,然后将OkHttpClient添加到Retrofit中,这是一个坑,我之前就踩到这个坑,怪不得我就想不通那个网络拦截器是怎么可能自己自动生效的,加上去之后一切就理解的通了。
登录页Activity:
package com.chen.retrofittest;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import java.io.IOException;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class LoginActivity extends AppCompatActivity {
private static final String TAG = "LoginActivity";
@BindView(R.id.username)
public EditText edit_username;
@BindView(R.id.password)
public EditText edit_password;
private Retrofit retrofit;
private RequestInterface request;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
ButterKnife.bind(this);
retrofit = new Retrofit.Builder()
.baseUrl("http://10.0.2.2/")
.client(OkHttpUtil.getOkHttpClient())
.addConverterFactory(GsonConverterFactory.create())
.build();
request = retrofit.create(RequestInterface.class);
}
@OnClick(R.id.user_login)
public void user_login() {
String username = edit_username.getText().toString();
String password = edit_password.getText().toString();
Call<ResponseBody> call = request.login(username, password);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
String s = null;
try {
s = new String(response.body().bytes());
} catch (IOException e) {
e.printStackTrace();
}
JsonElement je = new JsonParser().parse(s);
String status = je.getAsJsonObject().get("status").toString();
if (status.equals("200")) {
String token = je.getAsJsonObject().get("data").getAsJsonObject().get("token").toString().replace("\"", "");
Log.e(TAG, "token:" + token);
SharedPreferences sharedPreferences = getSharedPreferences("myConfig", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();//获取编辑器
editor.putString("token", token);
editor.commit();//提交修改
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
finish();
}else{
Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
}
}
十、配置网络拦截器
package com.chen.retrofittest;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* token拦截器
*/
public class TokenInterceptor implements Interceptor {
private String token; //用于添加的请求头
@Override
public Response intercept(Chain chain) throws IOException {
//从SharePreferences中获取token
SharedPreferences sharedPreferences =BaseApplication.getContext().getSharedPreferences("myConfig", Context.MODE_PRIVATE);
token = sharedPreferences.getString("token", "");
Request request = chain.request()
.newBuilder()
.addHeader("token", token)
.build();
Response response = chain.proceed(request);
//Log.e("返回数据:",response.body().string());
return response;
}
}
十一、创建OkHttp工具类
package com.chen.retrofittest;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
public class OkHttpUtil {
public static OkHttpClient getOkHttpClient() {
//定制OkHttp
OkHttpClient.Builder httpClientBuilder = new OkHttpClient
.Builder();
//设置超时时间
httpClientBuilder.connectTimeout(3000, TimeUnit.SECONDS);
httpClientBuilder.writeTimeout(3000, TimeUnit.SECONDS);
httpClientBuilder.readTimeout(3000, TimeUnit.SECONDS);
//使用拦截器
httpClientBuilder.addInterceptor(new TokenInterceptor());
return httpClientBuilder.build();
}
}
十二、编写请求接口
package com.chen.retrofittest;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface RequestInterface {
@POST("login")
Call<ResponseBody> login(@Query("username")String username,@Query("password") String password);
@POST("getMessage")
Call<ResponseBody> getMessage();
}