适配了Task新的数据结构,完成了限流控制并通过了测试,实现了多次登录失败banIP,为MybatisPlus添加了防全表更新及删除的插件
parent
8cc1111489
commit
1a001fb599
|
@ -23,3 +23,7 @@ group /project/{projectId}/group
|
|||
|
||||
---
|
||||
导入账户 大权限
|
||||
|
||||
工作进度统计
|
||||
个人/项目工作项具体完成统计
|
||||
工作项时间进度统计
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import java.util.concurrent.TimeUnit;
|
|||
* @author 佘语殊
|
||||
* @since 2022/7/11 16:57
|
||||
*/
|
||||
//TODO: 加到代码里
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Target({ElementType.METHOD})
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -111,6 +111,7 @@ public class ProjectGroupController {
|
|||
return ResponseMap.ofSuccess(projectGroupService.collectStatsForGroupPositions(token, projectId));
|
||||
}
|
||||
|
||||
//FIXME: DELETE
|
||||
@SneakyThrows
|
||||
@GetMapping("/{staffId}/stats")
|
||||
public ResponseList<StaffProcessDTO> getProjectProcessOfStaff(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -84,6 +84,9 @@ public class BeanUtils {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查object是否在set中
|
||||
*/
|
||||
public static boolean checkInSet(Object object, Object... set) {
|
||||
return Arrays.asList(set).contains(object);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -18,7 +18,7 @@ public class RedisTests {
|
|||
|
||||
@Test
|
||||
void test() {
|
||||
redisTemplate.opsForList().rightPush(123456, 89);
|
||||
System.out.println(redisTemplate.opsForValue().get("l1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in New Issue