前段时间项目中写了一个调用银行服务的webservice,之前的调用方式都是直连服务没有安全校验,领导让加一个IP白名单校验,苦思冥想后决定使用Filter来过滤请求方的IP地址,符合要求的放行,否则拦截;实现过程中发现如何拦截这个请求终止访问服务成了难题,于是乎尝试抛出异常。度娘一番,几经折腾整理出以下两种方法。
BaseException.java
package com.chinachg.tbsp.utils.exception.base;
/**
*
* Description:
*
* @author Christy
* @date 2018年12月31日 下午5:09:20
*/
public class BaseException extends RuntimeException {
private static final long serialVersionUID = 7376631700004310801L;
protected int code;
public BaseException() {
}
public BaseException(String message) {
super(message);
}
public BaseException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
IpNotAllowedException.java
package com.chinachg.tbsp.utils.exception;
import com.chinachg.tbsp.utils.exception.base.BaseException;
/**
*
* Description:
*
* @author Christy
* @date 2019年1月28日 下午2:02:40
*/
public class IpNotAllowedException extends BaseException {
private static final long serialVersionUID = 2316147387642584340L;
public IpNotAllowedException() {
}
public IpNotAllowedException(String message) {
super(message);
}
public IpNotAllowedException(int code, String message) {
super(code, message);
}
}
GlobalExceptionHandler.java
package com.chinachg.tbsp.utils.exception.handler;
import com.chinachg.tbsp.utils.ExceptionUtil;
import com.chinachg.tbsp.utils.exception.*;
import com.chinachg.tbsp.utils.values.ResultInfo;
import com.chinachg.tbsp.utils.values.ResultType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理 IpNotAllowedException 异常
*/
@ExceptionHandler(IpNotAllowedException.class)
@ResponseBody
public Map<String, Object> handleException(HttpServletRequest request, IpNotAllowedException e) {
log.error("开始捕获异常:");
log.error("异常名称:" + e.toString());
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("异常详情:");
stringBuilder.append(System.getProperty("line.separator"));
stringBuilder.append(ExceptionUtil.getStackTraceString(e));
log.error(stringBuilder.toString());
return ResultInfo.getDataMap(e.getCode(), e.getMessage(), null);
}
}
以上三步工作做完后,如果我们在业务中抛出相应的异常(IpNotAllowedException)时就会被全局的异常处理中捕获
package com.chinachg.tbsp.utils;
import javax.servlet.http.HttpServletRequest;
/**
* @Author Christy
* @DESC
* @Date 2020/12/9 9:52
**/
public class IpUtil {
/**
* 获取用户真实IP地址,不使用request.getRemoteAddr()的原因是有可能用户使用了代理软件方式避免真实IP地址,
* 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值
*
* @return ip
*/
public static String getRealIP(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if( ip.indexOf(",")!=-1 ){
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
System.out.println("Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
System.out.println("WL-Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
System.out.println("HTTP_CLIENT_IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
System.out.println("HTTP_X_FORWARDED_FOR ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
System.out.println("X-Real-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
System.out.println("getRemoteAddr ip: " + ip);
}
return ip;
}
}
万事俱备只欠东风,主角姗姗来迟,开始我们的第一种解决方案,上Filter
package com.chinachg.tbsp.filters;
import com.chinachg.tbsp.utils.IpUtil;
import com.chinachg.tbsp.utils.aware.CustomerAware;
import com.chinachg.tbsp.utils.exception.IpNotAllowedException;
import com.chinachg.tbsp.utils.redis.RedisUtil;
import com.chinachg.tbsp.utils.values.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerExceptionResolver;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* @Author Christy
* @DESC
* @Date 2020/12/9 16:56
**/
@WebFilter(urlPatterns = "/*", filterName = "ipNotAllowedFilter")
@Slf4j
public class IpFilter implements Filter {
private RedisUtil redisUtil;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
redisUtil = CustomerAware.getBean(RedisUtil.class);
//先获取相关需要验证的ip列表
//过滤ip,若用户在白名单内,则放行
String ipAddress = IpUtil.getRealIP(request);
//所用需要验证的ip,暂时批量验证
List<Object> objectList = redisUtil.lGetAll(Constants.REDIS_IP_ADDRESS_LIST_KEY);
if (!CollectionUtils.isEmpty(objectList)){
List<String> ipAddressList = (List<String>)(List)objectList.get(0);
if(!ipAddressList.contains(ipAddress)){
log.error("{}禁止访问",ipAddress);
// 这种直接抛出异常的方式无法在全局异常处理中捕获
throw new IpNotAllowedException(403,"当前IP无权访问");
}
}
filterChain.doFilter(request, response);
}
}
上面说了,直接抛出异常的方式无法在全局异常中捕获,这时候访问如果ip不在白名单中,访问服务会直接报500
错误
这种方式不仅不友好,而且状态码也不对,一般情况下拒绝访问的状态码是403
,这里就需要手动处理一下。
要想实现自定义的异常并返回友好的数据格式,这里就要用到Springboot内置的对异常进行统一处理的Controller–BasicErrorController
,我们自定义一个ErrorController继承这个Controller
package com.chinachg.tbsp.controller;
import com.alibaba.fastjson.JSONObject;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @Author Christy
* @DESC
* @Date 2020/12/10 9:19
**/
@RestController
public class ErrorController extends BasicErrorController {
public ErrorController() {
super(new DefaultErrorAttributes(), new ErrorProperties());
}
@Override
@RequestMapping(produces = {
MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
JSONObject result = new JSONObject();
result.put("code", 403);
result.put("msg",body.get("message"));
return new ResponseEntity(result.toString(), status);
}
}
这种方式返回的结果看似很完美了,但是通过上述代码我们可以发现这个结果是JSON字符串,跟我们系统设置的统一返回格式不一致
@GetMapping("/not_allowed")
public Map<String,Object> notAllowed(){
return ResultInfo.getDataMap(ResultType.FAIL.getCode(), "当前ip无权访问", null);
}
要想与之前设置的返回结果格式保持一致,就需要引入第二种解决方案,直接在Filter中抛出能够被全局异常捕捉到的异常
package com.chinachg.tbsp.filters;
import com.chinachg.tbsp.utils.IpUtil;
import com.chinachg.tbsp.utils.aware.CustomerAware;
import com.chinachg.tbsp.utils.exception.IpException;
import com.chinachg.tbsp.utils.exception.IpNotAllowedException;
import com.chinachg.tbsp.utils.redis.RedisUtil;
import com.chinachg.tbsp.utils.values.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerExceptionResolver;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* @Author Christy
* @DESC
* @Date 2020/12/9 16:56
**/
@WebFilter(urlPatterns = "/*", filterName = "ipNotAllowedFilter")
@Slf4j
public class IpFilter implements Filter {
private RedisUtil redisUtil;
/** 在Filter中注入HandlerExceptionResolver **/
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
redisUtil = CustomerAware.getBean(RedisUtil.class);
//先获取相关需要验证的ip列表
//过滤ip,若用户在白名单内,则放行
String ipAddress = IpUtil.getRealIP(request);
//所用需要验证的ip,暂时批量验证
List<Object> objectList = redisUtil.lGetAll(Constants.REDIS_IP_ADDRESS_LIST_KEY);
if (!CollectionUtils.isEmpty(objectList)){
List<String> ipAddressList = (List<String>)(List)objectList.get(0);
if(!ipAddressList.contains(ipAddress)){
log.error("{}禁止访问",ipAddress);
/** 通过HandlerExceptionResolver抛出可被全局异常处理捕获到的异常 **/
resolver.resolveException(request, response, null, new IpNotAllowedException(403,"当前IP无权访问"));
return;
}
}
filterChain.doFilter(request, response);
}
}
此篇完结!!!