為合作公司提供介面的許可權驗證問題 攔截器 cookie
巨大的建築,總是由一木一石疊起來的,我們何妨做做這一木一石呢?我時常做些零碎事,就是為此。
這是對的,但是我沒有說過這句話!—— 魯迅
問題描述
通過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的介面就行了.
這裡是寫了一些簡單驗證是思路與遇到的問題,至於加密啊,密碼啊,授權啊等便是另外的事了.