SpringBoot项目通用的配置
SpringBoot项目通用的配置
一、定义自定义异常类
因为后台Java项目是Web工程,所以有异常消息,我们要在原有异常消息的基础之上,封装状态码所以需要我们自己创建一个异常类。
自定义异常类继承的父类,我没有选择Exception。因为Exception类型的异常,我们必须要手动显式处理,要么上抛,要么捕获。我希望我定义的异常采用既可以采用显式处理,也可以隐式处理,所以我选择继承RuntimeException这个父类。RuntimeException类型的异常可以被虚拟机隐式处理,这样就省去了我们很多手动处理异常的麻烦。
-
创建
exception
包 -
创建EmosException类
@Data
@AllArgsConstructor
public class EmosException extends RuntimeException {
/**
* msg
*/
private String msg;
/**
* code
*/
private int code = 500;
public EmosException(String msg){
super(msg);
this.msg = msg;
}
public EmosException(String msg,Throwable e){
super(msg,e);
this.msg = msg;
}
public EmosException(String msg,int code,Throwable e){
super(msg,e);
this.msg = msg;
this.code = code;
}
}
二、自定义全局异常类
因为客户端提交的消息不正确,后端系统可能向客户端返回了大量的异常内容。这里对返回的异常内容做一下精简。
在config
包中创建ExceptionAdvice
类。
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public String exceptionHandler(Exception e){
log.error("执行异常",e);
if(e instanceof MethodArgumentNotValidException){
MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
return exception.getBindingResult().getFieldError().getDefaultMessage();
}else if(e instanceof EmosException){
EmosException exception = (EmosException) e;
return exception.getMsg();
}else if(e instanceof UnauthenticatedException){
return "你不具备相关权限";
}else {
return "后端执行异常";
}
}
}
三、 通用Web返回类
虽然SpringMVC的Controller可以自动把对象转换成JSON返回给客户端,但是我们需要制定一个统一的标准,保证所有Controller返回的数据格式一致。最简便的办法就是定义封装类,来统一封装返回给客户端的数据。
修改pom.xml
文件,添加依赖库。Apache
的httpcomponents
库里面的HttpStatus
类封装了很多状态码,所以我们在Web返回对象中封装状态码,可以用到这些状态码。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.13</version>
</dependency>
先创建common.util
包,然后创建R
类。
import org.apache.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
public class R extends HashMap<String, Object> {
public R(){
put("code", HttpStatus.SC_OK);
put("msg","success");
}
@Override
public R put(String key,Object vale){
super.put(key,vale);
return this;
}
public static R ok(){
return new R();
}
public static R ok(String msg){
R r = new R();
r.put("msg",msg);
return r;
}
public static R ok(Map<String,Object> map){
R r = new R();
r.putAll(map);
return r;
}
public static R error(int code,String msg){
R r = new R();
r.put("code",code);
r.put("msg",msg);
return r;
}
public static R error(String msg){
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR,msg);
}
public static R error(){
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR,"未知异常,请联系管理员");
}
}
四、接入Swagger
开发前后端分离架构的项目,往往调试后端Web接口需要用到POSTMAN工具。虽然POSTMAN工具的功能非常强大,但是请求参数很多的情况下,我们手写这些参数和数据还是非常麻烦的。因此我们需要一个调试后端Web接口更加简便的方法。恰好Swagger提供了REST API调用方式,我们不需要借助任何工具的情况下,访问Swagger页面,就可以对Web接口进行调用和调试,这种调试方式的效率要远超POSTMAN软件。
4.1 添加依赖库
在pom.xml
文件中添加Swagger依赖库,这里我们使用的是Swagger2版本,在UI方面,比Swagger1版本要好看很多。
<!-- swagger 2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
4.2 创建Swagger配置类
在config
包中创建SwaggerConfig
类。
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
Docket docket = new Docket(DocumentationType.SWAGGER_2);
// ApiInfoBuilder 用于在Swagger界面上添加各种信息
ApiInfoBuilder builder = new ApiInfoBuilder();
builder.title("EMOS在线办公系统");
builder.version("V0.0.1");
ApiInfo apiInfo = builder.build();
// 设置页面显示信息(desc描述,作者,lic均可设置)
docket.apiInfo(apiInfo);
// ApiSelectorBuilder 用来设置哪些类中的方法会生成到REST API中
ApiSelectorBuilder selectorBuilder = docket.select();
//所有包下的类
selectorBuilder.paths(PathSelectors.any());
//使用@ApiOperation的方法会被提取到REST API中
selectorBuilder.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class));
docket = selectorBuilder.build();
/*
* 下面的语句是开启对JWT的支持,当用户用Swagger调用受JWT认证保护的方法,
* 必须要先提交参数(例如令牌)
*/
//存储用户必须提交的参数
List<ApiKey> apikey = Lists.newArrayList();
//规定用户需要输入什么参数
apikey.add(new ApiKey("token", "token", "header"));
docket.securitySchemes(apikey);
//如果用户JWT认证通过,则在Swagger中全局有效
AuthorizationScope scope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] scopeArray = {scope};
//存储令牌和作用域
SecurityReference reference = new SecurityReference("token", scopeArray);
List<SecurityReference> refList = Lists.newArrayList();
refList.add(reference);
SecurityContext context = SecurityContext.builder().securityReferences(refList).build();
List<SecurityContext> cxtList = Lists.newArrayList();
cxtList.add(context);
docket.securityContexts(cxtList);
return docket;
}
}
4.3测试Web接口
打开浏览器,访问http://127.0.0.1:8080/emos-wx-api/swagger-ui.html
五、抵御Xss攻击
5.1 XSS攻击的危害
XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java、 VBScript、ActiveX、 Flash 或者甚至是普通的HTML。攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。
例如用户在发帖或者注册的时候,在文本框中输入<script>alert('xss')</script>
这段代码如果不经过转义处理,而直接保存到数据库。将来视图层渲染HTML的时候,把这段代码输出到页面上,那么<script>
标签的内容就会被执行。
通常情况下,我们登陆到某个网站。如果网站使用 HttpSession
保存登陆凭证,那么 SessionId 会以 cookie 的形式保存在浏览器上。如果黑客在这个网页发帖的时候,填写的 JavaScript 代码是用来获取 cookie 内容的,并且把cookie 内容通过Ajax发送给黑客自己的电脑。于是只要有人在这个网站上浏览黑客发的帖子,那么视图层渲染HTML页面,就会执行注入的XSS脚本,于是你的Cookie
信息就泄露了。黑客在自己的电脑上构建出Cookie
,就可以冒充已经登陆的用户。
即便很多网站使用了JWT,登陆凭证( Token令牌
)是存储在浏览器上面的。所以用XSS脚本可以轻松的从Storage中提取出Token
,黑客依然可以轻松的冒充已经登陆的用户。
所以避免XSS攻击最有效的办法就是对用户输入的数据进行转义,然后存储到数据库里面。等到视图层渲染HTML页面的时候。转义后的文字是不会被当做JavaScript执行的,这就可以抵御XSS攻击。
5.2 导入依赖库
因为Hutool
工具包带有XSS转义的工具类,所以我们要导入Hutool
,然后利用Servlet
规范提供的请求包装类,定义数据转义功能。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.0</version>
</dependency>
5.3 定义请求包装类
我们平时写Web项目遇到的HttpServletRequest
,它其实是个接口。如果我们想要重新定义请求类,扩展这个接口是最不应该的。因为 HttpServletRequest
接口中抽象方法太多了,我们逐一实现起来太耗费时间。所以我们应该挑选一个简单一点的自定义请求类的方式。那就是继承HttpservletRequestWrapper
父类。
JavaEE只是一个标准,具体的实现由各家应用服务器厂商来完成。比如说Tomcat
在实现Servlet
规范的时候,就自定义了HttpServletRequest
接口的实现类。同时JavaEE规范还定义了HttpSerrvletRequestWrapper
,这个类是请求类的包装类,用上了装饰器模式。不得不说这里用到的设计模式真的非常棒,无论各家应用服务器厂商怎么去实现 HttpservletRequest 接口,用户想要自定义请求,只需要继承HttpServletRequestwrapper
,对应覆盖某个方法即可,然后把请求传入请求包装类,装饰器模式就会替代请求对象中对应的某个方法。用户的代码和服务器厂商的代码完全解耦,我们不用关心HttpServletRequest
接口是怎么实现的,借助于包装类我们可以随意修改请求中的方法。
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import cn.hutool.json.JSONUtil;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (!StrUtil.hasEmpty(value)) {
value = HtmlUtil.filter(value);
}
return value;
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
for (int i = 0; i < values.length; i++) {
String value = values[i];
if (!StrUtil.hasEmpty(value)) {
value = HtmlUtil.filter(value);
}
values[i] = value;
}
}
return values;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> parameters = super.getParameterMap();
Map<String, String[]> map = new LinkedHashMap<>();
if (parameters != null) {
for (String key : parameters.keySet()) {
String[] values = parameters.get(key);
for (int i = 0; i < values.length; i++) {
String value = values[i];
if (!StrUtil.hasEmpty(value)) {
value = HtmlUtil.filter(value);
}
values[i] = value;
}
map.put(key, values);
}
}
return map;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
if (!StrUtil.hasEmpty(value)) {
value = HtmlUtil.filter(value);
}
return value;
}
@Override
public ServletInputStream getInputStream() throws IOException {
InputStream in = super.getInputStream();
StringBuffer body = new StringBuffer();
InputStreamReader reader = new InputStreamReader(in, Charset.forName("UTF-8"));
BufferedReader buffer = new BufferedReader(reader);
String line = buffer.readLine();
while (line!=null) {
body.append(line);
line = buffer.readLine();
}
buffer.close();
reader.close();
in.close();
Map<String,Object> map = JSONUtil.parseObj(body.toString());
Map<String, Object> resultMap = new HashMap(map.size());
for (String key : map.keySet()) {
Object val = map.get(key);
if (map.get(key) instanceof String) {
resultMap.put(key, HtmlUtil.filter(val.toString()));
} else {
resultMap.put(key, val);
}
}
String str = JSONUtil.toJsonStr(resultMap);
final ByteArrayInputStream bain = new ByteArrayInputStream(str.getBytes());
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bain.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
};
}
}