从零开始的后台管理系统(2)
发布于 2020-07-08 修改于 2020-07-14 从零开始的后台管理 java springboot
本章前提
- 了解logback
- 了解swagger
- 了解xss攻击
- 了解druid
- 了解mybatis
本章内容
代码
日志配置、全局异常捕获、swagger配置、xss过滤、druid与mybatis配置。
基本目录创建
各个包名就是字面上的意思。还有的是,application.yml
为配置文件,与application.properties
,个人偏好于.yml格式的文件。
日志
springboot默认使用logback日志。
基本配置
在resources下创建logback-spring.xml
。logback-spring.xml
会自动被springboot加载。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<configuration debug="false">
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%logger{50}:logger名字长度50,%msg:日志消息,%n是换行符-->
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
<!--设置编码-->
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 文件名格式 -->
<FileNamePattern>
log/%d{yyyy-MM-dd}/%d{yyyy-MM-dd}.log
</FileNamePattern>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{yyyy-MM-dd HH:mm:ss} -%msg%n
</Pattern>
</layout>
</appender>
<!-- com.guai下的打印是DEBUG -->
<logger name="com.guai" level="DEBUG" />
<!-- INFO级别打印 -->
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="file" />
</root>
</configuration>
配置好后,启动项目后会按照配置文件里的格式打印日志,最后也会生成日志文件。
响应信息主体
添加pom依赖
1 | <!--lombok--> |
定义返回状态码枚举
在com.guai.common.enums
下创建ResultEnum.java
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum ResultEnum {
SUCCESS(0,"成功"),
FAIL(1,"失败"),
;
private int code ;
private String message;
ResultEnum(int code,String message){
this.code=code;
this.message=message;
}
}
其中的状态码code可以自定义。
封装响应消息主体
在com.guai.common.utils
下创建R.java
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
private int code;
private String message;
private T data;
public R(){
this(ResultEnum.SUCCESS,"success");
}
public R(T t){
this(ResultEnum.SUCCESS,"success",t);
}
public R(ResultEnum e, String message){
this(e,message,null);
}
public R(ResultEnum e ,String message,T data){
this.code = e.getCode();
this.data = data;
this.message= message;
}
public static <T> R ok(){
return new R();
}
public static <T> R ok(T data){
return new R(data);
}
public static <T> R oke(String message){
return new R(ResultEnum.SUCCESS,message);
}
public static <T> R error(){
return new R(ResultEnum.FAIL,"error");
}
public static <T> R error(String message){
return new R(ResultEnum.FAIL,message);
}
public static <T> R error(T data){
return new R(ResultEnum.FAIL,"error",data);
}
public static <T> R error(String message,T data){
return new R(ResultEnum.FAIL,message,data);
}
public static <T> R result(ResultEnum e,String message){
return new R(e,message);
}
public static <T> R result(ResultEnum e,String message,T data){
return new R(e,message,data);
}
}
虽然上面定义了很多方法,但实际常用的就ok()
、ok(T data)
、error()
。
测试
在TestController.java
中添加新的接口。1
2
3
4"/r") (
public R r(String some){
return R.ok(some);
}
启动服务访问接口http://localhost:8080/r?some=abcd
。
全局异常捕获
在com.guai.common.exception
下创建GlobalExceptionHandler.java
。1
2
3
4
5
6
7
8
9
10
public class GlobalExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);
(Exception.class)
public R handleException(Exception e) {
LOG.error(e.getMessage(), e);
return R.error(e.getMessage());
}
}
在TestController.java
中添加新的接口。1
2
3
4"ex") (
public R r() throws Exception {
throw new Exception("错误信息");
}
重启服务访问接口http://localhost:8080/ex
。
在访问一个不存在的接口时,springboot有自己默认的处理页,如下。
这个目前是无法被全局异常捕获,要做以下修改。
在application.yml
中添加1
2
3
4
5spring:
mvc:
throw-exception-if-no-handler-found: true
resources:
add-mappings: false
这个添加之后,异常就可以捕获到了。
为了与Exception.class
区分,在GlobalExceptionHandler
中添加NoHandlerFoundException.class
的异常捕获。1
2
3
4
5
6 (NoHandlerFoundException.class)
public R handlerNoFoundException(Exception e)
{
LOG.error(e.getMessage(), e);
return R.error("路径不存在");
}
再次访问不存在的接口。
有一个注意事项,添加spring.mvc.throw-exception-if-no-handler-found=true
与spring.resources.add-mappings=false
之后,项目中的静态文件访问将会出问题,在下面使用swagger的时候会有解决方案。
api文档
swagger
添加pom依赖
1 | <dependency> |
添加配置
在com.guai.common.config
下创建Swagger2Config
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 2
public class Swagger2Config {
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//为当前包路径
.apis(RequestHandlerSelectors.any())
.paths(Predicates.not(PathSelectors.regex("/error.*")))// 错误路径不监控
.paths(PathSelectors.any())
.build();
}
//构建 api文档的详细信息函数
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
//页面标题
.title("功能测试")
//创建人
.contact(new Contact("小怪", "http://blog.lsiru.com", "gqiwei163@.com"))
//版本号
.version("1.0")
//描述
.description("API 描述")
.build();
}
}
因为swagger页面是静态文件,之前又做全局异常捕获,要做下处理。在com.guai.common.config
下创建ResourcesConfig
。1
2
3
4
5
6
7
8
9
10
public class ResourcesConfig implements WebMvcConfigurer {
public void addResourceHandlers(ResourceHandlerRegistry registry)
{
/** swagger配置 */
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
简单测试
启动服务器,访问http://localhost:8080/swagger-ui.html
。
之所以有这么多个接口,是因为我们使用了@RequestMapping()
注解,会加载所有的method。规定method之后就会只存在你设置的method,如@RequestMapping(value ="/xxx",method = RequestMethod.POST)
、@PostMapping("/xxx")
。
UI
使用swagger自己的页面,觉得太丑,不好用,可以使用另一个ui——swagger-bootstrap-ui。
添加依赖
1 | <dependency> |
修改ResourcesConfig
在addResourceHandlers(ResourceHandlerRegistry registry)
里添加registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
。
简单测试
启动服务器,访问http://localhost:8080/doc.html
。
测试
后续会采用swagger-bootstrap-ui,故swagger-ui在之后篇章中会被删除。
给R.java添加注解
1 |
|
创建TestDO
1 |
|
修改TestController
1 | "测试内容",tags="测试") (value = |
启动服务器,访问http://localhost:8080/doc.html
,查看swagger接口测试,参数所有备注都能查看。
添加xss过滤器
xss过滤器主要处理请求中的html代码,防止xss攻击,这里采用Jsoup来处理。
代码编写
添加pom依赖
1 | <dependency> |
创建JsoupUtil.java
在com.guai.common.utils
下创建JsoupUtil.java
,主要是处理html。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class JsoupUtil {
/**
* 使用自带的 basicWithImages 白名单
* 允许的便签有 a,b,blockquote,br,cite,code,dd,dl,dt,em,i,li,ol,p,pre,q,small,span,strike,strong,sub,sup,u,ul,img
* 以及 a 标签的 href,img 标签的 src,align,alt,height,width,title 属性
*/
private static final Whitelist whitelist = Whitelist.basicWithImages();
/** 配置过滤化参数, 不对代码进行格式化 */
private static final Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
static {
// 富文本编辑时一些样式是使用 style 来进行实现的
// 比如红色字体 style="color:red;"
// 所以需要给所有标签添加 style 属性
whitelist.addAttributes(":all", "style");
}
public static String clean(String content) {
return Jsoup.clean(content, "", whitelist, outputSettings);
}
}
创建XssHttpServletRequestWrapper.java
在com.guai.common.filter
下创建XssHttpServletRequestWrapper.java
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
public String getParameter(String name) {
name = JsoupUtil.clean(name);
String value = super.getParameter(name);
if (StringUtils.isNotBlank(value)) {
value = JsoupUtil.clean(value);
}
return value;
}
public String[] getParameterValues(String name) {
String[] arr = super.getParameterValues(name);
if(arr != null){
for (int i=0;i<arr.length;i++) {
arr[i] = JsoupUtil.clean(arr[i]);
}
}
return arr;
}
public String getHeader(String name) {
name = JsoupUtil.clean(name);
String value = super.getHeader(name);
if (StringUtils.isNotBlank(value)) {
value = JsoupUtil.clean(value);
}
return value;
}
}
重写getParameter(String name)
、getParameterValues(String name)
、getHeader(String name)
,将请求参数全都做xss过滤。
创建XssFilter.java
在com.guai.common.filter
下创建XssFilter.java
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56public class XssFilter implements Filter {
public List<String> excludes = new ArrayList<String>();
public void init(FilterConfig filterConfig) throws ServletException {
String temp = filterConfig.getInitParameter("excludes");
if(temp != null){
String[] url = temp.split(",");
for(int i=0;url!=null && i<url.length;i++){
if(!"".equals(url[i])){
excludes.add(url[i]); //添加白名单
}
}
}
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if(handleExcludeURL(request,response)){
filterChain.doFilter(request,response);
return ;
}
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(request);
filterChain.doFilter(xssRequest,response);
}
/**
* 判断连接是否在白名单里
* @param request
* @param response
* @return
*/
public boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response){
if(excludes.isEmpty()||excludes ==null){
return false;
}
String url = request.getServletPath();
for(String pattern : excludes){
Pattern p = Pattern.compile("^"+pattern);
Matcher m = p.matcher(url);
if(m.find()){
return true;
}
}
return false;
}
public void destroy() {
}
}
创建FilterConfig.java
在com.guai.common.config
下创建FilterConfig.java
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FilterConfig {
public FilterRegistrationBean xssFilterRegistrationBean(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new XssFilter());
filterRegistrationBean.setOrder(1);//优先级 越小代表优先级越高
filterRegistrationBean.setEnabled(true);//是否开启
filterRegistrationBean.addUrlPatterns("/*");
Map<String,String> initParameters = new HashMap<String,String>();
initParameters.put("excludes","");//白名单, 多个以逗号相隔
filterRegistrationBean.setInitParameters(initParameters);
return filterRegistrationBean;
}
}
至此xss过滤器就已经处理完了,接下来就是测试了。
测试
TestController.java
中添加接口。1
2
3
4"xss") (
public R xss(String url){
return R.ok(url);
}
启动服务器,然后在浏览器中访问http://localhost:8080/xss?url=<script>alert(1)</script>测试
。
xss过滤成功。对于xss攻击,完全可以自己视情况在XssHttpServletRequestWrapper.java
中自定义处理方式。
数据库连接
添加pom依赖
1 | <!-- druid --> |
连接池druid配置
druid是Java语言中最好的数据库连接池。提供强大的监控和扩展功能。
配置文件
在application.yml
中添加1
2
3
4#读取application-dev.yml 配置文件 测试环境dev、生产环境pro
spring:
profiles:
active: dev
在application-dev.yml
中添加1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/life?useUnicode=true&characterEncoding=utf8
username: root
password: 123456789
#WebStatFilter
web-stat-filter:
enabled: true
profile-enable: true
url-pattern: /*
#StatViewServlet
stat-view-servlet:
enabled: true
initial-size: 1
min-idle: 3
max-active: 20
#配置获取连接等待超时时间
max-wait: 60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 30000
#让连接池知道数据库已经断开了,并且自动测试连接查询
validation-query: select 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
filter:
stat:
enabled: true
#开启慢查询
log-slow-sql: true
slow-sql-millis: 2000
#合并sql
merge-sql: true
wall:
enabled: true
更多的配置详情请访问https://github.com/alibaba/druid/。
druid测试
启动服务,访问http://localhost:8080/druid/index.html
。
mybaits
配置修改
在application.yml
添加1
2
3
4
5
6mybatis:
#扫描mapper.xml路径
mapper-locations: /mybatis/**/*.xml
configuration:
#开启驼峰原则
map-underscore-to-camel-case: true
在启动类GuaiApplication.java
添加注解@MapperScan("com.guai.*.dao")
开启扫描。
测试
测试sql使用注解的形式,之后将会写在xml文件中。
在com.guai.system.dao
下创建TestDAO.java
,其中表life_plan可以换成自己库里其他随意一张表。1
2
3
4
5
public interface TestDAO {
"select count(*) from life_plan") (
public int select();
}
修改TestController.java
。1
2
3
4
5
6
7
8
9
10
11public class TestController {
private TestDAO testDAO;
...
"mybatis") (
public R mybatis(){
return R.ok(testDAO.select());
}
}
启动服务访问接口http://localhost:8080/mybatis
。
此时再去访问druid的监控网址,进入SQL监控和SQL防火墙已经有了数据。
本章结语
本章感觉乱乱的。下章开始安全管理框架的配置。