This article was first published on July 27, 2017CSDN, please indicate the source if necessary.
Source code portal
The previous article focused on packaging Retrofit to make it easier to use. In the previous encapsulation, it is not elegant to manually invoke the previous request after the token expires and refreshes the token again. Therefore, based on the original basis, this article will optimize the token verification mechanism based on the encapsulation in the previous article. Enable expiration auto-refresh and re-invoke the request. You will learn how to implement token authentication through the following sections.
- Why introduce token mechanism
- The token mechanism verification process
- RxJava+Retrofit package implements token validation
1. Why is the Token mechanism introduced
1. What is token?
Token refers to a token that is usually carried by the client to the server. The server generates a string based on the IMEI/Mac of the client and returns it to the client and sets the validity period for the string. This serves as a token for client and server interaction. The client carries the token to the server with each request in place of the username and password. The server verifies that the token is valid and returns data to the client. Otherwise, the server returns an error code to the client. The client processes the error code accordingly.
2. So why introduce token mechanism?
There are two main reasons: (1) To ensure safety. Without the introduction of token, we would have to carry the username and password every time we requested data. That is, the username and password are transferred over the network every time the data is requested. This greatly increases the security risk, easy to be intercepted by hackers. Therefore, the introduction of token mechanism also ensures security to a certain extent. (2) Reduce the server pressure. Before the token mechanism is introduced, the user name and password need to be sent to the server to verify the user’s identity. Server authentication of user names and passwords is a query operation, and a large number of users can put a corresponding strain on the server. After the token mechanism is introduced, the server can use the token as the unique identifier of a user to verify whether the user’s identity is legitimate. This can greatly reduce the strain on the server.
Ii. Verification process of token mechanism
The token verification process is not unique, and you can decide which one to use. In this paper, OAuth2.0 protocol is used to implement token authentication mechanism. The main steps are as follows:
- The token and refreshToken are obtained and saved locally after login using the user name and password.
- The token is valid for 2 hours and the refreshToken is valid for 15 days.
- Each network request requires a Token instead of a refreshToken.
- If the server determines that the token has expired, it returns the corresponding error code. After determining the error code, the client invokes the refreshToken interface to obtain the token and refreshToken again and stores the token.
- If the app is not used for 15 consecutive days or the user changes the password, the refreshToken expires and you need to log in again to obtain the Token and refreshToken.
RxJava+Retrofit package implements token automatic refresh
With the above two sections in mind, we can implement token authentication ourselves. Here we use RxJava and Retrofit encapsulated in the previous article to implement the token mechanism.
1. Login authentication, obtain the token and refresh_token
The login requires two parameters: username, password, and AppKey as a unique ID. The server returns token and refreshToken each time the login succeeds. The entity class LoginRequest is as follows:
public class LoginRequest extends BaseRequest{
private String userId;
private String password;
}
Copy the code
Then we can call the login interface to get the token. After a successful login, we can store the Token and refreshToekn locally. For example, to submit a form, the code is as follows:
public void login() {
Map<String, Object> map = MapUtils.entityToMap(new BaseRequest());
map.put("userId"."123456");
map.put("password"."123123");
IdeaApi.getApiService()
.login(map)
.subscribeOn(Schedulers.io())
.compose(activity.<BasicResponse<LoginResponse>>bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver<BasicResponse<LoginResponse>>(activity) {
@Override
public void onSuccess(BasicResponse<LoginResponse> response) {
LoginResponse results = response.getResults();
ToastUtils.show("Login successful! Access to the token" + results.getToken() + ", ready to store locally."); / * * * these data can be stored in the User, the User stored in the local database. * / SharedPreferencesHelper put (activity,"token", results.getToken());
SharedPreferencesHelper.put(activity, "refresh_token", results.getRefresh_token());
SharedPreferencesHelper.put(activity, "refresh_secret", results.getRefresh_secret()); }}); }Copy the code
2. Identify the requirement and throw an exception
Since the validity of tokens is short, we need to refresh tokens frequently to ensure their validity. If the token expires or is invalid when requesting the network, the server will return the corresponding error code. You need to check whether the token is invalid based on the status code. If invalid, invoke the Refresh token interface to obtain the token again. If refreshToekn also expires then we need to log in again.
Now, our requirement is to implement automatic refresh after the token expires. After the refresh is successful, the original request will be automatically invoked. If the refreshToken also expires, the login will be logged out. Based on this, we can think of the retryWhen operator of RxJava, which determines that tokens are out of date and automatically refreshes.
Then, our first task is how to determine the expiration of token and refreshToken. Remember in the last article we modify GsonResponseBodyConverter class to the response code to retrieve data from the data according to the background. Obviously, this is the appropriate place to determine whether the token has expired. In the next see GsonResponseBodyConverter code:
@Override
public Object convert(ResponseBody value) throws IOException {
try {
BasicResponse response = (BasicResponse) adapter.fromJson(value.charStream());
if (response.getCode() == SUCCESS) {
if (response.getData() == null)
throw new ServerNoDataException(0, "");
return response.getData();
} else if (response.getCode() == TOKEN_EXPIRED) {
throw new TokenExpiredException(response.getCode(), response.getMessage());
} else if (response.getCode() == REFRESH_TOKEN_EXPIRED) {
throw new RefreshTokenExpiredException(response.getCode(), response.getMessage());
} else if(response.getCode() ! = SUCCESS) {// API specific error, Throw New ServerResponseException(Response.getCode (), Response.getMessage ())) in the corresponding onError method of DefaultObserver; } } finally { value.close(); }return null;
}
Copy the code
We have defined several exceptions in the above code, and throw the corresponding exceptions after judging the corresponding error codes. Here we can focus on TokenExpiredException under the care and RefreshTokenExpiredException, respectively, represents the token and refreshToken expired date.
3. Add a proxy to refresh the token expiration automatically
Because almost all requests require validation of token expiration, uniform processing is required. We can use proxy classes to do uniform proxy handling for Retrofit’s apis. The code is as follows:
public class IdeaApiProxy implements IGlobalManager {
@SuppressWarnings("unchecked")
public <T> T getApiService(Class<T> tClass,String baseUrl) {
T t = RetrofitService.getRetrofitBuilder(baseUrl)
.build().create(tClass);
return(T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class<? >[] { tClass }, new ProxyHandler(t, this)); } @Override public voidexitLogin() {}}Copy the code
In this case, we need to create the API request through the getApiService method in IdeaApiProxy. The ProxyHandler implements the InvocationHandler. The ProxyHandler class is our core class for handling automatic token refreshes. The idea is to call method and wrap it with retryWhen, and get the corresponding exception information in retryWhen for processing. See retryWhen code:
@Override
public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
return Observable.just(true).flatMap(new Function<Object, ObservableSource<? >>() { @Override public ObservableSource<? > apply(Object o) throws Exception { try { try {if (mIsTokenNeedRefresh) {
updateMethodToken(method, args);
}
return(Observable<? >) method.invoke(mProxyObject, args); } catch (InvocationTargetException e) { e.printStackTrace(); } } catch (IllegalAccessException e) { e.printStackTrace(); }returnnull; } }).retryWhen(new Function<Observable<Throwable>, ObservableSource<? >>() { @Override public ObservableSource<? > apply(Observable<Throwable> observable) throws Exception {returnobservable.flatMap(new Function<Throwable, ObservableSource<? >>() { @Override public ObservableSource<? > apply(Throwable throwable) throws Exception {if(Throwable instanceof TokenExpiredException) {// Token expiredreturn refreshTokenWhenTokenInvalid();
} else if(throwable instanceof RefreshTokenExpiredException) {/ / RefreshToken expired, perform logged out of the operations. mGlobalManager.logout();return Observable.error(throwable);
}
returnObservable.error(throwable); }}); }}); }Copy the code
The token refresh operation is performed for the exception of TokenExpiredException whose token has expired. The token refresh operation is performed by directly calling Retrofit methods without the need to go through the proxy. And it must be a synchronized code block, together to see refreshTokenWhenTokenInvalid method with the code:
private Observable<? >refreshTokenWhenTokenInvalid() {
synchronized (ProxyHandler.class) {
// Have refreshed the token successfully in the valid time.
if(new Date().getTime() -tokenChangedTime < REFRESH_TOKEN_VALID_TIME) {// Prevent refreshing token mIsTokenNeedRefresh =true;
return Observable.just(true);
} else {
Map<String, Object> map = MapUtils.entityToMap(new BaseRequestData());
RetrofitHelper.getApiService()
.refreshToken(map)
//.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver<RefreshTokenResponse>() {
@Override
public void onSuccess(RefreshTokenResponse response) {
if(response ! = null) {/ / save the data to the local mGlobalManager tokenRefresh (response); mIsTokenNeedRefresh =true; tokenChangedTime = new Date().getTime(); } } @Override public void onError(Throwable e) { super.onError(e); mRefreshTokenError = e; }});if(mRefreshTokenError ! = null) {return Observable.error(mRefreshTokenError);
} else {
return Observable.just(true); }}}}Copy the code
4. Replace the old token after refreshing the token
When the token refresh is successful, shall we replace the old token? The Method class in Java8 already supports dynamically fetching method names, which was not supported in previous Versions of Java. What about here? Looking at retroFIT calls, you can see that RetroFIT can convert methods in an interface into API requests and need to encapsulate parameters. So you need to look at how Retrofit is implemented, right? It turns out that Retrofit adds @Interface annotations for each Method, using getParameterAnnotations from the Method class. The main code is as follows:
private void updateMethodToken(Method method, Object[] args) {
ServerKey serverKey = RealmDatabaseHelper.queryFirstFrom(ServerKey.class);
String token = serverKey.getToken();
if(mIsTokenNeedRefresh && ! TextUtils.isEmpty(token)) { Annotation[][] annotationsArray = method.getParameterAnnotations(); Annotation[] annotations;if(annotationsArray ! = null && annotationsArray.length > 0) {for (int i = 0; i < annotationsArray.length; i++) {
annotations = annotationsArray[i];
for (Annotation annotation : annotations) {
if(the annotation instanceof FieldMap | | the annotation instanceof QueryMap) {/ / in the Map submission formif (args[i] instanceof Map)
((Map<String, Object>) args[i]).put(TOKEN, token);
} else if (annotation instanceof Query) {
if(TOKEN.equals(((Query) annotation).value())) { args[i] = token; }}else if (annotation instanceof Field) {
if(TOKEN.equals(((Field) annotation).value())) { args[i] = token; }}else if(annotation instanceof Part){// Upload fileif (TOKEN.equals(((Part) annotation).value())) {
RequestBody tokenBody = RequestBody.create(MediaType.parse("multipart/form-data"), token); args[i] = tokenBody; }}else if(Annotation Instanceof Body){// Post submits JSON dataif(args[i] instanceof BaseRequest){
BaseRequest requestData= (BaseRequest) args[i];
requestData.setToken(token);
args[i]=requestData;
}
}
}
}
}
mIsTokenNeedRefresh = false; }}Copy the code
Here we iterate through all the token fields and replace them with new tokens. However, the above method only applies to GET requests and POST requests submitted in form format. If it is a POST request and the submission format is JSON, you can add it yourself. In addition, this method does not apply to the way the token is placed in the request header.
(1) Rxjava2+Retrofit perfect encapsulation (2) Rxjava2+Retrofit Token automatic refresh (3) Rxjava2+Retrofit file upload and download
See RxJava+Retrofit for implementing automatic refreshing of global Expired Tokens in the Demo
Download the source code