如何保证API接口安全?( 二 )


具体的实践,也可以分两种:

  • 第一种:采用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";//有效期默认为 2hourpublic static final Long EXPIRATION_TIME = 1000L*60*60*2;/*** 创建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 = https://www.isolves.com/it/cxkf/bk/2022-01-24/"/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 {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 从http请求头中取出tokenfinal 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;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//方法结束后,移除缓存的tokenWebContextUtil.removeUserToken();}}在生成token的时候,我们可以将一些基本的用户信息,例如用户ID、用户姓名,存入token中,这样当token鉴权通过之后,我们只需要通过解析里面的信息,即可获取对应的用户ID,可以省下去数据库查询一些基本信息的操作 。
【如何保证API接口安全?】同时,使用的过程中,尽量不要存放敏感信息,因为很容易被黑客解析!
2.2、接口签名同样的思路,站在服务端验证的角度,我们可以先编写一个签名拦截器,验证客户端传入的参数是否合法,只要有一项不合法,就提示错误 。
具体代码实践如下:
public class SignInterceptor implements HandlerInterceptor {@Autowiredprivate AppSecretService appSecretService;@Autowiredprivate RedisUtil redisUtil;@Overridepublic 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, 5*60);request.setAttribute("reidsKey",key);}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {//请求处理完毕之后,移除缓存String value = https://www.isolves.com/it/cxkf/bk/2022-01-24/request.getAttribute("reidsKey");if(!StringUtils.isEmpty(value)){redisUtil.remove(value);}}}


推荐阅读