1、 服务端记录404的错误
使用拦截器,在请求返回时进行拦截
/**
* 在调用controller具体方法后拦截
*/
@Autowired
private ExceptionLogServicelogService;
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView modelAndView)throws Exception {
int status = response.getStatus();
if (status ==404 && !request.getRequestURI().contains("error")) {
ExceptionLog exceptionLog =new ExceptionLog();
//设置404异常信息
long happenTime = System.currentTimeMillis();
exceptionLog.setExcId(UUID.randomUUID().toString());
exceptionLog.setExcName("404");
exceptionLog.setOperUri(request.getRequestURI());
exceptionLog.setOperCreateTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
log.info("当前的异常是:404");
logService.insert(exceptionLog);
}
}
2、 服务器记录Jackson解析的非法字符的错误。如用户输入的Json信息是
http://localhost:8083/jeecg/user/testValidate
{
"fileid": “”,
"md5": ""
}在服务端记录, 用户输入的fileid中包含非法字符
这个过程就遇到了一个问题:ServletRequest的getReader()和getInputStream()两个方法只能被调用一次,而且不能两个都调用。那么如果Filter中调用了一次,在Controller里面就不能再调用了。HttpServletRequestWrapper把request保存下来,然后通过过滤器把保存下来的request再填充进去,这样就可以多次读取request了。
public class RequestWrapper extends HttpServletRequestWrapper {
private final String body;
public RequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
} finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
body = stringBuilder.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
public String getBody() {
return this.body;
}
}
package com.hyd.zcar.cms.Interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Slf4j
@WebFilter(urlPatterns = "/*", filterName = "channelFilter")
public class ChannelFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
//取Body数据
RequestWrapper requestWrapper = new RequestWrapper(request);
String body = requestWrapper.getBody();
filterChain.doFilter(requestWrapper != null ? requestWrapper : request, servletResponse);
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info(body);
}
@Override
public void destroy() {
}
}
3、 所有的用户请求,请求参数等都在服务器被记录
4、 记录的时候,采用2种方式,一种是JPA到本地数据库,一种是Log日志
解决方法:通过aop来对标注了OperLog的注解进行切入,在后置通知中进行用户的操作记录,分别记录到数据库跟本地;当发生异常时,有异常通知来进行异常的记录。
@Autowired
private OperationLogService operationLogService;
@Autowired
private ExceptionLogService exceptionLogService;
/**
* 设置操作日志切入点 记录操作日志 在注解的位置切入代码
*/
@Pointcut("@annotation(com.hyd.zcar.cms.utils.annotation.OperLog)")
public void operLogPoinCut() {
}
/**
* 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
*
* @param joinPoint 切入点
* @param keys 返回结果
*/
@AfterReturning(value = "operLogPoinCut()", returning = "keys")
public void saveOperLog(JoinPoint joinPoint, Object keys) {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
OperationLog operlog = new OperationLog();
try {
operlog.setOperId(UUID.randomUUID().toString()); // 主键ID
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取操作
OperLog opLog = method.getAnnotation(OperLog.class);
if (opLog != null) {
String operModul = opLog.operModul();
String operType = opLog.operType();
String operDesc = opLog.operDesc();
operlog.setOperModul(operModul); // 操作模块
operlog.setOperType(operType); // 操作类型
operlog.setOperDesc(operDesc); // 操作描述
}
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
methodName = className + "." + methodName;
operlog.setOperMethod(methodName); // 请求方法
// 请求的参数
Map<String, String> rtnMap = converMap(request.getParameterMap());
// 将参数所在的数组转换成json
String params = JSON.toJSONString(rtnMap);
operlog.setOperRequParam(params); // 请求参数
operlog.setOperRespParam(JSON.toJSONString(keys)); // 返回结果
operlog.setOperUri(request.getRequestURI()); // 请求URI
operationLogService.insert(operlog);
log.info("当前的记录是:"+methodName+params);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行
*
* @param joinPoint 切入点
* @param e 异常信息
*/
@AfterThrowing(pointcut = "operExceptionLogPoinCut()", throwing = "e")
public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
ExceptionLog excepLog = new ExceptionLog();
try {
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
excepLog.setExcId(UUID.randomUUID().toString());
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
methodName = className + "." + methodName;
// 请求的参数
Map<String, String> rtnMap = converMap(request.getParameterMap());
// 将参数所在的数组转换成json
String params = JSON.toJSONString(rtnMap);
excepLog.setExcRequParam(params); // 请求参数
excepLog.setOperMethod(methodName); // 请求方法名
excepLog.setExcName(e.getClass().getName()); // 异常名称
excepLog.setExcMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace())); // 异常信息
excepLog.setOperUri(request.getRequestURI()); // 操作URI
exceptionLogService.insert(excepLog);
log.info("当前的异常是:"+e.getClass().getName()+excepLog.getExcMessage());
} catch (Exception e2) {
e2.printStackTrace();
}
}
结果如下图所示:
操作:
异常:
5、 记录返回的信息,采用国际化的返回方式,设置示例,中文及英文
后端接收到一个请求后,需要返回一个提示信息,而此时我们可以使这个返回信息支持国际化,这里就用到了org.springframework.context.MessageSource接口,MessageSource提供了三个方法
@Nullable//参数字段可为空
String getMessage(String var1, @Nullable Object[] var2, @Nullable String var3, Locale var4);
String getMessage(String var1, @Nullable Object[] var2, Locale var3) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable var1, Locale var2) throws NoSuchMessageException;
String getMessage(String var1, @Nullable Object[] var2, @Nullable String var3, Locale var4):用来从MessageSource获取消息的基本方法。如果在指定的locale中没有找到消息,则使用默认的消息。var2中的参数将使用标准类库中的MessageFormat来作消息中替换值。
String getMessage(String code, Object[] args, Locale loc):本质上和上一个方法相同,其区别在:没有指定默认值,如果没找到消息,会抛出一个NoSuchMessageException异常。
String getMessage(MessageSourceResolvable resolvable, Locale locale):上面方法中所使用的属性都封装到一个MessageSourceResolvable实现中,而本方法可以指定MessageSourceResolvable实现。
这里首先我们需要获取到当前请求的Locale,有两种方法:
Locale locale = LocaleContextHolder.getLocale();
Locale locale = RequestContextUtils.getLocale(request);
SpringBoot自动配置好了管理国际化资源文件的组件;
@ConfigurationProperties(prefix = "spring.messages")
public class MessageSourceAutoConfiguration {
private String basename = "messages";
//我们的配置文件可以直接放在类路径下叫messages.properties;
...
}
spring:
messages:
basename: i18N.message #修改默认路径
运行结果如下图所示:
6、 错误的分类定义,你有什么建议,在代码中进行示例
7、 记录返回给客户端的时候,应包含哪些信息,方便未来的故障排查。在代码中进行示例。
自己定义合适的异常,再交由异常处理器去处理,返回给前端封装好的处理结果。针对于不同的异常,返回不同的错误码。
public class CustomerException extends RuntimeException {
private static final long serialVersionUID = 1L;
public CustomerException(String message) {
super(message);
}
}
@RestControllerAdvice
@Slf4j
public class DemoExceptionHandler {
@ExceptionHandler(CustomerException.class)
public Result<?> customerException(CustomerException e) {
log.error(e.getMessage(), e);
return Result.custom("自定义异常处理");
}
}
package com.example.demo;
/*import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;*/
import lombok.Data;
import java.io.Serializable;
//import org.jeecg.common.constant.CommonConstant;
/**
* 接口返回数据格式
* @author scott
* @email jeecgos@163.com
* @date 2019年1月19日
*/
@Data
//@ApiModel(value="接口返回对象", description="接口返回对象")
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 成功标志
*/
//@ApiModelProperty(value = "成功标志")
private boolean success = true;
/**
* 返回处理消息
*/
//@ApiModelProperty(value = "返回处理消息")
private String message = "操作成功!";
/**
* 返回代码
*/
//@ApiModelProperty(value = "返回代码")
private Integer code = 0;
private Long frontBackUnicode ;
/**
* 返回数据对象 data
*/
//@ApiModelProperty(value = "返回数据对象")
private T result;
/**
* 时间戳
*/
//@ApiModelProperty(value = "时间戳")
private long timestamp = System.currentTimeMillis();
public Result() {
}
public static Result<Object> custom(String message) {
Result<Object> r = new Result<Object>();
r.message = message;
r.code = 9999;
r.success = false;
return r;
}
}
结果展示:
8、 Download文件中,浏览器在本地能另存为。在本次http的协议字段,需要解释
-
Content-Type 实体头部用于指示资源的MIME类型 media type 。
在响应中,Content-Type标头告诉客户端实际返回的内容的内容类型。浏览器会在某些情况下进行MIME查找,并不一定遵循此标题的值; 为了防止这种行为,可以将标题 X-Content-Type-Options 设置为 nosniff。
在请求中 (如POST 或 PUT),客户端告诉服务器实际发送的数据类型。
语法:
Content-Type: text/html; charset=utf-8Content-Type: multipart/form-data; boundary=something
-
content-disposition响应头控制浏览器以下载的形式打开文件
Content-Disposition 响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。在multipart/form-data类型的应答消息体中,
Content-Disposition
消息头可以被用在multipart消息体的子部分中,用来给出其对应字段的相关信息。各个子部分由在Content-Type
中定义的分隔符分隔。用在消息体自身则无实际意义。Content-Disposition消息头最初是在MIME标准中定义的,HTTP表单及
POST
请求只用到了其所有参数的一个子集。只有form-data
以及可选的name
和filename
三个参数可以应用在HTTP场景中。在HTTP场景中,第一个参数或者是inline(默认值,表示回复中的消息体会以页面的一部分或者整个页面的形式展示),或者是attachment(意味着消息体应该被下载到本地;大多数浏览器会呈现一个“保存为”的对话框,将filename的值预填为下载后的文件名,假如它存在的话)。
9、 从Http协议字段解释一下,跨域的时候,客户端涉及的字段,服务器涉及的字段的含义,以及它的作业原理。
预检请求
首先,预检请求只会存在于浏览器端的请求中。根据浏览器的同源策略,就是出于安全考虑,浏览器会限制从脚本发起的跨域HTTP请求。
像XMLHttpRequest和Fetch都遵循同源策略。
浏览器限制跨域请求一般有两种方式:
0.浏览器限制发起跨域请求
1.跨域请求可以正常发起,但是返回的结果被浏览器拦截了
在浏览器请求中,HTTP请求包括: 简单请求 和 需预检的请求
简单请求
简单请求不会触发CORS预检请求,简单请求术语并不属于Fetch(其中定义了CORS 关于Fetch:Fetch和Ajax)规范。
若满足所有下述条件,则该请求可视为“简单请求”:
请求Method满足GET,POST,HEAD中的一种,并且Content-Type满足text/plain,
multipart/form-data,
application/x-www-form-urlencoded中的一种,就可以称为简单请求,还要求这个请求没有设置自定义请求头!
WebKit Nightly 和 Safari Technology Preview 为Accept
, Accept-Language
, 和 Content-Language
首部字段的值添加了额外的限制。如果这些首部字段的值是“非标准”的,WebKit/Safari 就不会将这些请求视为“简单请求”。WebKit/Safari 并没有在文档中列出哪些值是“非标准”的,不过我们可以在这里找到相关讨论:Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language, Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS, and Switch to a blacklist model for restricted Accept headers in simple CORS requests。其它浏览器并不支持这些额外的限制,因为它们不属于规范的一部分。
需预检的请求
“需预检的请求”要求必须首先使用OPTIONS方法发起一个预检请求到服务区,以获知服务器是否允许该实际请求。“预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
非简单请求都是需预检的请求,包括:
0.使用下列请求Method:
PUT,DELETE,CONNET,OPTIONS,TARCE,PATCH
1.使用人为设置了对CORS 安全的首部字段集合之外的其他首部字段。安全首部字段包括:
Accept,Accpet-Language,Content-Language,Content-Type,DPR,DownLink,Save-Data,Viewport-Width,Width
2.Content-Type不在text/plain,
multipart/form-data,
application/x-www-form-urlencoded中的。
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: stoken, Content-Type
首部字段Origin后知服务器该请求来源为http:foo/example,
首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段:stoken 与 Content-Type。服务器据此决定,该实际请求是否被允许。
接下来看预检请求的响应。
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: stoken, Content-Type
Access-Control-Max-Age: 86400
看到前面对请求字段的解释,响应字段的含义也就很明了了。而这些响应头的设置都是需要我们后端程序员在服务器端设置的。
Access-Control-Allow-Origin:* 表明服务器端运行来源于任何站点的请求,这个是一定要设置的,如果不进行设置的话,表明服务器不允许来自任何站点的请求,导致浏览器无法进行正式请求。我们这里把这个响应头设置为*,代表运行任何站点,但从安全角度来看,最好是设置为特定的站点。Access-Control-Max-Age:86400的含义:表明该响应的有效时间为 86400 秒,也就是24小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
服务端设置自定义Header响应头
当遇到自定义Header请求的时候,我们在服务器端可以通过设置Access-Control-Allow-Headers来解决跨域问题。那么如果我们是想设置自定义响应头呢,该怎么办呢?
首先,我们看服务端没有做任何处理的情况。当服务端没有做处理的时候,前端想要获取服务端设置的自定义Header的时候,会报出如下错误:
Refused to get unsafe header "stoken"
原因1:W3C的 xhr 标准中做了限制,规定客户端无法获取 response 中的 Set-Cookie、Set-Cookie2这2个字段,无论是同域还是跨域请求;
原因2:W3C 的 cors 标准对于跨域请求也做了限制,规定对于跨域请求,客户端允许获取的response header字段只限于“simple response header”和“Access-Control-Expose-Headers”
“simple response header”包括的 header 字段有:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma;
很明显,自定义header不在simple response header中。
“Access-Control-Expose-Headers”:首先得注意是”Access-Control-Expose-Headers”进行跨域请求时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。
Spring MVC在4.2及之后的版本中提供一个注解@CorsOrigin,很方便的对细粒度的跨域问题进行处理。