巨大的建築,總是由一木一石疊起來的,我們何妨做做這一木一石呢?我時常做些零碎事,就是爲此。
這是對的,但是我沒有說過這句話! —— 魯迅
問題描述
通過Api爲第三方公司提供接口服務是一項很common的需求,其中一種實現方式就是利用restful的形式,提供http接口服務.
提供出接口之後,誰都可以來訪問,對系統本身是有一定危險性的,所以我們要做一下驗證信息,來確認對方的身份.
對於自己內部的接口,比如用於提供給mask(前臺頁面)訪問的api,可以做登錄驗證,只有登錄了,纔可以訪問。
但是對於第三方公司而言,不是我們的用戶,屬於合作伙伴,性質不一樣.
我們需要給他們提供10個接口,這10個接口是需要放開權限的,不然直接訪問的話,會直接提示:*未登錄* 的錯誤。
權限放開了,那麼權限如何驗證呢.
權限驗證
不增加任何驗證,直接可以訪問
安全性差,比如推送數據的接口(一般數據是有格式的,他要是自己猜確實不好猜到),假如第三方公司在正常使用的時候,請求數據推送,
被壞人截取到了相應的信息。通過分析,他就可以一直推送,知道把你的數據庫填滿.
接口的驗證
- 客戶端訪問接口,需要佩戴token,假如這個token正好是我們產生的,則驗證通過,token有時效限制。
- 這樣一來,需要提供一個接口,便是產生token的接口,而這個接口是開放的,誰都可以訪問。
- 那麼這時候,假如一個壞人想要惡意訪問的話,他首先需要知道這個產生token的接口是什麼,其次需要知道自己要訪問的接口是什麼,然後再訪問
安全性比開始的已經大一些了。此時假如他截取到了某次請求的數據,再請求的話,就只能在實效內管用了.
對token產生的接口進行驗證處理
上面是對token產生的接口可以隨意訪問,一旦被人知道了這個接口,則可以隨意獲取token值了,加一個判斷的話,比如需要用戶名,和密碼,則可以進行一種驗證。
簡單的寫法,就是對不同的第三方協議公司定一個用戶名,密碼,直接傳過來.
除了驗證身份之外,加上授權處理.
這種情況就跟咱們的登錄差不多了,但是身份不一樣。
一般登錄的設計裏面,都會有角色的設計,身份不一致的話,可以新增一種角色,這種角色就給服務公司提供一種控制,這樣就類似於登錄了.
但是一般對接公司需要的接口都是固定的,假如需要根據功能收費的話,授權是需要考慮的.
以下主要說一下接口的簡單驗證.
接口的驗證
提供產生token接口
比如直接返回UUID
@RequestMapping(value = "/", method = RequestMethod.POST)
@ResponseBody
public Result<String, State> getToken(HttpServletRequest request, HttpServletResponse response) {
Result<String, State> result = new Result<>();
UUID uuid = UUID.randomUUID();
String token = uuid.toString();
//放到redis裏面,實效爲30分鐘.
RedisCacheUtil.setex(BUSSINESSKEY, token, token, 30 * 60);
result.setCode(State.SUCCESS);
result.setData(token);
return result;
}
這樣,就把生成的token返回了,那麼接口的寫法如下:
@RequestMapping(value = "/", method = RequestMethod.GET)
@ResponseBody
public Result<String, State> getSomeThing(@RequestParam String token,
String param) {
Result<String, State> result = new Result<>();
String tokenRedis = RedisCacheUtil.get(BUSSINESSKEY, token, String.class);
if (token.equals(tokenRedis) {
result.setCode(State.SUCCESS);
} else {
result.setCode(State.FAILED);
}
return result;
}
關於redis裏key值的討論
開始我打算用請求方的ip,假如是ip的話,那麼同一個ip則會用同一個token,不同的ip用不同的token,假如你
知道了別人的token,不是同一個ip,也照樣不能訪問。
現實發現問題,是因爲我們的接口是寫在api服務裏面,而api服務是一個內網服務,第三方(App)是通過請求我們的nginx服務轉發到api裏面。
這樣的話,每次我獲取到的ip,都是我們自己服務器的內網地址,並不能用.
後來發現用token作爲key值也可以,這樣每個用戶,就擁有自己的token了,但是問題是隨便一個人一旦獲取到了別人的token,在token相應未失效的時候,也是可以用的.
用攔截器來處理token的驗證。
按照上面的方式寫接口,假如提供10個接口的話,則在每個接口中都需要接收token參數.不好維護。
配置攔截器
<mvc:interceptors>
<!--跨域過濾器-->
<bean class="com.enn.zhwl.interceptor.CrossInterceptor"/>
<mvc:interceptor>
<mvc:mapping path="/hdd/out/**" />
<mvc:exclude-mapping path="/hdd/out/token/**" />
<bean class="com.enn.zhwl.interceptor.HddInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
- springMVC框架中,在servlet.xml中加入攔截器的配置
- mvc:mapping 是改攔截器對哪些類生效
- mvc:exclude-mapping 是將上面生效的文件中剔除一些類
- bean 則是攔截器的實現類.
攔截器實現
public class HddInterceptor implements HandlerInterceptor {
private static final String BUSSINESSKEY = "HDDTOKEN";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getParameter("token");
String tokenRedis = RedisCacheUtil.get(BUSSINESSKEY, token, String.class);
if (token.equals(tokenRedis) {
return true;
} else {
PrintWriter writer = response.getWriter();
writer.write("token error");
writer.close();
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
token作爲參數傳遞的問題
這樣是讓客戶端將token值放到請求的參數中,來進行接收。
現在有一種情況,就是一般接口提供有兩種方式。
- 接收普通變量,比如String 等類型的值
- 接收對象類型(@RequestBody)。
接收普通變量和對象類型時,發送的ajax請求的 contentType 是不一樣的。
發送對象數據的時候,contentType='application/json',對於這種情況,直接用 request.getParameter 就獲取不到了.
token放到cookie中進行傳遞
一般登錄驗證的時候,token就是放到cookie中的.放到cookie中可以解決上面的問題。
而且還方便,客戶端完全感知不到token的存在,也不用在調用方法的時候,顯示傳遞.
修改設置token的方法
將token放到cookie中而不是放到data中.
public Result<String, State> getToken(HttpServletRequest request, HttpServletResponse response) {
Result<String, State> result = new Result<>();
UUID uuid = UUID.randomUUID();
String token = uuid.toString();
//放到redis裏面,實效爲30分鐘.
RedisCacheUtil.setex(BUSSINESSKEY, token, token, 30 * 60);
//放到cookie中而不是放到data中.
Cookie cookie = new Cookie("hddtoken", token);
cookie.setPath("/");
response.addCookie(cookie);
result.setCode(State.SUCCESS);
return result;
}
修改攔截器的實現
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//String token = request.getParameter("token");
Cookie[] cookies = request.getCookies();
Optional<Cookie> cookieToken = Arrays.stream(cookies).filter(cookie -> cookie.getName().equals("hddtoken")).findFirst();
//這裏需要判斷token是否存在,就不寫了
String token = cookieToken.get().getValue();
String tokenRedis = RedisCacheUtil.get(BUSSINESSKEY, token, String.class);
if (token.equals(tokenRedis) {
return true;
} else {
PrintWriter writer = response.getWriter();
writer.write("token error");
writer.close();
return false;
}
}
這樣客戶端也不用傳了,接收端也不用接收了,一切都默默的執行,只需要在請求之前,先調用一下獲取token的接口就行了.
這裏是寫了一些簡單驗證是思路與遇到的問題,至於加密啊,密碼啊,授權啊等便是另外的事了.