巨大的建筑, 总是由一木一石叠起来的, 我们何妨做做这一木一石呢? 我时常做些零碎事, 就是为此.
这是对的, 但是我没有说过这句话! -- 鲁迅
问题描述
通过 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 的接口就行了.
这里是写了一些简单验证是思路与遇到的问题, 至于加密啊, 密码啊, 授权啊等便是另外的事了.
来源: http://www.jianshu.com/p/bfa938e5e7b1