Commit a1449b04 by tntxia

修改密码和增加操作日志的Bug修改

parent f29e0f4d
package com.dji.sdk.common; package com.dji.sdk.common;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.HttpStatus;
@Schema(description = "The data format of the http response.") @Schema(description = "The data format of the http response.")
public class HttpResultResponse<T> { public class HttpResultResponse<T> {
...@@ -92,4 +93,11 @@ public class HttpResultResponse<T> { ...@@ -92,4 +93,11 @@ public class HttpResultResponse<T> {
.setCode(errorInfo.getCode()) .setCode(errorInfo.getCode())
.setMessage(errorInfo.getMessage()); .setMessage(errorInfo.getMessage());
} }
public static HttpResultResponse<Object> unauthorized(String message) {
return new HttpResultResponse<Object>()
.setCode(HttpStatus.UNAUTHORIZED.value())
.setMessage(message);
}
} }
...@@ -2,6 +2,7 @@ package com.dji.sample.manage.controller; ...@@ -2,6 +2,7 @@ package com.dji.sample.manage.controller;
import com.aliyun.oss.internal.SignUtils; import com.aliyun.oss.internal.SignUtils;
import com.dji.sample.common.error.CommonErrorEnum; import com.dji.sample.common.error.CommonErrorEnum;
import com.dji.sample.common.util.IPUtils;
import com.dji.sample.manage.model.dto.UserDTO; import com.dji.sample.manage.model.dto.UserDTO;
import com.dji.sample.manage.model.dto.UserLoginDTO; import com.dji.sample.manage.model.dto.UserLoginDTO;
import com.dji.sample.manage.service.IUserService; import com.dji.sample.manage.service.IUserService;
...@@ -25,11 +26,15 @@ public class LoginController { ...@@ -25,11 +26,15 @@ public class LoginController {
private IUserService userService; private IUserService userService;
@PostMapping("/login") @PostMapping("/login")
public HttpResultResponse login(@RequestBody UserLoginDTO loginDTO) { public HttpResultResponse login(@RequestBody UserLoginDTO loginDTO, HttpServletRequest request) {
// Get request info for logging
String ipAddress = IPUtils.getIpAddr(request);
String userAgent = request.getHeader("User-Agent");
loginDTO.setIpAddress(ipAddress);
loginDTO.setUserAgent(userAgent);
String username = loginDTO.getUsername();
String password = loginDTO.getPassword();
// return userService.userLogin(username, password, loginDTO.getFlag());
return userService.userLogin(loginDTO); return userService.userLogin(loginDTO);
} }
......
package com.dji.sample.manage.controller;
import com.dji.sample.common.util.SecurityUtils;
import com.dji.sample.manage.model.dto.OperateRecordDTO;
import com.dji.sample.manage.service.IOperateRecordService;
import com.dji.sdk.common.HttpResultResponse;
import com.dji.sdk.common.PaginationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* REST API for querying operation records from the {@code operate_record} table.
*
* <p>Access control:
* <ul>
* <li>ADMIN and above — can query all users' records; may also pass {@code user_id} to filter by a specific user.</li>
* <li>Regular users — can only see their own records; {@code user_id} param is ignored and replaced with the caller's own ID.</li>
* </ul>
*/
@RestController
@RequestMapping("${url.manage.prefix}${url.manage.version}/operateRecord")
public class OperateRecordController {
@Autowired
private IOperateRecordService operateRecordService;
/**
* Paginated query of operation records.
*
* @param userId filter by user ID (admins only; ignored for regular users)
* @param dockSn filter by dock SN (optional)
* @param operatingType filter by operation type, e.g. FLIGHT_TASK_CREATE (optional)
* @param startTime filter start time, Unix ms (optional)
* @param endTime filter end time, Unix ms (optional)
* @param page page number, default 1
* @param pageSize page size, default 20
* @return paginated list of operation records
*/
@GetMapping("/list")
public HttpResultResponse<PaginationData<OperateRecordDTO>> list(
@RequestParam(name = "user_id", required = false) String userId,
@RequestParam(required = false) String dockSn,
@RequestParam(name = "operating_type", required = false) String operatingType,
@RequestParam(name = "start_time", required = false) Long startTime,
@RequestParam(name = "end_time", required = false) Long endTime,
@RequestParam(defaultValue = "1") int page,
@RequestParam(name = "page_size", defaultValue = "20") int pageSize) {
String workspaceId = SecurityUtils.getWorkspaceId();
// Admins can filter by any user; regular users are restricted to their own records.
String effectiveUserId = SecurityUtils.aboveAdminRole() ? userId : SecurityUtils.getUserId();
PaginationData<OperateRecordDTO> data = operateRecordService.page(
workspaceId, effectiveUserId, dockSn, operatingType, startTime, endTime, page, pageSize);
return HttpResultResponse.success(data);
}
}
package com.dji.sample.manage.controller; package com.dji.sample.manage.controller;
import com.dji.sample.common.error.CommonErrorEnum;
import com.dji.sample.common.model.CustomClaim; import com.dji.sample.common.model.CustomClaim;
import com.dji.sample.common.util.SecurityUtils;
import com.dji.sample.manage.model.dto.UserListDTO; import com.dji.sample.manage.model.dto.UserListDTO;
import com.dji.sample.manage.model.entity.UserEntity; import com.dji.sample.manage.model.entity.UserEntity;
import com.dji.sample.manage.model.param.ChangePasswordParam;
import com.dji.sample.manage.model.param.searchParam.LoginLogSearchParam;
import com.dji.sample.manage.model.param.searchParam.UserSearchParam; import com.dji.sample.manage.model.param.searchParam.UserSearchParam;
import com.dji.sample.manage.service.IUserService; import com.dji.sample.manage.service.IUserService;
import com.dji.sdk.common.HttpResultResponse; import com.dji.sdk.common.HttpResultResponse;
import com.dji.sdk.common.PaginationData; import com.dji.sdk.common.PaginationData;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
...@@ -104,4 +109,87 @@ public class UserController { ...@@ -104,4 +109,87 @@ public class UserController {
return userService.getUserByUserId(userId, workspaceId); return userService.getUserByUserId(userId, workspaceId);
} }
/**
* 获取用户登录日志
*
* @param param 查询参数
* @param page 第几页
* @param pageSize 每页大小
* @return 返回结果
*/
@PostMapping("/login_log")
public HttpResultResponse<Object> loginLog(
@RequestBody(required = false) LoginLogSearchParam param,
@RequestParam(defaultValue = "1") int page,
@RequestParam(value = "page_size", defaultValue = "50") int pageSize
) {
String userId = SecurityUtils.getUserId();
if (StringUtils.isBlank(userId)) {
throw new RuntimeException(CommonErrorEnum.NO_TOKEN.getMessage());
}
Integer roleType = SecurityUtils.getRoleType();
return HttpResultResponse.success(userService.getUserLoginLog(param, userId, roleType, page, pageSize));
}
/**
* Current user changes their own password.
* Requires: oldPassword, newPassword, confirmPassword.
*
* @param request HTTP request
* @param param change-password parameters
* @return result
*/
@PostMapping("/changePassword")
public HttpResultResponse<Object> changePassword(HttpServletRequest request,
@RequestBody ChangePasswordParam param) {
CustomClaim customClaim = (CustomClaim) request.getAttribute(TOKEN_CLAIM);
return userService.changePassword(customClaim.getId(), param);
}
/**
* Admin resets a user's password.
* The new password must comply with all password rules.
*
* @param request HTTP request
* @param param reset-password parameters containing newPassword
* @return result
*/
@PostMapping("/resetPassword")
public HttpResultResponse<Object> resetPassword(HttpServletRequest request,
@RequestBody ChangePasswordParam param) {
CustomClaim customClaim = (CustomClaim) request.getAttribute(TOKEN_CLAIM);
String userId = customClaim.getId();
return userService.resetPassword(userId, param);
}
/**
* Admin changes the password of a specific user.
* Requires admin role. The new password must comply with all password rules.
*
* @param param parameters containing userId and newPassword
* @return result
*/
@PostMapping("/changePasswordByAdmin")
public HttpResultResponse<Object> changePasswordByAdmin(@RequestBody ChangePasswordParam param) {
return userService.changePasswordByAdmin(param.getUserId(), param);
}
/**
* Check whether the current user's password has expired.
*
* @param request HTTP request
* @return true/false
*/
@GetMapping("/passwordExpired")
public HttpResultResponse<Boolean> passwordExpired(HttpServletRequest request) {
CustomClaim customClaim = (CustomClaim) request.getAttribute(TOKEN_CLAIM);
boolean expired = userService.isPasswordExpired(customClaim.getId());
return HttpResultResponse.success(expired);
}
} }
package com.dji.sample.manage.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dji.sample.manage.model.entity.UserPasswordHistoryEntity;
public interface IUserPasswordHistoryMapper extends BaseMapper<UserPasswordHistoryEntity> {
}
...@@ -44,4 +44,23 @@ public class UserDTO { ...@@ -44,4 +44,23 @@ public class UserDTO {
@JsonProperty("org_logo") @JsonProperty("org_logo")
protected String orgLogo; protected String orgLogo;
/**
* 是否新增用户
*/
@JsonProperty("new_add")
private Boolean newAdd;
/**
* 是否需要修改密码
*/
@JsonProperty("need_change_password")
private Long needChangePassword;
/**
* Whether the password has expired (including first-time login).
* Front-end should redirect to the change-password page when true.
*/
@JsonProperty("password_expired")
private Boolean passwordExpired;
} }
...@@ -27,6 +27,13 @@ public class UserLoginDTO { ...@@ -27,6 +27,13 @@ public class UserLoginDTO {
private String orgName; private String orgName;
private String ipAddress;
/**
* 浏览器的用户代理信息
*/
private String userAgent;
/** /**
* 游客登录时间戳 * 游客登录时间戳
*/ */
......
...@@ -42,6 +42,14 @@ public class UserEntity implements Serializable { ...@@ -42,6 +42,14 @@ public class UserEntity implements Serializable {
@TableField(value = "role_type") @TableField(value = "role_type")
private Integer roleType; private Integer roleType;
@TableField(value = "phone_number")
private String phoneNumber;
@TableField(value = "aop_file")
private String aopFile;
private String email;
@TableField(value = "login_try_count") @TableField(value = "login_try_count")
private Integer loginTryCount; private Integer loginTryCount;
......
package com.dji.sample.manage.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
@TableName(value = "manage_user_password_history")
@Data
public class UserPasswordHistoryEntity implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(value = "user_id")
private String userId;
@TableField(value = "password")
private String password;
@TableField(value = "workspace_id")
private String workspaceId;
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Long createTime;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private Long updateTime;
}
package com.dji.sample.manage.model.param;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class ChangePasswordParam {
/**
* Target user id – required when an admin changes another user's password.
* Not required when a user changes their own password.
*/
@JsonProperty("user_id")
private String userId;
/**
* Current (old) password – required when user changes their own password.
* Not required when an admin resets another user's password.
*/
@JsonProperty("old_password")
private String oldPassword;
/**
* New password in plain text.
*/
@JsonProperty("new_password")
private String newPassword;
}
package com.dji.sample.manage.model.password;
/**
* Strategy interface for a single password validation rule.
* <p>
* Each implementation encapsulates one specific rule (length, character categories,
* weak-password list, etc.) and returns a {@link PasswordRuleResult} indicating
* whether the password passes the rule and, on failure, an explanatory message.
* </p>
* <p>
* Implementations are registered as Spring components and collected by
* {@link com.dji.sample.manage.service.password.PasswordRuleChain}.
* </p>
*/
public interface PasswordRule {
/**
* Validate the given password against this rule.
*
* @param password plain-text password to validate (never {@code null})
* @param context optional user context; may be {@code null} for anonymous checks
* @return {@link PasswordRuleResult#ok()} on success, or
* {@link PasswordRuleResult#fail(String)} with a human-readable reason
*/
PasswordRuleResult validate(String password, PasswordRuleContext context);
/**
* Display name of this rule — used for logging and management UI.
*
* @return rule name
*/
String getRuleName();
}
package com.dji.sample.manage.model.password;
import lombok.Builder;
import lombok.Getter;
/**
* Carries the contextual information needed by password rules that perform
* user-specific checks (e.g. similarity to username / phone number).
* Pass {@code null} when no user context is available (e.g. during initial setup).
*/
@Getter
@Builder
public class PasswordRuleContext {
/** The user's login name. May be {@code null}. */
private final String username;
/** The user's phone number (may contain non-digit characters). May be {@code null}. */
private final String phoneNumber;
}
package com.dji.sample.manage.model.password;
import lombok.Getter;
/**
* Immutable result returned by a {@link PasswordRule}.
*/
@Getter
public final class PasswordRuleResult {
private final boolean valid;
/** Human-readable failure reason; {@code null} when {@link #valid} is {@code true}. */
private final String reason;
private PasswordRuleResult(boolean valid, String reason) {
this.valid = valid;
this.reason = reason;
}
/** Factory: rule passed. */
public static PasswordRuleResult ok() {
return new PasswordRuleResult(true, null);
}
/** Factory: rule failed with the supplied reason message. */
public static PasswordRuleResult fail(String reason) {
return new PasswordRuleResult(false, reason);
}
}
package com.dji.sample.manage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.dji.sample.manage.model.entity.UserPasswordHistoryEntity;
public interface IUserPasswordHistoryService extends IService<UserPasswordHistoryEntity> {
/**
* Save a new password record to history.
* Keeps only the most recent {@code maxHistory} records per user.
*
* @param userId user uuid
* @param workspaceId workspace id
* @param encodedPassword BCrypt-encoded password to record
* @param maxHistory maximum number of records to retain (e.g. 8)
*/
void savePasswordHistory(String userId, String workspaceId, String encodedPassword, int maxHistory);
/**
* Check whether the plain-text password matches any of the recent {@code maxHistory} history records.
*
* @param userId user uuid
* @param rawPassword plain-text password to check
* @param maxHistory number of history records to check
* @return true if the password was recently used
*/
boolean isPasswordRecentlyUsed(String userId, String rawPassword, int maxHistory);
}
...@@ -6,6 +6,7 @@ import com.dji.sample.manage.model.dto.UserDTO; ...@@ -6,6 +6,7 @@ import com.dji.sample.manage.model.dto.UserDTO;
import com.dji.sample.manage.model.dto.UserListDTO; import com.dji.sample.manage.model.dto.UserListDTO;
import com.dji.sample.manage.model.dto.UserLoginDTO; import com.dji.sample.manage.model.dto.UserLoginDTO;
import com.dji.sample.manage.model.entity.UserEntity; import com.dji.sample.manage.model.entity.UserEntity;
import com.dji.sample.manage.model.param.ChangePasswordParam;
import com.dji.sample.manage.model.param.searchParam.LoginLogSearchParam; import com.dji.sample.manage.model.param.searchParam.LoginLogSearchParam;
import com.dji.sample.manage.model.param.searchParam.UserSearchParam; import com.dji.sample.manage.model.param.searchParam.UserSearchParam;
import com.dji.sdk.common.HttpResultResponse; import com.dji.sdk.common.HttpResultResponse;
...@@ -23,6 +24,8 @@ public interface IUserService extends IService<UserEntity> { ...@@ -23,6 +24,8 @@ public interface IUserService extends IService<UserEntity> {
*/ */
HttpResultResponse getUserByUsername(String username, String workspaceId); HttpResultResponse getUserByUsername(String username, String workspaceId);
/** /**
* get user login log * get user login log
* *
...@@ -96,4 +99,42 @@ public interface IUserService extends IService<UserEntity> { ...@@ -96,4 +99,42 @@ public interface IUserService extends IService<UserEntity> {
UserEntity addOrgAdminUser(UserEntity user); UserEntity addOrgAdminUser(UserEntity user);
/**
* Change the current user's own password.
* Enforces: old-password verification, strength rules, history check.
*
* @param userId current user id (from token)
* @param param ChangePasswordParam containing oldPassword / newPassword / confirmPassword
* @return result
*/
HttpResultResponse<Object> changePassword(String userId, ChangePasswordParam param);
/**
* Admin resets another user's password.
* Enforces: strength rules, history check.
*
* @param targetUserId user whose password will be reset
* @param param ChangePasswordParam containing newPassword
* @return result
*/
HttpResultResponse<Object> resetPassword(String targetUserId, ChangePasswordParam param);
/**
* Admin changes a specific user's password directly.
* Requires admin role. Enforces: strength rules, history check.
*
* @param targetUserId user whose password will be changed
* @param param ChangePasswordParam containing newPassword
* @return result
*/
HttpResultResponse<Object> changePasswordByAdmin(String targetUserId, ChangePasswordParam param);
/**
* Check whether the given user's password has expired.
*
* @param userId user id
* @return true if password is expired
*/
boolean isPasswordExpired(String userId);
} }
package com.dji.sample.manage.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dji.sample.common.util.SecurityUtils;
import com.dji.sample.manage.dao.IUserPasswordHistoryMapper;
import com.dji.sample.manage.model.entity.UserPasswordHistoryEntity;
import com.dji.sample.manage.service.IUserPasswordHistoryService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class UserPasswordHistoryServiceImpl
extends ServiceImpl<IUserPasswordHistoryMapper, UserPasswordHistoryEntity>
implements IUserPasswordHistoryService {
@Override
public void savePasswordHistory(String userId, String workspaceId, String encodedPassword, int maxHistory) {
// Insert new record
UserPasswordHistoryEntity record = new UserPasswordHistoryEntity();
record.setUserId(userId);
record.setWorkspaceId(workspaceId);
record.setPassword(encodedPassword);
this.save(record);
// Prune old records – keep only the most recent maxHistory entries
LambdaQueryWrapper<UserPasswordHistoryEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserPasswordHistoryEntity::getUserId, userId)
.orderByAsc(UserPasswordHistoryEntity::getCreateTime);
List<UserPasswordHistoryEntity> history = this.list(wrapper);
if (history.size() > maxHistory) {
int toDelete = history.size() - maxHistory;
// oldest records come first due to orderByAsc
for (int i = 0; i < toDelete; i++) {
this.removeById(history.get(i).getId());
}
}
}
@Override
public boolean isPasswordRecentlyUsed(String userId, String rawPassword, int maxHistory) {
LambdaQueryWrapper<UserPasswordHistoryEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserPasswordHistoryEntity::getUserId, userId)
.orderByDesc(UserPasswordHistoryEntity::getCreateTime)
.last("LIMIT " + maxHistory);
List<UserPasswordHistoryEntity> history = this.list(wrapper);
return history.stream()
.anyMatch(h -> SecurityUtils.matchesPassword(rawPassword, h.getPassword()));
}
}
package com.dji.sample.manage.service.password;
import com.dji.sample.manage.model.password.PasswordRule;
import com.dji.sample.manage.model.password.PasswordRuleContext;
import com.dji.sample.manage.model.password.PasswordRuleResult;
/**
* Rule 2 – Character-category combination.
* Password must contain at least {@value #MIN_CATEGORIES} of the following 4 types:
* uppercase letters (A-Z), lowercase letters (a-z), digits (0-9), special characters.
*/
public class CharacterCategoryPasswordRule implements PasswordRule {
/** Minimum number of distinct character categories required. */
public static final int MIN_CATEGORIES = 3;
@Override
public PasswordRuleResult validate(String password, PasswordRuleContext context) {
boolean hasUpper = password.chars().anyMatch(Character::isUpperCase);
boolean hasLower = password.chars().anyMatch(Character::isLowerCase);
boolean hasDigit = password.chars().anyMatch(Character::isDigit);
boolean hasSpecial = password.chars().anyMatch(c -> !Character.isLetterOrDigit(c));
int count = (hasUpper ? 1 : 0) + (hasLower ? 1 : 0)
+ (hasDigit ? 1 : 0) + (hasSpecial ? 1 : 0);
if (count < MIN_CATEGORIES) {
return PasswordRuleResult.fail(
"Password must contain at least " + MIN_CATEGORIES + " of the following 4 types: "
+ "uppercase letters (A-Z), lowercase letters (a-z), digits (0-9), special characters (!@#$%...)");
}
return PasswordRuleResult.ok();
}
@Override
public String getRuleName() {
return "CharacterCategory";
}
}
package com.dji.sample.manage.service.password;
import com.dji.sample.manage.model.password.PasswordRule;
import com.dji.sample.manage.model.password.PasswordRuleContext;
import com.dji.sample.manage.model.password.PasswordRuleResult;
/**
* Rule 1 – Minimum length.
* Password must be at least {@value #MIN_LENGTH} characters long.
*/
public class MinLengthPasswordRule implements PasswordRule {
public static final int MIN_LENGTH = 8;
@Override
public PasswordRuleResult validate(String password, PasswordRuleContext context) {
if (password == null || password.length() < MIN_LENGTH) {
return PasswordRuleResult.fail(
"Password must be at least " + MIN_LENGTH + " characters long");
}
return PasswordRuleResult.ok();
}
@Override
public String getRuleName() {
return "MinLength";
}
}
package com.dji.sample.manage.service.password;
import com.dji.sample.manage.model.password.PasswordRule;
import com.dji.sample.manage.model.password.PasswordRuleContext;
import com.dji.sample.manage.model.password.PasswordRuleResult;
/**
* Rule 4 – No consecutive repeated characters.
* Password must NOT contain {@value #MAX_REP_LEN} or more identical characters in a row
* (e.g. {@code aaaa}, {@code 1111}, {@code 111222}).
*/
public class NoRepeatedCharsPasswordRule implements PasswordRule {
/** Maximum allowed run of identical consecutive characters (exclusive). */
public static final int MAX_REP_LEN = 4;
@Override
public PasswordRuleResult validate(String password, PasswordRuleContext context) {
int repLen = 1;
for (int i = 1; i < password.length(); i++) {
if (password.charAt(i) == password.charAt(i - 1)) {
repLen++;
if (repLen >= MAX_REP_LEN) {
return PasswordRuleResult.fail(
"Password must not contain " + MAX_REP_LEN
+ " or more repeated identical characters (e.g. aaaa, 1111)");
}
} else {
repLen = 1;
}
}
return PasswordRuleResult.ok();
}
@Override
public String getRuleName() {
return "NoRepeatedChars";
}
}
package com.dji.sample.manage.service.password;
import com.dji.sample.manage.model.password.PasswordRule;
import com.dji.sample.manage.model.password.PasswordRuleContext;
import com.dji.sample.manage.model.password.PasswordRuleResult;
/**
* Rule 3 – No consecutive sequential characters.
* Password must NOT contain {@value #MAX_SEQ_LEN} or more characters that are
* consecutively increasing or decreasing in ASCII value (e.g. {@code 1234}, {@code abcd}, {@code dcba}).
*/
public class NoSequentialCharsPasswordRule implements PasswordRule {
/** Maximum allowed sequential-character run length (exclusive). */
public static final int MAX_SEQ_LEN = 4;
@Override
public PasswordRuleResult validate(String password, PasswordRuleContext context) {
int seqLen = 1;
for (int i = 1; i < password.length(); i++) {
int diff = password.charAt(i) - password.charAt(i - 1);
if (diff == 1 || diff == -1) {
seqLen++;
if (seqLen >= MAX_SEQ_LEN) {
return PasswordRuleResult.fail(
"Password must not contain " + MAX_SEQ_LEN
+ " or more consecutive sequential characters (e.g. 1234, abcd, dcba)");
}
} else {
seqLen = 1;
}
}
return PasswordRuleResult.ok();
}
@Override
public String getRuleName() {
return "NoSequentialChars";
}
}
package com.dji.sample.manage.service.password;
import com.dji.sample.manage.model.password.PasswordRule;
import com.dji.sample.manage.model.password.PasswordRuleContext;
import com.dji.sample.manage.model.password.PasswordRuleResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
/**
* Orchestrates all registered {@link PasswordRule} implementations in {@link org.springframework.core.annotation.Order} sequence.
* <p>
* Spring auto-collects every {@code PasswordRule} bean and sorts them by {@code @Order}.
* To add, remove, or reorder a rule, simply create/delete/annotate the corresponding
* {@code @Component} — no changes to this class are needed.
* </p>
*
* <h3>Usage</h3>
* <pre>{@code
* PasswordRuleContext ctx = PasswordRuleContext.builder()
* .username(user.getUsername())
* .phoneNumber(user.getPhoneNumber())
* .build();
* PasswordRuleResult result = passwordRuleChain.validate(rawPassword, ctx);
* if (!result.isValid()) {
* throw new RuntimeException(result.getReason());
* }
* }</pre>
*/
@Slf4j
@Component
public class PasswordRuleChain {
private List<PasswordRule> rules;
/**
* Spring injects all {@link PasswordRule} beans ordered by {@code @Order}.
*/
public PasswordRuleChain() {
this.rules = new ArrayList<>();
// 1. 密码至少8位
this.rules.add(new MinLengthPasswordRule());
// 2. 组合规则:必须同时包含以下4 类字符中的至少 3 类 大写英文字母,小写英文字母,数字,特殊符号
// this.rules.add(new CharacterCategoryPasswordRule());
log.info("PasswordRuleChain initialized with {} rules",
rules.size());
}
/**
* Generate a random password that satisfies all registered rules.
* The generated password always contains at least one character from each of the
* 4 categories (uppercase, lowercase, digit, special) and is then shuffled,
* guaranteeing the {@link CharacterCategoryPasswordRule} passes.
* Length is fixed at {@value #GENERATED_PASSWORD_LENGTH} characters.
*
* @return a rule-compliant random plain-text password
*/
public String generatePassword() {
SecureRandom random = new SecureRandom();
// Guarantee at least one character from each category
char[] upper = "ABCDEFGHJKLMNPQRSTUVWXYZ".toCharArray();
char[] lower = "abcdefghjkmnpqrstuvwxyz".toCharArray();
char[] digits = "23456789".toCharArray();
char[] special = "!@#$%^&*".toCharArray();
char[] all = (new String(upper) + new String(lower)
+ new String(digits) + new String(special)).toCharArray();
StringBuilder sb = new StringBuilder(GENERATED_PASSWORD_LENGTH);
// One guaranteed character from each category
sb.append(upper [random.nextInt(upper.length)]);
sb.append(lower [random.nextInt(lower.length)]);
sb.append(digits [random.nextInt(digits.length)]);
sb.append(special[random.nextInt(special.length)]);
// Fill the remainder from all categories
for (int i = 4; i < GENERATED_PASSWORD_LENGTH; i++) {
sb.append(all[random.nextInt(all.length)]);
}
// Shuffle to avoid predictable category positions
char[] pwd = sb.toString().toCharArray();
for (int i = pwd.length - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
char tmp = pwd[i]; pwd[i] = pwd[j]; pwd[j] = tmp;
}
return new String(pwd);
}
/** Length of system-generated passwords. */
private static final int GENERATED_PASSWORD_LENGTH = 12;
/**
* Run the password through every rule in order.
* Returns the first failure encountered, or {@link PasswordRuleResult#ok()} if all rules pass.
*
* @param password plain-text password; must not be {@code null} or blank
* @param context optional user context for personal-info checks
* @return first failing {@link PasswordRuleResult}, or ok
*/
public PasswordRuleResult validate(String password, PasswordRuleContext context) {
if (!StringUtils.hasText(password)) {
return PasswordRuleResult.fail("Password must not be empty");
}
for (PasswordRule rule : rules) {
PasswordRuleResult result = rule.validate(password, context);
if (!result.isValid()) {
log.debug("Password failed rule [{}]: {}", rule.getRuleName(), result.getReason());
return result;
}
}
return PasswordRuleResult.ok();
}
}
package com.dji.sample.manage.service.password;
import com.dji.sample.manage.model.password.PasswordRule;
import com.dji.sample.manage.model.password.PasswordRuleContext;
import com.dji.sample.manage.model.password.PasswordRuleResult;
import org.springframework.util.StringUtils;
/**
* Rule 6 – Similarity to personal information.
* Password must NOT be similar to the user's username or contain the last 6 digits of their phone number.
* Skipped when {@link PasswordRuleContext} is {@code null} or the relevant fields are blank.
*/
public class UserSimilarityPasswordRule implements PasswordRule {
@Override
public PasswordRuleResult validate(String password, PasswordRuleContext context) {
if (context == null) {
return PasswordRuleResult.ok();
}
String lower = password.toLowerCase();
// Check username similarity
if (StringUtils.hasText(context.getUsername())) {
String uname = context.getUsername().toLowerCase();
if (lower.contains(uname) || uname.contains(lower)) {
return PasswordRuleResult.fail("Password must not be similar to your username");
}
}
// Check phone number: forbid last 6 digits of phone appearing in the password
if (StringUtils.hasText(context.getPhoneNumber())) {
String phone = context.getPhoneNumber().replaceAll("\\D", "");
if (phone.length() >= 6 && password.contains(phone.substring(phone.length() - 6))) {
return PasswordRuleResult.fail("Password must not contain your phone number");
}
}
return PasswordRuleResult.ok();
}
@Override
public String getRuleName() {
return "UserSimilarity";
}
}
package com.dji.sample.manage.service.password;
import com.dji.sample.manage.model.password.PasswordRule;
import com.dji.sample.manage.model.password.PasswordRuleContext;
import com.dji.sample.manage.model.password.PasswordRuleResult;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Rule 5 – Weak-password blacklist.
* Password must NOT appear in the list of publicly known / commonly guessed passwords.
*/
public class WeakPasswordRule implements PasswordRule {
private static final Set<String> WEAK_PASSWORDS = new HashSet<>(Arrays.asList(
"password", "password1", "password123", "passw0rd",
"admin", "admin123", "administrator",
"12345678", "123456789", "1234567890",
"qwerty", "qwerty123", "qwertyuiop",
"abc123", "abcd1234",
"iloveyou", "welcome", "login", "master",
"111111", "222222", "666666", "888888", "123123"
));
@Override
public PasswordRuleResult validate(String password, PasswordRuleContext context) {
if (WEAK_PASSWORDS.contains(password.toLowerCase())) {
return PasswordRuleResult.fail(
"Password is too common or weak. Please choose a more complex password");
}
return PasswordRuleResult.ok();
}
@Override
public String getRuleName() {
return "WeakPasswordBlacklist";
}
}
server:
port: 6789
my:
file:
base-path: /app/filePath
spring:
main:
allow-bean-definition-overriding: true
application:
# name: cloud-api-sample
name: geoair-api
datasource:
druid:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud_sample?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: root
initial-size: 10
min-idle: 10
max-active: 20
max-wait: 60000
redis:
# host: geoair_redis
# host: cloud_api_sample_redis
# 深圳
host: localhost
port: 6379
database: 0
username: # if you enable
password:
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
servlet:
multipart:
max-file-size: 2GB
max-request-size: 2GB
# 邮箱
mail:
host: smtpdm.aliyun.com #阿里云发送服务器地址
port: 25 #端口号
username: geofly@stmp-geofly.geotwin.cc #发送人地址
# password: ENC(Grg2n2TYzgJv9zpwufsf37ndTe+M1cYk) #密码
password: GeoSys2326 #密码
jwt:
issuer: DJI
subject: CloudApiSample
secret: CloudApiSample
age: 86400
mqtt:
# @see com.dji.sample.component.mqtt.model.MqttUseEnum
# BASIC parameters are required.
NET:
# protocol: MQTT # @see com.dji.sample.component.mqtt.model.MqttProtocolEnum
# host: 183.11.236.162
# port: 54418
# username: JavaServer
# password: 123456
# client-id: 123456
protocol: WSS
host: geofly.geotwin.cn
port: 443
path: /mqtt
username: JavaServer
password: 123456
BASIC:
protocol: MQTT # @see com.dji.sample.component.mqtt.model.MqttProtocolEnum
# 深圳
# host: 183.11.236.162
# port: 54418
# host: 203.186.109.106
# host: emqx-broker
# host: 192.168.32.90
# port: 44418
# host: 203.186.109.106
# port: 54941
host: localhost
port: 1883
username: JavaServer
password: 123456
client-id: 123456
# If the protocol is ws/wss, this value is required.
path:
DRC:
# 深圳
protocol: WSS
host: geofly.geotwin.cn
port: 443
# protocol: WSS # @see com.dji.sample.component.mqtt.model.MqttProtocolEnum
# host: emqx-broker
# host: 192.168.32.90
# port: 8083
# host: geofly-dev.geotwin.cc
# host: geofly.geotwin.cc
# port: 443
path: /mqtt
username: JavaServer
password: 123456
cloud-sdk:
mqtt:
# Topics that need to be subscribed when initially connecting to mqtt, multiple topics are divided by ",".
inbound-topic: sys/product/+/status,thing/product/+/requests,thing/product/+/osd,/ai_info
url:
manage:
prefix: manage
version: /api/v1
map:
prefix: map
version: /api/v1
media:
prefix: media
version: /api/v1
wayline:
prefix: wayline
version: /api/v1
storage:
prefix: storage
version: /api/v1
control:
prefix: control
version: /api/v1
psdk:
prefix: psdk
version: /api/v1
# Tutorial: https://www.alibabacloud.com/help/en/object-storage-service/latest/use-a-temporary-credential-provided-by-sts-to-access-oss
#oss:
# enable: false
# provider: ALIYUN # @see com.dji.sample.component.OssConfiguration.model.enums.OssTypeEnum
# endpoint: https://oss-cn-hangzhou.aliyuncs.com
# access-key: Please enter your access key.
# secret-key: Please enter your secret key.
# expire: 3600
# region: Please enter your oss region. # cn-hangzhou
# role-session-name: cloudApi
# role-arn: Please enter your role arn. # acs:ram::123456789:role/stsrole
# bucket: Please enter your bucket name.
# object-dir-prefix: Please enter a folder name.
#oss:
# enable: true
# provider: aws
# endpoint: https://s3.us-east-1.amazonaws.com
# access-key:
# secret-key:
# expire: 3600
# region: us-east-1
# role-session-name: cloudApi
# role-arn:
# bucket: cloudapi-bucket
# object-dir-prefix: wayline
oss:
enable: true
provider: minio
# 香港
# endpoint: https://gt7-oss.geotwin.cc
# 深圳
endpoint: https://gt-oss-dev.geotwin.cn
access-key: minioadmin
secret-key: minioadmin
bucket: gtfly
expire: 86400 # 24 * 3600 24小时
region: cn-guangzhou # us-east-1
object-dir-prefix: wayline
logging:
level:
com.dji: debug
file:
name: logs/cloud-api-sample.log
ntp:
server:
host: Google.mzr.me
# To create a license for an application: https://developer.dji.com/user/apps/#all
cloud-api:
app:
id: 163614
key: 09e2fc9ac2ade8607d07826b3c1a822
license: fvwK+dVYFBUwxGUxXS3EglmsuTbyFA6400LGuqLrmebHmnhLL9U5DPTI0l4Wu49Xc6fdmNjP6e7ZzHqEhhwloVvhSn4tKMc9wAIsTLlHz1r/j7YbHo5xDX2LHLJOss1e8ud8Gl2lC3gfo9Dsc+qZiCfBX/uIeeZUgk/rR8m1erE=
# 旧配置
# id: 158488
# key: 15e3972c29ef16b9eda5e3415d943d7
# license: el2GS5tHPIEjXazwGZC1LkIEQ3B43U7uQ9x30ateJAACX1miOSOdjKLn0clBJTLKO3Fd/EnrlNnVwoPOmYvn9K6skalfLgY3bBv5I6sJq0nzaIGpdCHelNA/81GCvb9j1sWdN3adRIk/v5GtmSswauqElmgBGN/r37yp7pi5b5E=
# id: 158487
# key: 11de4b6e406e51bf78c75c4cabefe09
# license: MQzV5RtcqtuZuPlP6MAq0/1xF7gD2ujuN+0tmh+rDzOb7xECeIcRoyGItLcicTGWu3OkunX4XB8/7RcUp5yjyZhyvjHuqhkRDxSwfToidln8mRJ9UvltcSgKRCYCYnR/Oa6u3u0bs2azz20t6zCKmfdAIY0GzHI7YqgmOHowBXU=
livestream:
url:
# It is recommended to use a program to create Token. https://github.com/AgoraIO/Tools/blob/master/DynamicKey/AgoraDynamicKey/java/src/main/java/io/agora/media/RtcTokenBuilder2.java
agora:
channel: Please enter the agora channel.
token: Please enter the agora temporary token.
uid: 654321
# RTMP Note: This IP is the address of the streaming server. If you want to see livestream on web page, you need to convert the RTMP stream to WebRTC stream.
rtmp:
# url: rtmp://203.186.109.106:44424/live/ # Example: 'rtmp://192.168.1.1/live/'
# 深圳
# url: rtmp://183.11.236.162:54424/live/ # Example: 'rtmp://192.168.1.1/live/'
url: rtmp://183.11.236.162:54460/live/ # Example: 'rtmp://192.168.1.1/live/'
rtsp:
username: Please enter the username.
password: Please enter the password.
port: 8554
# GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters.
gb28181:
serverIP: Please enter the server ip.
serverPort: 0
serverID: Please enter the server id.
agentID: Please enter the agent id.
agentPassword: Please enter the agent password.
localPort: 0
channel: Please enter the channel.
# Webrtc: Only supports using whip standard
whip:
url: Please enter the rtmp access address. # Example:http://192.168.1.1:1985/rtc/v1/whip/?app=live&stream=
uom:
# 数据源 Integer
# 1:无人机系统直报的数据
# 2:无人机制造商自建的无人机运行服务系统代报的数据
# 3:无人机机体上加装的单独数据模块代报的数据
# 4:采集设备采集代报的广播数据
source: 2
# 识别信息报送平台
# 报送平台的名称,如“DJI FLY”
platform: "GEO FLY"
# 报送程序版本
# 程序版本号,以第一次完成报送对接记为“version1.0”,每次升级报送程序累加版本号后缀
programVersion: "version1.0"
# 上报平台 API 地址 https://uom.receive.caacic.cn/addFlightRoute
# url: https://218.189.32.212:8080/addFlightRoute
url: https://220.232.168.7:8080/addFlightRoute
appID: GEOSYS
appKey: geosys_uas
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment