当你的公司体量上来了时候,这个时候可能有一些公司开始找你进行技能对接了,转变成由你来供应api接口,那这个时候,我们该当如何设计并担保API接口安全呢?
二、方案先容最常用的方案,紧张有两种:
token方案接口署名2.1、token方案个中 token 方案,是一种在web端利用最广的接口鉴权方案,我记得在之前写过一篇《手把手教你,利用JWT实现单点登录》的文章,里面先容的比较详细,有兴趣的朋友可以看一下,没理解的也没紧要,我们在此大略的先容一下 token 方案。
从上图,我们可以很清晰的看到,token 方案的实现紧张有以下几个步骤:
1、用户登录成功之后,做事端会给用户天生一个唯一有效的凭据,这个有效值被称为token2、当用户每次要求其他的业务接口时,须要在要求头部带上token3、做事端接管到客户端业务接口要求时,会验证token的合法性,如果不合法会提示给客户端;如果合法,才会进入业务处理流程。在实际利用过程中,当用户登录成功之后,天生的token存放在redis中时是有时效的,一样平常设置为2个小时,过了2个小时之后会自动失落效,这个时候我们就须要重新登录,然后再次获取有效token。
token方案,是目前业务类型的项目当中利用最广的方案,而且实用性非常高,可以很有效的防止黑客们进行抓包、爬取数据。
但是 token 方案也有一些缺陷!
最明显的便是与第三方公司进行接口对接的时候,当你的接口要求量非常大,这个时候 token 溘然失落效了,会有大量的接口要求失落败。
这个我深有体会,我记得在很早的时候,跟一家中、大型互联网公司进行联调的时候,他们供应给我的接口对接方案便是token方案,当时我司的流量高峰期时候,要求他们的接口大量报错,缘故原由便是由于token失落效了,当token失落效时,我们会调用他们刷新token接口,刷新完成之后,在token失落效与重新刷新token这个韶光间隔期间,就会涌现大量的要求失落败的日志,因此在实际API对接过程中,我不推举大家采取 token方案。
2.2、接口署名接口署名,顾名思义,便是通过一些署名规则对参数进行署名,然后把署名的信息放入要求头部,做事端收到客户端要求之后,同样的只须要按照已定的规则生产对应的署名串与客户真个署名信息进行比拟,如果同等,就进入业务处理流程;如果不通过,就提示署名验证失落败。
在接口署名方案中,紧张有四个核心参数:
1、appid表示运用ID,个中与之匹配的还有appsecret,表示运用密钥,用于数据的署名加密,不同的对接项目分配不同的appid和appsecret,担保数据安全2、timestamp 表示韶光戳,当要求的韶光戳与做事器中的韶光戳,差值在5分钟之内,属于有效要求,不在此范围内,属于无效要求3、nonce 表示临时流水号,用于防止重复提交验证4、signature 表示署名字段,用于判断接口要求是否有效。个中署名的生成规则,分两个步骤:
第一步:对要求参数进行一次md5加密署名//步骤一String 参数1 = 要求办法 + 要求URL相对地址 + 要求Body字符串;String 参数1加密结果= md5(参数1)
第二步:对第一步署名结果,再进行一次md5加密署名
//步骤二String 参数2 = appsecret + timestamp + nonce + 参数1加密结果;String 参数2加密结果= md5(参数2)
参数2加密结果,便是我们要的终极署名串。
接口署名方案,尤其是在接口要求量很大的情形下,依然很稳定。
换句话说,你可以将接口署名看作成对token方案的一种补充。
但是如果想把接口署名方案,推广到前后端对接,答案是:不适宜。
由于署名打算非常繁芜,其次,便是随意马虎泄露appsecret!
说了这么多,下面我们就一起来用程序实践一下吧!
就像上文所说,token方案重点在于,当用户登录成功之后,我们只须要天生好对应的token,然后将其返回给前端,不才次要求业务接口的时候,须要把token带上。
详细的实践,也可以分两种:
第一种:采取uuid天生token,然后将token存放在redis中,同时设置有效期2哥小时第二种:采取JWT工具来天生token,这种token是可以跨平台的,天然支持分布式,实在实质也是采取韶光戳+密钥,来天生一个token。下面,我们先容的是第二种实现办法。
首先,编写一个jwt 工具。
public class JwtTokenUtil { //定义token返转头部 public static final String AUTH_HEADER_KEY = "Authorization"; //token前缀 public static final String TOKEN_PREFIX = "Bearer "; //署名密钥 public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x"; //有效期默认为 2hour public static final Long EXPIRATION_TIME = 1000L60602; / 创建TOKEN @param content @return / public static String createToken(String content){ return TOKEN_PREFIX + JWT.create() .withSubject(content) .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .sign(Algorithm.HMAC512(KEY)); } / 验证token @param token / public static String verifyToken(String token) throws Exception { try { return JWT.require(Algorithm.HMAC512(KEY)) .build() .verify(token.replace(TOKEN_PREFIX, "")) .getSubject(); } catch (TokenExpiredException e){ throw new Exception("token已失落效,请重新登录",e); } catch (JWTVerificationException e) { throw new Exception("token验证失落败!
",e); } }}
接着,我们在登录的时候,天生一个token,然后返回给客户端。
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){ //...参数合法性验证 //从数据库获取用户信息 User dbUser = userService.selectByUserNo(userDto.getUserNo); //....用户、密码验证 //创建token,并将token放在相应头 UserToken userToken = new UserToken(); BeanUtils.copyProperties(dbUser,userToken); String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken)); response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token); //定义返回结果 UserVo result = new UserVo(); BeanUtils.copyProperties(dbUser,result); return result;}
末了,编写一个统一拦截器,用于验证客户端传入的token是否有效。
@Slf4jpublic class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从http要求头中取出token final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY); //如果不是映射到方法,直接通过 if(!(handler instanceof HandlerMethod)){ return true; } //如果是方法探测,直接通过 if (HttpMethod.OPTIONS.equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return true; } //如果方法有JwtIgnore表明,直接通过 HandlerMethod handlerMethod = (HandlerMethod) handler; Method method=handlerMethod.getMethod(); if (method.isAnnotationPresent(JwtIgnore.class)) { JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class); if(jwtIgnore.value()){ return true; } } LocalAssert.isStringEmpty(token, "token为空,鉴权失落败!
"); //验证,并获取token内部信息 String userToken = JwtTokenUtil.verifyToken(token); //将token放入本地缓存 WebContextUtil.setUserToken(userToken); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //方法结束后,移除缓存的token WebContextUtil.removeUserToken(); }}
在天生token的时候,我们可以将一些基本的用户信息,例如用户ID、用户姓名,存入token中,这样当token鉴权通过之后,我们只须要通过解析里面的信息,即可获取对应的用户ID,可以省下去数据库查询一些基本信息的操作。
同时,利用的过程中,只管即便不要存放敏感信息,由于很随意马虎被黑客解析!
同样的思路,站在做事端验证的角度,我们可以先编写一个署名拦截器,验证客户端传入的参数是否合法,只要有一项不合法,就提示缺点。
详细代码实践如下:
public class SignInterceptor implements HandlerInterceptor { @Autowired private AppSecretService appSecretService; @Autowired private RedisUtil redisUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //appId验证 final String appId = request.getHeader("appid"); if(StringUtils.isEmpty(appId)){ throw new CommonException("appid不能为空"); } String appSecret = appSecretService.getAppSecretByAppId(appId); if(StringUtils.isEmpty(appSecret)){ throw new CommonException("appid不合法"); } //韶光戳验证 final String timestamp = request.getHeader("timestamp"); if(StringUtils.isEmpty(timestamp)){ throw new CommonException("timestamp不能为空"); } //大于5分钟,造孽要求 long diff = System.currentTimeMillis() - Long.parseLong(timestamp); if(Math.abs(diff) > 1000 60 5){ throw new CommonException("timestamp已过期"); } //临时流水号,防止重复提交 final String nonce = request.getHeader("nonce"); if(StringUtils.isEmpty(nonce)){ throw new CommonException("nonce不能为空"); } //验证署名 final String signature = request.getHeader("signature"); if(StringUtils.isEmpty(nonce)){ throw new CommonException("signature不能为空"); } final String method = request.getMethod(); final String url = request.getRequestURI(); final String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8")); String signResult = SignUtil.getSignature(method, url, body, timestamp, nonce, appSecret); if(!signature.equals(signResult)){ throw new CommonException("署名验证失落败"); } //检讨是否重复要求 String key = appId + "_" + timestamp + "_" + nonce; if(redisUtil.exist(key)){ throw new CommonException("当前要求正在处理,请不要重复提交"); } //设置5分钟 redisUtil.save(key, signResult, 560); request.setAttribute("reidsKey",key); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //要求处理完毕之后,移除缓存 String value = request.getAttribute("reidsKey"); if(!StringUtils.isEmpty(value)){ redisUtil.remove(value); } }}
署名工具类SignUtil:
public class SignUtil { / 署名打算 @param method @param url @param body @param timestamp @param nonce @param appSecret @return / public static String getSignature(String method, String url, String body, String timestamp, String nonce, String appSecret){ //第一层署名 String requestStr1 = method + url + body + appSecret; String signResult1 = DigestUtils.md5Hex(requestStr1); //第二层署名 String requestStr2 = appSecret + timestamp + nonce + signResult1; String signResult2 = DigestUtils.md5Hex(requestStr2); return signResult2; }}
署名打算,可以换成hamc办法进行打算,思路大致一样。
三、小结上面先容的token和接口署名方案,对外都可以对供应的接口起到保护浸染,防止别人修改要求,或者仿照要求。
但是短缺对数据自身的安全保护,即要求的参数和返回的数据都是有可能被别人拦截获取的,而这些数据又是明文的,以是只要被拦截,就能得到相应的业务数据。
对付这种情形,推举大家对要求参数和返回参数进行加密处理,例如RSA、AES等加密工具。
同时,在生产环境,采取https办法进行传输,可以起到很好的安全保护浸染!