Spring boot + React JS 重置密码实现

问题描述 投票:0回答:2

我正在尝试在我的 Spring Boot/React 应用程序中实现密码重置/忘记密码功能,但我在尝试完全理解它时遇到了一些问题,我不确定我所采取的方法是否正确。

我已经可以注册/登录了

  • 我创建了一个 REST 端点:/sendForgotPasswordEmail [POST]
    • 这会通过用户在反应表单中输入的电子邮件地址找到用户,并查看具有该电子邮件的帐户是否存在。
    • 如果找到用户帐户,请向他们发送一封包含唯一令牌的电子邮件,其中包含重置密码表单的链接(固定在此处)
    • 我还创建了一个生成随机 UUID 的令牌实体,因此当用户单击“忘记密码”并填写详细信息时,电子邮件会生成以下链接:http://localhost:3000/reset-password/e90c4fa5-2e74-4b44-b3c4 -5d6f1c616461
    @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);
        }
    }

  • 我创建了一个 REST 端点:/resetPassword [POST]
    • 读取使用@RequestParam生成的token,检查是否过期等
    • 我有一个名为 ResetPassword(String email, String password) 的服务方法,它通过电子邮件搜索用户,并将用户密码设置为该方法中提供的密码。
    @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

}


我想弄清楚的是,究竟如何执行此操作,并确保我可以成功地将用户重定向到重置密码页面,并且他们可以从那里更改密码。 我不确定我的控制器方法在哪里丢失了某些东西或者我哪里出了问题。

当用户在收到电子邮件后到达重置密码表单时,前端+后端需要发生什么过程。

我将不胜感激任何帮助。

javascript java reactjs spring spring-boot
2个回答
2
投票

通常流程是这样的:

  • 在前端:用户点击“提醒密码”,他只提供电子邮件地址 -> 将带有电子邮件的 POST 请求发送到后端
  • 后端接受电子邮件,生成令牌并发送具有唯一网址的电子邮件,如上所述,
  • 用户检查邮件后,单击链接(GET 方法),您需要在前端 URL 上使用例如 GET /password-recovery 端点,该端点从参数中解析令牌,在后端验证令牌是否仍然有效并允许在另一个表单上更改密码。因此,有 2 个额外的后端请求用于令牌验证和密码更改。

0
投票

我想回答我的解决方案 后端侧 用户输入电子邮件后,第一个前端将触发此方法

@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>
  );
}
© www.soinside.com 2019 - 2024. All rights reserved.