我正在尝试在我的 Spring Boot/React 应用程序中实现密码重置/忘记密码功能,但我在尝试完全理解它时遇到了一些问题,我不确定我所采取的方法是否正确。
我已经可以注册/登录了
@PostMapping("/sendForgotPasswordEmail")
public ResponseEntity<?> sendForgotPasswordEmail(@Valid @RequestBody ForgotPasswordEmailRequest request) {
User user = userService.findUserByEmailAddress(request.getForgotPasswordEmail());
ResetPasswordToken resetPasswordToken = userService.createResetPasswordToken(user);
if(userService.existsByEmailAddress(request.getForgotPasswordEmail())) {
emailService.sendResetPasswordEmail(request.getForgotPasswordEmail(), resetPasswordToken.getToken());
return ResponseEntity.ok(new ApiResponse("Please check your email for a password reset link" , true));
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
}
@PostMapping("/resetPassword")
public ResponseEntity<?> resetPassword(@RequestParam("token") String token, @Valid @RequestBody ForgotPasswordEmailRequest request) {
ResetPasswordToken resetPasswordToken = userService.findByResetPasswordToken(token);
if(resetPasswordToken == null) throw new BadRequestException("Invalid token");
Calendar calendar = Calendar.getInstance();
if((resetPasswordToken.getExpiredAt().getTime() - calendar.getTime().getTime()) <= 0) {
throw new BadRequestException("Link expired. Generate new link from http://localhost:3000/signup");
}
userService.resetPassword(request.getForgotPasswordEmail(), request.getNewPassword());
return ResponseEntity.ok(token);
}
UserServiceImpl.java(重置令牌相关方法)
@Override
public ResetPasswordToken createResetPasswordToken(User user) {
ResetPasswordToken resetPasswordToken = new ResetPasswordToken(user);
return resetPasswordTokenRepository.save(resetPasswordToken);
}
@Override
public ResetPasswordToken findByResetPasswordToken(String token) {
return resetPasswordTokenRepository.findByToken(token);
}
@Override
public void resetPassword(String emailAddress, String password) {
User user = findUserByEmailAddress(emailAddress);
user.setPassword(passwordEncoder.encode(password));
}
忘记密码电子邮件请求.java
public class ForgotPasswordEmailRequest {
private String forgotPasswordEmail;
private String newPassword;
// constructors / getters and setters
}
我想弄清楚的是,究竟如何执行此操作,并确保我可以成功地将用户重定向到重置密码页面,并且他们可以从那里更改密码。 我不确定我的控制器方法在哪里丢失了某些东西或者我哪里出了问题。
当用户在收到电子邮件后到达重置密码表单时,前端+后端需要发生什么过程。
我将不胜感激任何帮助。
通常流程是这样的:
我想回答我的解决方案 后端侧 用户输入电子邮件后,第一个前端将触发此方法
@PostMapping(FORGOT_PASSWORD_MAIL)
public ResponseEntity<BaseResponse<Boolean>> forgotPasswordMail(@RequestBody ForgotPasswordRequestDto dto) {
return ResponseEntity.ok(BaseResponse.<Boolean>builder()
.success(true)
.message("Yeni sifre olusturma linki mail adresine gonderilmistir!")
.data(userService.forgotPasswordMail(dto.forgotPasswordEmail()))
.code(200)
.build()
);
}
邮件链接将如下所示:http://localhost:9090/v1/dev/user/new-password?auth=c631bc10057344d28b8a31e5a5bc572f
用户单击链接后,用户将被重定向到新密码前端,链接将如下所示:http://localhost:3000/set-new-password?code=c631bc10057344d28b8a31e5a5bc572f
后端方法会是这样的
@GetMapping(NEW_PASSWORD)
public RedirectView setNewPassword(@RequestParam(name = "auth") String authCode){
Optional<Long> userIdOptional = userAuthVerifyCodeService.findUserIdByAuthCode(authCode);
if (userIdOptional.isEmpty()) {
throw new HRAppException(ErrorType.NOTFOUND_USER);
}
return new RedirectView(ReactApis.NEW_PASSWORD_PAGE + "?code=" + authCode);
}
用户输入新密码后,新密码将与 userId 代码一起发送,并通过从前端获取此后端方法将被触发
@PostMapping(NEW_PASSWORD)
public ResponseEntity<BaseResponse<Boolean>> setNewPassword(@RequestBody @Valid NewPasswordRequestDto dto){
if (!dto.password().equals(dto.rePassword())){
throw new HRAppException(ErrorType.PASSWORD_ERROR);
}
return ResponseEntity.ok(BaseResponse.<Boolean>builder()
.success(true)
.message("Yeni sifre basiriyla olsuturuldu!")
.data(userService.updateUserPassword(dto))
.code(200)
.build()
);
}
这是正面
切片还原
interface IForgotPasswordState {
isForgotPasswordLoading: boolean;
isResetPasswordLoading: boolean;
isSuccess: boolean;
}
const initialState: IForgotPasswordState = {
isForgotPasswordLoading: false,
isResetPasswordLoading: false,
isSuccess: false,
};
export const fetchForgotPassword = createAsyncThunk(
"forgotPassword/fetchForgotPassword",
async (payload: IForgotPasswordRequest) => {
const response = await fetch(apis.userService + "/auth-forgot-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).then((data) => data.json());
return response;
}
);
export const fetchNewPassword = createAsyncThunk(
"forgotPassword/fetchNewPassword",
async (payload: INewPasswordRequest) => {
const response = await fetch(apis.userService + "/new-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).then((data) => data.json());
return response;
}
);
const forgotPasswordSlice = createSlice({
name: "forgotPassword",
initialState: initialState,
reducers: {},
extraReducers: (build) => {
build.addCase(fetchForgotPassword.pending, (state) => {
state.isForgotPasswordLoading = true;
});
build.addCase(
fetchForgotPassword.fulfilled,
(state, action: PayloadAction<IBaseResponse>) => {
state.isForgotPasswordLoading = false;
if (action.payload.code === 200) {
Swal.fire({
icon: "success",
title: action.payload.message,
timer: 3000,
});
}
}
);
build.addCase(fetchNewPassword.pending, (state) => {
state.isResetPasswordLoading = true;
});
build.addCase(
fetchNewPassword.fulfilled,
(state, action: PayloadAction<IBaseResponse>) => {
state.isResetPasswordLoading = false;
if (action.payload.code === 200) {
state.isSuccess = true;
} else {
Swal.fire({
icon: "error",
title: "Hata!",
text: action.payload.message,
});
state.isSuccess = false;
}
}
);
},
});
这是新密码页面
function SetNewPassword() {
const dispatch = useDispatch<hrDispatch>();
const { isSuccess } = hrUseSelector((state) => state.forgotPassword);
const location = useLocation();
const [authCode, setCode] = useState("");
useEffect(() => {
const query = new URLSearchParams(window.location.search);
const codeFromUrl = query.get("code");
if (codeFromUrl) setCode(codeFromUrl);
}, [location.state]);
const [showPassword, setShowPassword] = React.useState(false);
const [showRePassword, setShowRePassword] = React.useState(false);
const [isPasswordEmpty, setIsPasswordEmpty] = useState(false);
const [isWrong, setIsWrong] = useState(false);
const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleClickShowRePassword = () => setShowRePassword((show) => !show);
const handleMouseDownPassword = (
event: React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault();
};
const handleMouseDownRePassword = (
event: React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault();
};
const handleMouseUpPassword = (
event: React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault();
};
const handleMouseUpRePassword = (
event: React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault();
};
const [password, setPassword] = useState("");
const [rePassword, setRePassword] = useState("");
const doResetPassword = () => {
if (password === "" || rePassword === "") {
setIsPasswordEmpty(true);
return;
} else setIsPasswordEmpty(false);
if (password !== rePassword) {
setIsWrong(true);
return;
} else setIsWrong(false);
dispatch(fetchNewPassword({ password, rePassword, authCode }));
if (isSuccess) {
window.location.href = "/login";
}
};
return (
<section className="vh-100 gradient-custom-new-password">
<div className="container h-100">
<div className="row d-flex justify-content-center m-0 p-0 align-items-center h-50">
<div className="col-12 col-md-8 col-lg-6 col-xl-5">
<div className="card-body p-5 text-center">
<div className="mb-md-5 mt-md-4 pb-5 bg-light p-5 rounded-3 border border-light">
<h2 className="fw-bold mb-2">Yeni Sifre</h2>
<p className="text-black-50 mb-5">
Belirlemek istediginiz yeni sifreyi giriniz!
</p>
<div
data-mdb-input-init
className="form-outline form-white mb-4"
>
<input
type="text"
hidden
readOnly
value={authCode}
name="code"
/>
<TextField
placeholder="Sifre"
type={showPassword ? "text" : "password"}
onChange={(e) => {
setPassword(e.target.value);
if (e.target.value === "") {
setIsPasswordEmpty(true);
}
}}
error={isPasswordEmpty}
sx={{ width: "100%" }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label={
showPassword
? "hide the password"
: "display the password"
}
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
onMouseUp={handleMouseUpPassword}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
</div>
<div
data-mdb-input-init
className="form-outline form-white mb-4"
>
<TextField
placeholder="Sifre Tekrar"
type={showRePassword ? "text" : "password"}
value={rePassword}
onChange={(e) => {
setRePassword(e.target.value);
if (e.target.value !== password) {
setIsWrong(true);
} else {
setIsWrong(false);
}
}}
error={isWrong}
sx={{ width: "100%" }}
helperText={isWrong ? "Sifreler uyusmuyor" : null}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label={
showRePassword
? "hide the password"
: "display the password"
}
onClick={handleClickShowRePassword}
onMouseDown={handleMouseDownRePassword}
onMouseUp={handleMouseUpRePassword}
edge="end"
>
{showRePassword ? (
<VisibilityOff />
) : (
<Visibility />
)}
</IconButton>
</InputAdornment>
),
}}
/>
</div>
<button
data-mdb-button-init
data-mdb-ripple-init
className="btn btn-outline-dark btn-lg px-5"
type="submit"
onClick={doResetPassword}
>
Yenile
</button>
</div>
</div>
</div>
</div>
</div>
</section>
);
}