前言
项目进行至此,基本的用户功能和管理员功能已经具备,只要做好权限控制,就是一个完整的项目了。本篇将带领大家完成项目的权限控制和全局异常处理。
代码结构
本篇设计到的代码如下:
- MyInterceptor.java:自定义的拦截器,用于进行权限校验,并记录非法请求日志
- WebMvcConfig.java:Spring Boot 配置类,用于配置拦截器
- entity
- ErrorLog.java:异常日志记录实体类
- mapper
- ErrorLogMapper.java:异常日志持久类
- common/util
- ErrorLogUtils.java:异常日志记录工具类,负责记录各种类型的异常日志,对外提供不同的异常日志记录方法
- RequestUtils.java:请求工具类,修改了请求相关的方法,在发现请求处理有异常时记录异常日志
- controller
- ErrorController.java:全局异常处理类,系统有抛出异常时,捕获异常并记录异常日志
权限控制
流程设计
Spring Boot 项目的权限控制有多种不同的解决方案:
1. 自定义注解 + AOP
- 创建自定义注解,注解中定义允许访问的角色名等权限限制信息
- 在 Controller 中的接口方法上使用自定义注解,设置访问权限
- 创建切面类,切入点设置为自定义注解修饰的方法
- 在切面类中根据用户角色和接口要求的用户角色,实现对权限的控制
2. Spring Security
- 添加所需配置即可,实质上也就是多个拦截器
3. 拦截器
- 自定义拦截器,在需要权限控制的接口上进行权限验证
- 为不同的接口配置不同的拦截器
KellerNotes 项目在搭建时已经使用了统一请求的方式,使用了代理类转发的方式保护实际的业务接口。此时,只需要配置拦截器,使其禁止外部 IP 访问即可。因此,选择使用拦截器来实现权限的控制。
实现方案如下:
- 自定义拦截器,拦截非本地 IP 访问的请求
- 将拦截器配置到需要权限控制的接口中
- 如果有请求被拦截,说明是非法访问,记录日志,便于管理员查看及后续处理工作
功能实现
新建类 MyInterceptor 继承 HandlerInterceptorAdapter.java,重写 preHandler 方法。该方法返回一个布尔值,其值为 true 时,会正常进入所访问的接口;否则,请求在此处会被拦截。
@Component
public class MyInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Console.println("preHandle",RequestUtil.getRealIp(request),PublicConstant.address);
// 仅当请求的真实 IP 地址为本机地址时才允许访问
boolean check = PublicConstant.address.equals(RequestUtil.getRealIp(request));
if(!check){
ErrorLogUtils.accressErrorLog(request);
}
return check;
}
}
新建类 WebMvcConfig 继承 WebMvcConfigurationSupport.java,重写 addInterceptors 方法。该方法用于添加拦截器并配置拦截规则,如果有多个拦截器,都可以在该方法中配置。
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Resource
private MyInterceptor myInterceptor;
@Override
protected void addInterceptors(InterceptorRegistry registry) {
// 添加拦截器
registry.addInterceptor(myInterceptor)
// 拦截管理员功能接口
.addPathPatterns("/admin/**")
// 拦截实际业务接口
.addPathPatterns("/note/**")
.addPathPatterns("/notes/**")
.addPathPatterns("/userCard/**")
.addPathPatterns("/user/**");
}
}
上述代码配置的拦截规则为:
- 代理类及 /base 下的接口允许所有 IP 访问
- 其他接口都只允许本机 IP 访问
这样做的好处是:
- 在本地开发环境中,允许访问所有接口,用以测试业务逻辑
- 在生产环境中,将业务逻辑相关的接口全部对外屏蔽,保证了项目的安全
- 在后续将其改造成 Spring Cloud 项目时,只需要将代理类抽出来,做成一个代理服务,即可实现路由的统一分发和权限控制
日志记录
新建 ErrorLog.java 继承 BaseEntity.java,定义异常日志实体类,对应 error_log 表,用于记录系统中的各种异常信息:
@TableAttribute(name = "error_log",comment = "系统错误日志")
public class ErrorLog extends BaseEntity {
@KeyAttribute(autoIncr = true)
@FieldAttribute
private Integer id;
@FieldAttribute
private String ip;
@FieldAttribute
private String method;
@FieldAttribute(length = 500)
private String url;
@FieldAttribute(length = 2000)
private String params;
@FieldAttribute(length = 1000)
private String type;
@FieldAttribute(length = 2000)
private String message;
}
新建接口 ErrotLogMapper 继承 BaseMapper,实现异常日志的持久化:
@Mapper
public interface ErrorLogMapper extends BaseMapper<ErrorLog> {
}
新建工具类 ErrorLogUtils.java,用于处理各种异常日志,添加 accessErrorLog 方法,用于记录非法访问的日志:
public static void accessErrorLog(HttpServletRequest request){
// 标示异常类型为非法访问
ErrorLog log = new ErrorLog(ACCESS_ERROR);
// 记录访问者的 IP,对于严重的非法访问者,可以加入黑名单机制
log.setIp(RequestUtil.getRealIp(request));
// 记录非法请求的 URL
log.setUrl(request.getRequestURI());
// 记录请求的方法,如: GET、POST
log.setMethod(request.getMethod());
// 写入数据库
utils.mapper.baseInsertAndReturnKey(log);
}
效果测试
由于这个拦截针对的是 IP 地址,因此需要通过非本地地址访问,可以使用手机、其他电脑等设备通过局域网访问不允许访问的接口。这里我使用手机浏览器访问管理员权限的接口:
请求接口后,没有任何信息返回,查看数据库,发现该条请求已被拦截并记录:
系统全局异常处理
@ControllerAdvice 注解时一个增强型的 Controller 注解,可以用来实现:全局异常处理、全局数据绑定、全局数据预处理等功能。在本项目中,使用其实现全局异常处理的功能。
该功能可以捕获到所有 Controller 层抛出的异常,因此,如果在 Service 层或者其他地方使用 try catch
将异常捕获了,在全局异常处理中就找不到了。
功能实现
新建 ErrorController.java,使用 @ControllerAdvice 注解。添加 errorHandler 方法用于处理全局异常。
/**
* @author yangkaile
* @date 2020-05-09 10:32:01
* 处理全局异常
*/
@ControllerAdvice
public class ErrorController {
/**
* 全局异常捕捉处理
* @param ex
* @return
*/
@ResponseBody
// 该注解用于绑定异常类型,可以针对不同的异常做不同的处理,使用 Exception 表示对所有异常做统一的处理
@ExceptionHandler(value = Exception.class)
public ResponseEntity errorHandler(Exception ex) {
ErrorLogUtils.errorLog(ex);
return Response.error();
}
}
日志记录
在 ErrorLogUtils.java 中,添加 errorLog 方法,用于记录系统异常。在记录异常堆栈时要注意:一个异常堆栈可能有几十个,但我们比较关心的往往是项目中代码的堆栈信息而不是第三方依赖包或者 JVM 的异常堆栈,因此,在记录异常堆栈时做一下过滤还是有必要的。
/**
* 记录系统错误日志
* 打印异常堆栈
*
* @param e
*/
public static void errorLog(Exception e){
e.printStackTrace();
ErrorLog log = new ErrorLog(SYSTEM_ERROR);
// 获取项目包名,只记录项目代码中的异常信息,不记录 JVM 的异常
String packageName = KellerApplication.class.getPackage().getName();
// 遍历异常
StackTraceElement[] elements = e.getStackTrace();
StringBuilder builder = new StringBuilder();
builder.append(e.toString()).append(":");
for(StackTraceElement element:elements){
if(element.toString().startsWith(packageName)){
builder.append(element.toString()).append(",");
}
}
log.setMessage(builder.toString());
utils.mapper.baseInsertAndReturnKey(log);
}
效果测试
测试系统异常,就要先模拟出一条异常,这里修改 NotesController.java 中的一个接口,使其必定报错,如下:
@GetMapping
public ResponseEntity get(Integer kellerUserId){
if(kellerUserId == null){
return Response.badRequest();
}
UserInfo userInfo = null;
int type = userInfo.getType();
return Response.ok(service.getListByUser(kellerUserId));
}
修改完毕后,正常登录用户界面,发现页面提示有系统异常,查看数据库,发现有异常的记录:
完整的异常信息记录如下:
java.lang.NullPointerException:
com.justdoit.keller.common.util.ErrorLogUtils.requestErrorLog(ErrorLogUtils.java:72),
com.justdoit.keller.common.util.RequestUtil.doGet(RequestUtil.java:170),
com.justdoit.keller.common.proxy.ApiController.get(ApiController.java:42),
详细地记录了项目代码中的异常堆栈信息,至此,系统全局异常记录完成。
业务失败处理
除了非法访问、系统错误外,业务失败也要注意。在项目完成测试,正式上线后,业务级的异常(如:找不到用户要访问的笔记本、找不到用户信息)应该是不会出现的。如果出现了,就说明项目中的业务逻辑处理有问题,或者是前端请求参数被非法修改了。
功能实现
在 RequestUtils.java 的 doGet、doPost 方法中,对应答做解析,如果应答是业务失败,记录错误日志:
public static ResponseEntity doGet(HttpServletRequest request,Map<String,Object> params,RestTemplate restTemplate){
ResponseEntity<String> responseEntity ;
String url = getUrl(params,request);
try {
if(params == null){
responseEntity = restTemplate.getForEntity(url, String.class);
}else {
responseEntity = restTemplate.getForEntity(url, String.class, params);
}
// 记录 200 OK 应答的日志
ErrorLogUtils.requestSuccessLog(getRealIp(request),"GET",url,params,responseEntity);
}catch (Exception e){
responseEntity = ResponseUtils.getResponseFromException(e);
// 记录错误应答的日志
ErrorLogUtils.requestErrorLog(getRealIp(request),"GET",url,params,responseEntity);
}
return responseEntity;
}
日志记录
在 ErrorLogUtils.java 中添加 requestErrorLog 方法,用于记录业务错误的日志:
public static void requestErrorLog(String ip, String method, String url, Map<String,Object> params, ResponseEntity response){
Console.error("response requestErrorLog",ip,method,url,params,response.getBody());
ResultData resultData;
if(response.getBody() instanceof ResultData){
resultData = (ResultData)response.getBody();
}else{
resultData = JSON.parseObject(response.getBody().toString(),ResultData.class);
}
ErrorLog log = new ErrorLog();
log.setType(REQUEST_ERROR);
log.setIp(ip);
log.setMethod(method);
log.setUrl(url);
// 解析请求参数
StringBuilder builder = new StringBuilder();
for(String key : params.keySet()){
builder.append(key).append(":").append(params.get(key)).append(",");
}
log.setParams(builder.toString());
// 保存应答信息
if(resultData != null){
log.setMessage(resultData.getMessage());
}else {
log.setMessage(response.getBody().toString());
}
utils.mapper.baseInsertAndReturnKey(log);
}
效果测试
测试业务处理失败,需要修改一些请求参数,这里使用的是火狐浏览器,打开开发者工具 -> Web 控制台 -> 网络,选择一个请求,编辑并重发,将参数修改为错误的:
查看数据库,有了新的错误记录:
源码地址
本篇完整的源码地址:
小结
本篇带领大家使用拦截器完成对系统的访问控制,使用 @ControllerAdvice 完成对系统全局异常的处理,并将各种异常日志记录数据库,以便系统管理员可以直接明了地掌握系统情况。