Spring Boot后端架构设计(一)
1、自定义异常(表明是业务是出现了异常)
Exception:必须手动显式处理,要么上抛要么捕获
RuntimeException:既可以显式处理,又可以被虚拟机隐式处理(省去了手动处理异常)
@Data
public class CustomException extends RuntimeException {
private String msg;
private int code = 500;
public CustomException(String msg) {
super(msg);
this.msg = msg;
}
public CustomException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public CustomException(String msg, int code) {
super(msg);
this.msg = msg;
this.code = code;
}
public CustomException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
}
2、封装R对象
数据结构:HashMap
设计模式:静态工厂方法
- 状态码
- 消息
- 数据
导入httpcomponents(里面有很多HTTP状态码)
继承HashMap:因为HashMap可以绑定数据,希望同样可以往R中绑定数据
提供了链式调用
提供预定义好的静态工厂方法来调用,不需要自己new对象
public class R extends HashMap<String, Object> {
public R() {
put("code", HttpStatus.SC_OK);
put("msg", "success");
}
public R put(String key, Object value) {
super.put(key, value);
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, "未知异常,请联系管理员");
}
}
3、整合Swagger
略
4、添加后端验证
@ApiModel 类
@ApiModelProperty 属性
@ApiModel
@Data
public class TestSayHelloForm {
@NotBlank
@Pattern(regexp = "^[\\u4e00-\\u9fa5]{2,15}$",message = "不符合正则表达式")
@ApiModelProperty("姓名")
private String name;
}
validation 校验
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、创建Form类(web方法的参数中需要加上@Valid)
@PostMapping("/sayHello")
@ApiOperation("测试方法")
public R sayHello(@Valid @RequestBody TestSayHelloForm form) {
return R.ok().put("message", "Hello,"+form.getName());
}
5、抵御XSS攻击
设计模式:装饰器模式
HttpServletRequest是一个接口,如果想要重新定义请求类,这接口中的抽象方法太多了,不应该扩展这个接口。
简单自定义请求类:
继承XssHttpServletRequestWrapper父类
JavaEE是一个标准,具体的实现由服务器厂商完成
举例:
Tomcat在实现Servlet规范时,定义了HttpServletRequest接口的实现类。
同时,JavaEE规范还定义了XssHttpServletRequestWrapper请求类的包装类
用到了装饰器模式,无论服务器厂商怎么实现HttpServletRequest接口,只需要继承HttpServletRequestWrapper然后覆盖某个方法即可,然后把请求传入请求包装类(XssFilter–>XssHttpServletRequestWrapper)。
用户的代码和服务器厂商的代码完全解耦,我们不需要关心HttpServletRequest接口是怎么实现的,借助包装类我们可以随意修改请求中的方法,装饰器模式是一种优雅的代码设计。
如果要增加请求的功能,除了子类做方法覆盖之外我可以使用HttpServletRequestWrapper类,这个类是用了装饰器模式,就像给请求套上了手机壳,只需要覆盖Wrapper类的方法,就能做到覆盖厂商请求对象里方法
XssHttpServletRequestWrapper
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
创建过滤器,把Request对象传入Wrapper对象
XssFilter
//普通的Servlet过滤器是优先执行的,是早于Spring MVC框架执行的
@WebFilter(urlPatterns = "/*")//拦截请求的路径(所有路径)
public class XssFilter implements Filter {
总结:
对请求里的数据做转义,做转义的时候把转义的代码写到HttpServletRequestWrapper类的方法里面,然后在项目里面创建一个过滤器,所有的请求都传入Wrapper对象,这样我们的转义就可以生效了。
1、XssFilter
2、XssHttpServletRequestWrapper
3、启动类加
@ServletComponentScan //如果没有这个filter**就不会生效
6、Shiro和JWT
Shiro
- 认证(是否登录)
- 授权(是否有权限)
为什么选用Shiro
Spring Security必须用在Spring项目中,Shiro的普及度更高
原理:
利用HttpSession或Redis来存储Token
利用Filter过滤器对每个Http请求过滤
缺点:使用HttpSession来保存用户登录凭证,保存在服务端,只适用于单体项目
集群模式下(多态Tomcat),进行负载均衡时,会出现问题。
解决:
引入JWT实现单点登录
JWT
将登录凭证加密后,以令牌字符串的方式保存在客户端,不需要关心后端部署架构,客户端需要在请求头中加上token,后端去验证。
并且JWT可以兼容:客户端,浏览器、小程序、物联网设备
这样就实现了全局的单点登录功能,把登录凭证加密保存在客户端的机制就叫做JWT
使用到了多态:
//扩展AuthenticationToken接口
public class OAuth2Token implements AuthenticationToken {
AuthenticationToken是接口类型,定义令牌的封装类的时候实现了这个接口,所以在这是一个多态的写法,传入的是这个接口的子类对象
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;//OAuth2Token是自己写的
}
令牌刷新机制
HttpSession机制:http超时时间为15分钟,HttpSession对象会自动续期
JWT没有这种机制
解决:在Java后端增加令牌自动续期的机制
方案一:双令牌机制
生成过期时间一短一长的令牌字符串,都保存在客户端,如果短的过期了,长短没有过期,重新生成两个令牌。
方案二:缓存令牌机制
令牌不但要保存在客户端,还要保存在Redis
缓存令牌的过期时间是客户端令牌的一倍,如果客户端令牌过期了,缓存令牌没有过期,则生成新的令牌。
OAuth2Filter
Filter拦截请求、响应
把新生成的令牌写到响应里面返回给客户端
1、令牌有效–>放行
2、过期:缓存令牌没有过期–>续期(JwtUtil),缓存令牌过期–>重新登录
新生成的令牌字符串要存储在两个位置
ThreadLocalToken封装类、Redis
多线程使用同一个对象会有并发读写问题
ThreadLocalToken封装类:给每个线程分配一个独立的保险柜(对象)
ThreadLocal是线程安全的
涉及到AOP、多线程
3、定义AOP切面
Filter类需要放行请求给web方法,返回R对象
定义一个AOP切面拦截器,拦截web方法返回的R对象,拦截到R对象后,从ThreadLocal中取出Token绑定在R对象里面。
总结:
ThreadLocalToken类是在OAuth2Filter(生产者)和TokenAspect(消费者)切面类之间传递数据的(第三方媒介)
同一个线程取出的数据和就是之前存的数据,和其他线程没有关系
7、精简返回客户端的异常
Spring MVC框架返回给客户端的异常消息包括执行栈等详细信息
@ControllerAdvice可以全局捕获SpringMVC异常
根据异常类型返回不同的提示
/**
* 精简返回给客户端的异常
*/
//引用日志模块
@Slf4j
//ControllerAdvice这个注解可以捕获Spring抛出的各种异常,因为后端项目Restful风格的调用,所以加上Rest
@RestControllerAdvice
public class ExceptionAdvice {
@ResponseBody //方法返回的字符串需要写到响应里面
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //响应的状态码 500
@ExceptionHandler(Exception.class) //捕获Spring MVC抛出的各种异常(全局捕获异常),Exception子类的异常
public String exceptionHandler(Exception e){//方法的名字随便定义
log.error("执行异常",e);
//根据异常的类型加以判断,返回不同的异常消息
//后端验证失败抛出的异常
if(e instanceof MethodArgumentNotValidException){
//强制转换
MethodArgumentNotValidException exception= (MethodArgumentNotValidException) e;
//获得具体的异常消息(精简的原因),将错误消息返回给前台
//直接getMsg获得的内容太多了,还包含执行栈的内容
return exception.getBindingResult().getFieldError().getDefaultMessage();
}
else if(e instanceof EmosException){
EmosException exception= (EmosException) e;
return exception.getMsg();//直接取出异常对象里面的消息就行了
}
else if(e instanceof UnauthorizedException){
return "你不具备相关权限";
}
else{
return "后端执行异常";
}
}
}