現在很多移動應用需要與後臺交互,與後臺交互就需要身份認證。最初想到用token的方式是當初看到微信公衆平臺的身份認證時採用了token這樣的一個參數,然後自己也照着實現了一個。
說到這個token機制,一個是調用端,一個是後臺。調用端我用JAVA語言開發的Android應用來展示,後臺用J2EE來展示。
一、token的獲取
token的獲取其實就是移動端的登錄過程,在登錄的時候要先向後臺獲取一個token值,首先來看登錄時Android端的代碼:
String name = (EditText)mName.getText().toString();
String password = (EditText)mPsw.getText().toString();
String url2 = "http://"+ip+":"+port+"/test/be/login.do?method=login";
OkHttpClientManager ohm = OkHttpClientManager.getInstance();
OkHttpClientManager.Param[] params = new OkHttpClientManager.Param[]{
new OkHttpClientManager.Param("name", name),
new OkHttpClientManager.Param("password", password)};
OkHttpClientManager.ResultCallback cb = new OkHttpClientManager.ResultCallback<String>() {
@Override
public void onError(Request request, Exception e) {
String resultMsg = "請求失敗,請檢查網絡連接!";
ToastUtil.showToastMsgError(LoginActivity.this, resultMsg);
cancleAnimation();//關閉登錄等待框
}
@Override
public void onResponse(String response) {
JSONObject returnJson = JSONObject.parseObject(response);
String resultId = returnJson.get("resultId").toString();
if (resultId.equals("00")) {
token = returnJson.get("token").toString();
System.out.println("======================token:" + token);
startApp();//進入應用
} else {
token = "";
String resultMsg = returnJson.get("resultMsg").toString();
ToastUtil.showToastMsgError(NewLoginActivity.this, resultMsg);
cancleAnimation();
}
}
};
OkHttpClientManager.getInstance().postAsyn(url2,cb, params);
OkHttpClientManager是經過封裝的OkHttp的調用類,可以直接調用向後臺發起http請求。
然後我們來看下後臺的處理,這裏後臺有幾個注意點:首先,login接口必須不被攔截器和過濾器給攔住,然後將用戶名和密碼傳輸到ctrl層,然後驗證該用戶名密碼的真實有效性,如果合適身份,返回token,如果身份有問題,返回相應的提示信息。
@RequestMapping(params="method=login")
@ResponseBody
public String login(HttpServletRequest request,HttpServletResponse response,SysUser command) throws Exception{
String resStr = "";
try{
String name = command.getName();
String password = command.getPassword();
UserSession userSession=new UserSession();
// 設置usersession登錄模式
userSession.setDlms("1");
command.setDlms("1");
SysUser sysuser=new SysUser();
Department department=new Department();
if(name==null||name.length()<1||name.indexOf("'")!=-1){
throw new Exception("該用戶不存在!");
}
if(password==null||password.length()<1||password.indexOf("'")!=-1){
throw new Exception("該密碼不存在!");
}
String strRemoteAddr=FuncUtil.getRemoteIpdz(request); // 獲取ip地址
System.out.println("strRemoteAddr:"+strRemoteAddr);
sysuserManager.validateSysuser(command,userSession,strRemoteAddr);//校驗用戶名、密碼,有問題會直接throw異常
sysuser=this.sysuserManager.getSysuserByUsername(name);//獲取用戶信息
if(sysuser==null){
throw new Exception(Constants.SYS_NO_RIGHT);
}
sysuser.setIp(strRemoteAddr);// 設置登錄人員的ip地址
sysuser.setDlms(command.getDlms());
department=this.departmentManager.getDepartment(sysuser.getGlbm());
if(department==null)
throw new Exception(Constants.SYS_NO_HIGHXZQH);
UUID token = UUID.randomUUID();
userSession.setSysuser(sysuser);//sysuser
userSession.setDepartment(department);
userSession.setYhdh(sysuser.getYhdh());
userSession.setGlbm(department.getGlbm());
userSession.setScdlsj(new Date().getTime()+"");
RestfulSession.setSession(token.toString(), userSession);
Map<String, Object> resultMap = new HashMap<String, Object>();
resultMap.put("resultId", "00");
resultMap.put("user", sysuser);
resultMap.put("token", token.toString());
resStr = JSON.toJSONString(resultMap);//返回toekn和用戶信息
}catch(Exception e){
Map<String, Object> resultMap = new HashMap<String, Object>();
resultMap.put("resultId", "01");
resultMap.put("resultMsg", e.getMessage());
resStr = JSON.toJSONString(resultMap);//返回異常
}
return resStr;
}
RestfulSession,這個類中的靜態參數主要就是用來緩存正在登錄中的用戶的token與對應的session
package com.tmri.framework.bean;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import com.tmri.share.frm.bean.UserSession;
public class RestfulSession{
private static Map<String,UserSession> session=new HashMap<String,UserSession>(10000);//最多放一萬個用戶信息
private static Map<String,String> clients=new HashMap<String,String>();
public static UserSession getSession(String sessionId){
return session.get(sessionId);
}
public static void setSession(String sessionId,UserSession clientUser){
String yhdh=clientUser.getYhdh();
String token=clients.get(yhdh);
if(StringUtils.isNotEmpty(token)){
session.remove(token);
}
session.put(sessionId,clientUser);
clients.put(clientUser.getYhdh(),sessionId);
}
public static void renewal(String sessionId){
session.get(sessionId).setScdlsj(new Date().getTime()+“”);
}
}
二、token的調用
登錄後每次向後臺的請求都要帶上這個token值。如果移動端規定時間(暫時定義爲半小時)內沒有向後臺發起請求,該token作廢,後臺返回token超時的提示。說到這裏,明眼人也看出來了,其實這個機制也是模仿J2EE的session機制。
以下是Android端登錄後向後臺發起訪問的代碼,一定要帶着token:
String creatUrl = "http://"+ip+":"+port+"/test/be/test.do?method=mytest"+"&token="+token;
OkHttpClientManager.ResultCallback cb = new OkHttpClientManager.ResultCallback<String>() {
@Override
public void onError(Request request, Exception e) {
ToastUtil.showToastMsgError(TestActivity.this, "迴應服務器失敗");
}
@Override
public void onResponse(String response) {
JSONObject returnJson = JSONObject.parseObject(response);
String resultId = returnJson.get("resultId").toString();
String resultMsg = "";
if(resultId.equals("00")){
resultMsg = "提交結果成功";
ToastUtil.showToastMsgError(TestActivity.this, resultMsg);
}
else if(resultId.equals("98")) // 會話過期
{
relogin("當前連接已過期,是否重新登陸?");
}else{
//tasklist.clear();
resultMsg = "提交結果失敗:"+returnJson.get("resultMsg").toString();
ToastUtil.showToastMsgError(TestActivity.this, resultMsg);
}
}
};
OkHttpClientManager.getInstance().getAsyn(creatUrl,cb);
訪問的時候除了加入白名單的url,其他請求都要經過攔截器的過濾,攔截器要判斷這次的請求是移動端訪問(帶token的),還是一般的網頁訪問,有報錯的話返回的東西也不一樣,移動端訪問報錯的話返回的是定義好格式的json字符串,而網頁訪問的話返回的一般是報錯頁面(其實,如果web端用前端框架的話,也可以是定義好格式的json字符串)。
以下是後臺的攔截器中的代碼:
Long timeout = (long) 1800000;//超時時間,30min
UserSession userSession;
String token = request.getParameter("token");
if(StrUtil.checkBN(token)){
// 有token,接口訪問
boolean flag=false;
try{
userSession=RestfulSession.getSession(token);
if(userSession!=null){
String currentURL=request.getServletPath();
if(!userSession.getMenuList().contains(currentURL+"?method="+request.getParameter("method"))){
sendRedirectJSON(request,response,"該用戶沒有權限");
return false;
}
Calendar now=Calendar.getInstance();
if(now.getTime().getTime()-Long.parseLong(userSession.getScdlsj())<timeout){// 如果該用戶最後訪問時間在30分鐘以內
flag=true;// 允許訪問
RestfulSession.renewal(token);//重置該次登錄的session計時
}else{
sendRedirectJSON(request,response,"已超時");
return false;
}
}
}catch(Exception e){
sendRedirectJSON(request,response,"請重新登錄");
}
if(!flag){
sendRedirectJSON(request,response,"未登錄");
return false;
}else{
return true;
}
}else{
//正常網頁訪問
userSession=(UserSession)request.getSession().getAttribute("userSession");
if(userSession==null){
sendRedirect(request,response);
return false;
}
String url="";
if(StrUtil.checkBN(request.getParameter("method"))){
url=handler.getClass().getName().substring(handler.getClass().getName().lastIndexOf(".")+1,handler.getClass().getName().length())+"."+request.getParameter("method");
}else{
url=handler.getClass().getName().substring(handler.getClass().getName().lastIndexOf(".")+1,handler.getClass().getName().length());
}
HashMap<String,String> rights=userSession.getRights();
if(rights==null){//沒有權限
sendRedirect(request,response);
return false;
}else{
if(rights.containsKey(url)){
return true;
}else{//沒有權限
sendRedirect(request,response);
return false;
}
}
}
三、其他
寫這篇blog的時候特意去搜了一下相關類似的文章,希望有所借鑑學習。
其中有以下的一篇,他採用了redis來緩存token的信息,這個在短時間登錄用戶數量達到一定規模的時候應該是個不錯的方案。
redis緩存token信息