从零构建后端项目-创建基础类
从零构建后端项目基础篇创建自定义异常类封装Web返回对象利用Swagger搭建REST API配置后端验证功能抵御即跨站脚本(XSS)攻击
目录
创建自定义异常类
因为后台Java项目是Web工程,所以有异常消息,我们要在原有异常消息的基础之上,封装状态码,所以需要我们自己创建一个异常类。
自定义异常类继承的父类,我没有选择Exception。因为Exception类型的异常,我们必须要手动显式处理,要么上抛,要么捕获。我希望我定义的异常采用既可以采用显式处理,也可以隐式处理,所以我选择继承RuntimeException这个父类。RuntimeException类型的异常可以被虚拟隐式处理,这样就省去了我们很多手动处理异常的麻烦。
1. 创建 com.example.emos.wx.exception 包
2. 创建EmosException类
package com.example.emos.wx.exception;
import lombok.Data;
@Data
public class EmosException extends RuntimeException{
private String msg;
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) {
super(msg);
this.msg = msg;
this.code = code;
}
public EmosException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
}
封装Web返回对象
虽然SpringMVC的Controller可以自动把对象转换成JSON返回给客户端,但是我们需要制定一个统一的标准,保证所有Controller返回的数据格式一致。最简便的办法就是定义封装类,来统一封装返回给客户端的数据。
1. 修改 pom.xml 文件,添加依赖库。 Apache 的 httpcomponents 库里面的 HttpStatus 类封装了很多状态码,所以我们在Web返回对象中封装状态吗,可以用到这些状态码。添加依赖后Maven重新加载项目。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.13</version>
</dependency>
2. 创建 com.example.emos.wx.common.util 包,然后创建 R 类
3. 代码
package com.example.emos.wx.common.util;
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");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
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 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 ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
利用Swagger搭建REST API
开发前后端分离架构的项目,往往调试后端Web接口需要用到POSTMAN工具。虽然POSTMAN工具的功能非常强大,但是请求参数很多的情况下,我们手写这些参数和数据还是非常麻烦的。因此我们需要一个调试后端Web接口更加简便的方法。恰好Swagger提供了REST API调用方式,我们不需要借助任何工具的情况下,访问Swagger页面,就可以对Web接口进行调用和调试,这种调试方式的效率要远超POSTMAN软件。
配置Swagger
1. (ApiInfoBuilder)定义Swagger页面基本信息
2. (ApiSelectorBuilder)哪些类中的方法会出现在Swagger上面
3. 开启对JWT的支持
·List<ApiKey>: 用户需要输入什么参数
·AuthorizationScope[]: JWT认证在Swagger中的作用域
·List<SecurityReference>: 令牌的作用域
·List<SecurityContext>: 令牌上下文
·Docket
把配置信息给Spring
一、添加依赖库
在 pom.xml 文件中添加Swagger依赖库,这里我们使用的是Swagger2版本,在UI方面,比 Swagger1版本要好看很多。
<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>
二、创建Swagger配置类
在 com.example.emos.wx.config 包中创建 SwaggerConfig 类
package com.example.emos.wx.config;
import io.swagger.annotations.ApiOperation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.ApiSelectorBuilder;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@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在线办公系统");
ApiInfo apiInfo = builder.build();
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 = new ArrayList();
//规定用户需要输入什么参数
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 refList = new ArrayList();
refList.add(reference);
SecurityContext context =
SecurityContext.builder().securityReferences(refList).build();
List cxtList = new ArrayList();
cxtList.add(context);
docket.securityContexts(cxtList);
return docket;
}
}
三、编写测试Web接口
在 com.example.emos.wx.controller 包中创建 TestController 类。
package com.example.emos.wx.controller;
import com.example.emos.wx.common.util.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
@Api("测试Web接口")
public class TestController {
@GetMapping("/sayHello")
@ApiOperation("最简单的测试方法")
public R sayHello(){
return R.ok().put("message","HelloWorld");
}
}
四、修改全局的配置文件
不修改会导致错误,就下面出现的问题。
because the return value of “springfox.documentation.spi.service.contexts.Orderings.patternsCondition(springfox.documentation.RequestHandler)” is null
在application.yml文件中增加配置,配置内容如下:
spring:
mvc:
pathmatch:
matching-strategy: ant-path-matcher
如Spring Boot 2.6发行说明中所述,您可以通过在application.properties文件中将Spring.mvc.pathmatch.matching-strategy设置为ant path matcher来恢复Springfox假定将使用的配置。请注意,只有在不使用Spring Boot的执行器时,此功能才起作用。无论配置的匹配策略如何,执行器始终使用基于路径模式的解析。如果您想在Spring Boot 2.6及更高版本中将其与执行器一起使用,则需要对Springfox进行更改。
因为在springboot2.6之后,将springmvc的默认匹配策略修改为了PathPatternParser,需要将其修改为AntPathMatcher就可以解决问题
五、测试Web接口
打开浏览器,访问 http://127.0.0.1:8080/emos-wx-api/swagger-ui.html
配置后端验证功能
库:Validation
创建Form类
·类声明要添加 @ApiModel【因为要出现在Swagger页面里】
·属性声明要添加 @ApiModelProperty【因为要出现在Swagger页面里】
·属性声明要添加验证注解 @NotNull @NotBlank @Min @Max @Range @Pattern
验证数据要使用 @Valid 注解
当用户提交请求的时候,SpringBoot项目就会把提交的数据封装到Form对象里面。同时执行后端的验证,如果数据满足要求,那么Web方法就可以正常的执行。如果不满足要求就会抛出异常,返回错误信息给客户端。
对于客户端提交表单或者Ajax中的数据,后端的Controller必须先要做验证,然后才能使用这些数据。既然要验证数据,那么不妨我们来使用一下 Validation 库。
一、添加依赖
在 pom.xml 文件中添加依赖,然后让Maven加载依赖库。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
二、创建Form类
validation 库在做后端验证的时候,要求必须用封装类(Form类)来保存客户端提交的数据, 然后在封装类中,我们可以定义验证的规则, validation 会执行这些规则,帮我们验证客户端 提交的数据。
我们为之前的 TestController 里面的 sayHello() 方法设置一个Form类,接受客户端提交的 name 数据。我们在 com.example.emos.wx.controller.form 包里面创建 TestSayHelloForm 类。
package com.example.emos.wx.controller.form;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@ApiModel
@Data
public class TestSayHelloForm {
@NotBlank
@Pattern(regexp = "^[\\u4e00-\\u9fa5]{2,15}$")
@ApiModelProperty("姓名")
private String name;
}
三、修改sayHello()方法
package com.example.emos.wx.controller;
import com.example.emos.wx.common.util.R;
import com.example.emos.wx.controller.form.TestSayHelloForm;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.Validation;
@RestController
@RequestMapping("/test")
@Api("测试Web接口")
public class TestController {
@PostMapping("/sayHello")
@ApiOperation("最简单的测试方法")
public R sayHello(@Valid @RequestBody TestSayHelloForm form){
return R.ok().put("message","Hello,"+form.getName());
}
}
四、执行测试
打开浏览器,访问 http://127.0.0.1:8080/emos-wx-api/swagger-ui.html
抵御即跨站脚本(XSS)攻击
库:Hutool
对Http请求中的数据转义
·设置过滤器
·覆盖Http请求的方法
HttpServletRequest是接口,各家服务器厂商会实现它。
如果直接继承各厂商的请求父类,那么我们的程序就跟厂商绑定在一起。
HttpServletRequestWrapper类使用了装饰器模式,装饰器封装了厂商的HttpServletRequest,只需要覆盖Wrapper类的方法,就能做到覆盖厂商请求对象里方法。
创建过滤器,把Request对象传入Wrapper对象。
getInputStream方法
非常重要。SpringMVC框架就是通过这个方法,从请求里面提取客户端提交的数据,然后把这些数据封装到Form对象里面。如果我们不对 getInputStream 方法读取的数据做转义,那么后端项目就不具备抵御跨站脚本攻击的能力。
Java不支持原生JSON格式
一、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攻击。
二、导入依赖库
因为 Hutool 工具包带有XSS转义的工具类,所以我们要导入 Hutool ,然后利用 Servlet 规范提供的请求包装类,定义数据转义功能。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.0</version>
</dependency>
三、定义请求包装类
我们平时写Web项目遇到的 HttpServletRequest ,它其实是个接口。如果我们想要重新定义请求类,扩展这个接口是最不应该的。因为 HttpServletRequest 接口中抽象方法太多了,我们逐一实现起来太耗费时间。所以我们应该挑选一个简单一点的自定义请求类的方式。那就是继承HttpServletRequestWrapper 父类。
JavaEE只是一个标准,具体的实现由各家应用服务器厂商来完成。比如说 Tomcat 在实现Servlet 规范的时候,就自定义了 HttpServletRequest 接口的实现类。同时JavaEE规范还定义了 HttpServletRequestWrapper ,这个类是请求类的包装类,用上了装饰器模式。不得不说这里用到的设计模式真的非常棒,无论各家应用服务器厂商怎么去实现 HttpServletRequest 接口,用户想要自定义请求,只需要继承 HttpServletRequestWrapper ,对应覆盖某个方法即可,然后把请求传入请求包装类,装饰器模式就会替代请求对象中对应的某个方法。用户的代码和服务器厂商的代码完全解耦,我们不用关心 HttpServletRequest 接口是怎么实现的,借助于包装类我们可以随意修改请求中的方法。同学们,如此优雅的代码设计,有时间你真该认真学习设计模式。
package com.example.emos.wx.config.xss;
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) {
}
};
}
}
四、创建过滤器,把所有请求对象传入包装类
为了让刚刚定义的包装类生效,我们还要在 com.example.emos.wx.config.xss 中创建XssFilter 过滤器。过滤器拦截所有请求,然后把请求传入包装类,这样包装类就能覆盖所有请求的参数方法,用户从请求中获得数据,全都经过转义。
package com.example.emos.wx.config.xss;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class XssFilter implements Filter {
public void init(FilterConfig config) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper( (HttpServletRequest) request);
chain.doFilter(xssRequest, response);
}
@Override
public void destroy() {
}
}
五、给主类添加注解
给SpringBoot主类添加 @ServletComponentScan 注解。
六、测试拦截XSS脚本
1. 把 TestSayHelloForm 中的正则表达式验证给去掉,因为 name 字段只可以是中文,所以无法接收XSS脚本。
2. 在Swagger中,执行 sayHello() 方法,向name属性传入<script>HelloWorld</script> ,然后观察返回的结果。
更多推荐
所有评论(0)