适配了Task新的数据结构,完成了限流控制并通过了测试,实现了多次登录失败banIP,为MybatisPlus添加了防全表更新及删除的插件

master
ArgonarioD 2022-07-12 12:01:04 +08:00
parent 8cc1111489
commit 1a001fb599
18 changed files with 153 additions and 31 deletions

View File

@ -22,4 +22,8 @@ group /project/{projectId}/group
项目日志 项目统计 克隆项目 项目公告
---
导入账户 大权限
导入账户 大权限
工作进度统计
个人/项目工作项具体完成统计
工作项时间进度统计

View File

@ -26,26 +26,28 @@ public class ExceptionHandlerAdvice {
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseMap handleUnauthorizedException(Exception e) {
// log.error(ExceptionUtils.getStackTrace(e));
// log.error(e.getMessage());
log.error(e.getMessage(), e);
return ResponseMap.of(HttpStatus.UNAUTHORIZED.value(), e.getMessage());
}
@ExceptionHandler(BadRequestException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseMap handleBadRequestException(BadRequestException e) {
// log.error(e.getMessage());
log.error(e.getMessage(), e);
return ResponseMap.of(HttpStatus.BAD_REQUEST.value(), e.getMessage());
}
@ExceptionHandler(ForbiddenException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseMap handleForbiddenException(ForbiddenException e) {
log.error(e.getMessage(), e);
return ResponseMap.of(HttpStatus.FORBIDDEN.value(), e.getMessage());
}
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseMap handleBindException(BindException e) {
log.error(e.getMessage(), e);
return ResponseMap.of(HttpStatus.BAD_REQUEST.value(),
e.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
@ -56,6 +58,7 @@ public class ExceptionHandlerAdvice {
@ExceptionHandler(TooManyRequestException.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public ResponseMap handleTooManyRequestException(TooManyRequestException e) {
log.error(e.getMessage(), e);
return ResponseMap.of(HttpStatus.TOO_MANY_REQUESTS.value(), e.getMessage());
}
}

View File

@ -7,6 +7,7 @@ import java.util.concurrent.TimeUnit;
* @author
* @since 2022/7/11 16:57
*/
//TODO: 加到代码里
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ElementType.METHOD})

View File

@ -38,7 +38,6 @@ public class RateLimitAOP {
if (limiter == null) {
limiter = RateLimiter.create(limit.permitsPerSecond());
Class<? extends RateLimiter> clazz = limiter.getClass();
//TODO: DEBUG TEST
Field burstSecondsField = clazz.getDeclaredField("maxBurstSeconds");
burstSecondsField.setAccessible(true);
burstSecondsField.set(limiter, limit.maxBurstSeconds());

View File

@ -2,6 +2,7 @@ package cn.edu.hfut.rmdjzz.projectmanagement.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
@ -23,6 +24,7 @@ public class MybatisPlusConfig {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
}

View File

@ -1,5 +1,6 @@
package cn.edu.hfut.rmdjzz.projectmanagement.controller;
import cn.edu.hfut.rmdjzz.projectmanagement.annotation.RateLimit;
import cn.edu.hfut.rmdjzz.projectmanagement.entity.Project;
import cn.edu.hfut.rmdjzz.projectmanagement.entity.dto.ProjectDTO;
import cn.edu.hfut.rmdjzz.projectmanagement.exception.BadRequestException;
@ -33,6 +34,7 @@ public class ProjectController {
private IProjectGroupService projectGroupService;
@Operation(summary = "根据Token获取该员工的ProjectList")
//@RateLimit(permitsPerSecond = 1, maxBurstSeconds = 5)
@SneakyThrows
@GetMapping
public ResponseList<ProjectDTO> getProjectListOfStaff(

View File

@ -111,6 +111,7 @@ public class ProjectGroupController {
return ResponseMap.ofSuccess(projectGroupService.collectStatsForGroupPositions(token, projectId));
}
//FIXME: DELETE
@SneakyThrows
@GetMapping("/{staffId}/stats")
public ResponseList<StaffProcessDTO> getProjectProcessOfStaff(

View File

@ -7,6 +7,7 @@ import cn.edu.hfut.rmdjzz.projectmanagement.exception.TokenException;
import cn.edu.hfut.rmdjzz.projectmanagement.service.IStaffService;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.FileUtils;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.TokenUtils;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.http.HttpUtils;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.http.ResponseMap;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.SneakyThrows;
@ -15,6 +16,7 @@ import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
@ -32,9 +34,12 @@ public class StaffController {
@PostMapping("/login")
public ResponseMap login(
@Parameter(description = "只需要传入staffUsername和staffPassword两个属性即可staffPassword需要md5加密后传输")
@RequestBody Staff staff
@RequestBody Staff staff,
HttpServletRequest request
) {
return ResponseMap.ofSuccess("登录成功", staffService.login(staff.getStaffUsername(), staff.getStaffPassword()));
String requestIpAddress = HttpUtils.getRequestIpAddress(request);
return ResponseMap.ofSuccess("登录成功",
staffService.login(requestIpAddress, staff.getStaffUsername(), staff.getStaffPassword()));
}
@SneakyThrows

View File

@ -25,6 +25,7 @@ public class TaskController {
private ITaskService taskService;
@Autowired
private IProjectService projectService;
@SneakyThrows
@GetMapping("/{fatherId}/subtask")
public ResponseList<TaskDTO> getSubTaskList(
@ -61,7 +62,7 @@ public class TaskController {
@PathVariable("projectId") Integer projectId,
@RequestBody Task task
) {
if(!projectService.checkOpenStatus(projectId))
if (!projectService.checkOpenStatus(projectId))
throw new BadRequestException(IProjectService.PROJECT_UNOPENED);
task.setTaskProjectId(projectId);
taskService.insertTask(token, task);
@ -76,7 +77,7 @@ public class TaskController {
@PathVariable("taskId") Long taskId,
@RequestBody Task task
) {
if(!projectService.checkOpenStatus(projectId))
if (!projectService.checkOpenStatus(projectId))
throw new BadRequestException(IProjectService.PROJECT_UNOPENED);
task.setTaskProjectId(projectId);
task.setTaskId(taskId);
@ -91,22 +92,32 @@ public class TaskController {
@PathVariable("projectId") Integer projectId,
@PathVariable("taskId") Long taskId
) {
if(!projectService.checkOpenStatus(projectId))
if (!projectService.checkOpenStatus(projectId))
throw new BadRequestException(IProjectService.PROJECT_UNOPENED);
taskService.deleteTaskAndSubTask(token, projectId, taskId);
return ResponseMap.ofSuccess();
}
@SneakyThrows
@GetMapping("/stats")
@GetMapping("/stats/trend")
public ResponseMap getTaskTrend(
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId
) {
if(!projectService.checkOpenStatus(projectId)) {
if (!projectService.checkOpenStatus(projectId)) {
throw new BadRequestException(IProjectService.PROJECT_UNOPENED);
}
return ResponseMap.ofSuccess("查询成功", taskService.getProjectTaskTrend(token, projectId));
}
//TODO:
@GetMapping({"/stats", "/stats/{staffId}"})
public ResponseMap getProjectStatistics(
@RequestHeader(TokenUtils.HEADER_TOKEN) String token,
@PathVariable Integer projectId,
@PathVariable(required = false) Integer staffId
) {
return ResponseMap.ofSuccess();
}
}

View File

@ -27,7 +27,6 @@ public class TaskDTO {
private LocalDateTime taskClosedTime;
private Integer taskPriority;
private String taskDescription;
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, Object> attachedInfo;
private Boolean hasChildren;
private Integer childrenCount;
}

View File

@ -17,7 +17,7 @@ public interface IStaffService extends IService<Staff> {
String STAFF_DOES_NOT_EXIST = "用户不存在";
Map<String, Object> login(String username, String password) throws BadRequestException, TokenException;
Map<String, Object> login(String requestIpAddress, String username, String password) throws BadRequestException, TokenException, ForbiddenException;
Boolean logout(String token) throws TokenException;

View File

@ -7,6 +7,7 @@ import cn.edu.hfut.rmdjzz.projectmanagement.mapper.StaffMapper;
import cn.edu.hfut.rmdjzz.projectmanagement.service.IStaffService;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.BeanUtils;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.MapBuilder;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.TimeUtils;
import cn.edu.hfut.rmdjzz.projectmanagement.utils.TokenUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -35,26 +36,44 @@ import java.util.concurrent.TimeUnit;
@Service
public class StaffServiceImpl extends ServiceImpl<StaffMapper, Staff> implements IStaffService {
private static final Long tokenDuration = 5 * 60 * 60L;
private static final int LOGIN_TRY_COUNT_TIMEOUT = 15; //minutes
private static final int LOGIN_MAX_TRY_COUNT = 5;
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
//TODO: ban掉多次登录失败的IP
@SuppressWarnings("ConstantConditions")
@Override
public Map<String, Object> login(String staffUsername, String password) throws BadRequestException {
public Map<String, Object> login(String requestIpAddress, String staffUsername, String password) throws BadRequestException, ForbiddenException {
if (Boolean.FALSE.equals(redisTemplate.hasKey(requestIpAddress))) {
redisTemplate.opsForValue().set(requestIpAddress, 0, LOGIN_TRY_COUNT_TIMEOUT, TimeUnit.MINUTES);
}
int loginCount = (int) redisTemplate.opsForValue().get(requestIpAddress);
if (loginCount >= LOGIN_MAX_TRY_COUNT) {
throw new ForbiddenException(String.format("还需要等待%s才能登录"
, TimeUtils.secondsFormat(redisTemplate.getExpire(requestIpAddress))));
}
if (staffUsername == null || staffUsername.strip().length() == 0)
throw new BadRequestException("用户名为空");
else if (!staffUsername.equals(staffUsername.replaceAll("[^a-zA-Z0-9]", "")))
throw new BadRequestException("用户名格式错误");
else if (password == null || password.trim().length() != 32)
throw new BadRequestException("密码格式错误");
//throw new BadRequestException("用户名为空");
throwLoginException(requestIpAddress, loginCount);
if (!staffUsername.equals(staffUsername.replaceAll("[^a-zA-Z0-9]", "")))
//throw new BadRequestException("用户名格式错误");
throwLoginException(requestIpAddress, loginCount);
if (password == null || password.trim().length() != 32)
//throw new BadRequestException("密码格式错误");
throwLoginException(requestIpAddress, loginCount);
Staff staff = getOne(Wrappers.<Staff>lambdaQuery().eq(Staff::getStaffUsername, staffUsername));
if (staff == null)
throw new BadRequestException(STAFF_DOES_NOT_EXIST);
//throw new BadRequestException(STAFF_DOES_NOT_EXIST);
throwLoginException(requestIpAddress, loginCount);
password = DigestUtils.md5DigestAsHex((password + staff.getStaffSalt()).getBytes());
if (!staff.getStaffPassword().equals(password))
throw new BadRequestException("密码错误");
if (!staff.getStaffPassword().equals(password)) {
throwLoginException(requestIpAddress, loginCount);
}
String token = TokenUtils.getToken(staff.getStaffUsername(), staff.getStaffId(), staff.getStaffGlobalLevel(), tokenDuration);
redisTemplate.opsForValue().set(
Objects.<Integer>requireNonNull(TokenUtils.getStaffId(token)),
@ -67,6 +86,11 @@ public class StaffServiceImpl extends ServiceImpl<StaffMapper, Staff> implements
.build();
}
private void throwLoginException(String requestIpAddress, int loginCount) throws BadRequestException {
redisTemplate.opsForValue().set(requestIpAddress, ++loginCount, LOGIN_TRY_COUNT_TIMEOUT, TimeUnit.MINUTES);
throw new BadRequestException(String.format("登录失败,%d分钟内还有%d次登录机会", LOGIN_TRY_COUNT_TIMEOUT, LOGIN_MAX_TRY_COUNT - loginCount));
}
@Override
public Boolean logout(String token) {
Integer staffId = TokenUtils.getStaffId(token);

View File

@ -286,10 +286,14 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task> implements IT
task.setTaskStatus(Task.STATUS_WAITING);
task.setChildrenCount(0);
task.setTaskClosedTime(null);
if (baseMapper.insert(task) == 0) {
if (baseMapper.insert(task) == 0 ||
baseMapper.update(null,
Wrappers.<Task>lambdaUpdate()
.eq(Task::getTaskId, task.getTaskFatherId())
.setSql("children_count = children_count + 1")) != 1
) {
throw new BadRequestException(BadRequestException.OPERATE_FAILED);
}
baseMapper.update(null, Wrappers.<Task>lambdaUpdate().setSql("children_count = children_count + 1"));
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new BadRequestException(BadRequestException.OPERATE_FAILED);

View File

@ -84,6 +84,9 @@ public class BeanUtils {
return false;
}
/**
* objectset
*/
public static boolean checkInSet(Object object, Object... set) {
return Arrays.asList(set).contains(object);
}

View File

@ -63,4 +63,22 @@ public class TimeUtils {
}
return true;
}
/**
*
*/
public static String secondsFormat(long seconds) {
long minutes = 0;
if ((minutes = seconds / 60) > 0) {
seconds %= 60;
}
StringBuilder sb = new StringBuilder();
if (minutes > 0) {
sb.append(minutes).append("分");
}
if (seconds > 0) {
sb.append(seconds).append("秒");
}
return sb.toString();
}
}

View File

@ -0,0 +1,48 @@
package cn.edu.hfut.rmdjzz.projectmanagement.utils.http;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @author
* @since 2022/7/12 10:57
*/
public class HttpUtils {
public static String getRequestIpAddress(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
// ipAddress = this.getRequest().getRemoteAddr();
return ipAddress;
}
}

View File

@ -11,7 +11,7 @@
task_name,
task_project_id,
task_holder_id,
s.staff_fullname AS task_holder_name,
s.staff_fullname AS task_holder_name,
task_status,
task_type,
t.task_father_id,
@ -22,11 +22,9 @@
task_priority,
task_description,
attached_info,
judge.task_father_id IS NOT NULL AS has_children
children_count
FROM task AS t
JOIN (SELECT staff_id, staff_fullname FROM staff) AS s ON t.task_holder_id = s.staff_id
LEFT JOIN (SELECT DISTINCT task_father_id FROM task WHERE is_deleted = 0) AS judge
ON t.task_id = judge.task_father_id
WHERE is_deleted = 0
AND task_project_id = #{projectId}
AND t.task_father_id = #{fatherId}

View File

@ -18,7 +18,7 @@ public class RedisTests {
@Test
void test() {
redisTemplate.opsForList().rightPush(123456, 89);
System.out.println(redisTemplate.opsForValue().get("l1"));
}
@Test