[Springboot]發(fā)送郵件憨颠、重置密碼業(yè)務(wù)實(shí)戰(zhàn)

前言

忘記密碼并通過郵件重置密碼是一個常見的業(yè)務(wù)需求胳徽,在開發(fā)我的個人小項(xiàng)目過程中,也需要用到這個業(yè)務(wù)爽彤,今天就給大家?guī)硪粋€業(yè)務(wù)實(shí)戰(zhàn)养盗。

開發(fā)環(huán)境

  • springboot:1.5.16.RELEASE

業(yè)務(wù)流程

根據(jù)controller中函數(shù)分為兩個部分:

  1. 用戶申請重置郵件:
  • 用戶在頁面中輸入郵箱
  • 服務(wù)器檢查是否允許重置(郵箱所指向用戶是否存在,重置是否過于頻繁适篙,重置是否到達(dá)日請求上限)
  • 驗(yàn)證通過后往核,想validate表寫入申請記錄,包含token匙瘪,用戶郵箱和id
  • 發(fā)送郵件(包含帶有token的鏈接)
  • 用戶點(diǎn)擊郵件內(nèi)連接
  • 跳轉(zhuǎn)到新密碼輸入網(wǎng)頁
  • 提交重置密碼請求(POST中包含token铆铆,新密碼)
  1. 用戶重置密碼
  • 服務(wù)器驗(yàn)證token(token是否過期,該用戶是否發(fā)起過其它新token)
  • 通過validate表記錄查找用戶id丹喻,修改用戶密碼

實(shí)戰(zhàn)

  1. pom.xml添加email依賴
<!--郵件: email-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
  1. 添加pm_validate表結(jié)構(gòu)

其中reset_token由UUID生成薄货,type默認(rèn)為resetPassword(方便以后新增需求),user_id為用戶表用戶id

-- ----------------------------
-- Table structure for pm_validate
-- ----------------------------
DROP TABLE IF EXISTS `pm_validate`;
CREATE TABLE `pm_validate` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `email` varchar(40) NOT NULL,
  `reset_token` varchar(40) NOT NULL,
  `type` varchar(20) NOT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

生成或編寫對應(yīng)pojo和mapper碍论。谅猾,由于我使用了mybatis-generator插件,需要運(yùn)行插件生成對應(yīng)pojo和mapper鳍悠。

  1. 修改application.properties税娜,添加郵箱配置
# 發(fā)送郵件配置
spring.mail.host=smtp.gmail.com
spring.mail.username=xxxxxx@gmail.com
spring.mail.password=xxxxxxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
  1. 編寫controller和service
  • ValidateController
@RestController
@RequestMapping(value = "/validate")
public class ValidateController {

    @Autowired
    private ValidateService validateService;

    @Autowired
    private UserService userService;

    @Value("${spring.mail.username}")
    private String from;

    /**
     * 發(fā)送忘記密碼郵件請求,每日申請次數(shù)不超過5次藏研,每次申請間隔不低于1分鐘
     * @param email
     * @param request
     * @return
     */
    @ApiOperation(value = "發(fā)送忘記密碼郵件", notes = "發(fā)送忘記密碼郵件")
    @RequestMapping(value = "/sendValidationEmail", method = {RequestMethod.POST})
    public ResponseData<String> sendValidationEmail(@ApiParam("郵箱地址") @RequestParam("email") String email,
                                               HttpServletRequest request){
        ResponseData<String> responseData = new ResponseData<>();
        List<User> users = userService.findUserByEmail(email);
        if (users == null){
            responseData.jsonFill(2, "該郵箱所屬用戶不存在", null);
        }else {
            if (validateService.sendValidateLimitation(email, 5,1)){
                // 若允許重置密碼敬矩,則在pm_validate表中插入一行數(shù)據(jù),帶有token
                Validate validate = new Validate();
                validateService.insertNewResetRecord(validate, users.get(0), UUID.randomUUID().toString());
                // 設(shè)置郵件內(nèi)容
                String appUrl = request.getScheme() + "://" + request.getServerName();
                SimpleMailMessage passwordResetEmail = new SimpleMailMessage();
                passwordResetEmail.setFrom(from);
                passwordResetEmail.setTo(email);
                passwordResetEmail.setSubject("【電商價格監(jiān)控】忘記密碼");
                passwordResetEmail.setText("您正在申請重置密碼蠢挡,請點(diǎn)擊此鏈接重置密碼: \n" + appUrl + "/validate/reset?token=" + validate.getResetToken());
                validateService.sendPasswordResetEmail(passwordResetEmail);
                responseData.jsonFill(1, null, null);
            }else {
                responseData.jsonFill(2,"操作過于頻繁弧岳,請稍后再試凳忙!",null);
            }
        }
        return responseData;
    }

    /**
     * 將url的token和數(shù)據(jù)庫里的token匹配,成功后便可修改密碼禽炬,token有效期為60分鐘
     * @param token
     * @param password
     * @param confirmPassword
     * @return
     */
    @ApiOperation(value = "重置密碼", notes = "重置密碼")
    @RequestMapping(value = "/resetPassword", method = RequestMethod.POST)
    public ResponseData<String> resetPassword(@ApiParam("token") @RequestParam("token") String token,
                                              @ApiParam("密碼") @RequestParam("password") String password,
                                              @ApiParam("密碼確認(rèn)") @RequestParam("confirmPassword") String confirmPassword){
        ResponseData<String> responseData = new ResponseData<>();
        // 通過token找到validate記錄
        List<Validate> validates = validateService.findUserByResetToken(token);
        if (validates == null){
            responseData.jsonFill(2,"該重置請求不存在",null);
        }else {
            Validate validate = validates.get(0);
            if (validateService.validateLimitation(validate.getEmail(), Long.MAX_VALUE, 60, token)){
                Integer userId = validate.getUserId();
                if (password.equals(confirmPassword)) {
                    userService.updatePassword(password, userId);
                    responseData.jsonFill(1, null,null);
                }else {
                    responseData.jsonFill(2,"確認(rèn)密碼和密碼不一致涧卵,請重新輸入", null);
                }
            }else {
                responseData.jsonFill(2,"該鏈接失效",null);
            }
        }
        return responseData;
    }
}
  • ValidateService
public interface ValidateService {
    void sendPasswordResetEmail(SimpleMailMessage email);
    int insertNewResetRecord(Validate validate, User users, String token);
    List<Validate> findUserByResetToken(String resetToken);
    boolean validateLimitation(String email, long requestPerDay, long interval, String token);
    boolean sendValidateLimitation(String email, long requestPerDay, long interval);
}
  • ValidateServiceImpl
@Service
public class ValidateServiceImpl implements ValidateService {

    @Autowired
    private JavaMailSender javaMailSender;

    @Autowired
    private ValidateMapper validateMapper;

    /**
     * 發(fā)送郵件:@Async進(jìn)行異步調(diào)用發(fā)送郵件接口
     * @param email
     */
    @Override
    @Async
    public void sendPasswordResetEmail(SimpleMailMessage email){
        javaMailSender.send(email);
    }

    /**
     * 在pm_validate表中插入一條validate記錄,userid腹尖,email屬性來自pm_user表柳恐,token由UUID生成
     * @param validate
     * @param users
     * @param token
     * @return
     */
    @Override
    public int insertNewResetRecord(Validate validate, User users, String token){
        validate.setUserId(users.getId());
        validate.setEmail(users.getEmail());
        validate.setResetToken(token);
        validate.setType("passwordReset");
        validate.setGmtCreate(new Date());
        validate.setGmtModified(new Date());
        return validateMapper.insert(validate);
    }

    /**
     * pm_validate表中,通過token查找重置申請記錄
     * @param token
     * @return
     */
    @Override
    public List<Validate> findUserByResetToken(String token){
        ValidateExample validateExample = new ValidateExample();
        ValidateExample.Criteria criteria = validateExample.createCriteria();
        criteria.andResetTokenEqualTo(token);
        return validateMapper.selectByExample(validateExample);
    }

    /**
     * 驗(yàn)證是否發(fā)送重置郵件:每個email的重置密碼每日請求上限為requestPerDay次热幔,與上一次的請求時間間隔為interval分鐘乐设。
     * @param email
     * @param requestPerDay
     * @param interval
     * @return
     */
    @Override
    public boolean sendValidateLimitation(String email, long requestPerDay, long interval){
        ValidateExample validateExample = new ValidateExample();
        ValidateExample.Criteria criteria= validateExample.createCriteria();
        criteria.andEmailEqualTo(email);
        List<Validate> validates = validateMapper.selectByExample(validateExample);
        // 若查無記錄,意味著第一次申請绎巨,直接放行
        if (validates.isEmpty()) {
            return true;
        }
        // 有記錄伤提,則判定是否頻繁申請以及是否達(dá)到日均請求上線
        long countTodayValidation = validates.stream().filter(x->DateUtils.isSameDay(x.getGmtModified(), new Date())).count();
        Optional validate = validates.stream().map(Validate::getGmtModified).max(Date::compareTo);
        Date dateOfLastRequest = new Date();
        if (validate.isPresent()) dateOfLastRequest = (Date) validate.get();
        long intervalForLastRequest = new Date().getTime() - dateOfLastRequest.getTime();

        return countTodayValidation <= requestPerDay && intervalForLastRequest >= interval * 60 * 1000;
    }

    /**
     * 驗(yàn)證連接是否失效:鏈接有兩種情況失效 1.超時 2.最近請求的一次鏈接自動覆蓋之前的鏈接(待看代碼)
     * @param email
     * @param requestPerDay
     * @param interval
     * @return
     */
    @Override
    public boolean validateLimitation(String email, long requestPerDay, long interval, String token){
        ValidateExample validateExample = new ValidateExample();
        ValidateExample.Criteria criteria= validateExample.createCriteria();
        criteria.andEmailEqualTo(email);
        List<Validate> validates = validateMapper.selectByExample(validateExample);
        // 有記錄才會調(diào)用該函數(shù),只需判斷是否超時
        Optional validate = validates.stream().map(Validate::getGmtModified).max(Date::compareTo);
        Date dateOfLastRequest = new Date();
        if (validate.isPresent()) dateOfLastRequest = (Date) validate.get();
        long intervalForLastRequest = new Date().getTime() - dateOfLastRequest.getTime();

        Optional lastRequestToken = validates.stream().filter(x-> x.getResetToken().equals(token)).map(Validate::getGmtModified).findAny();
        Date dateOfLastRequestToken = new Date();
        if (lastRequestToken.isPresent()) {
            dateOfLastRequestToken = (Date) lastRequestToken.get();
        }
        return intervalForLastRequest <= interval * 60 * 1000 && dateOfLastRequest == dateOfLastRequestToken;
    }
}

結(jié)語

如上實(shí)現(xiàn)了整個重置密碼流程认烁,前端網(wǎng)頁自行設(shè)計(jì)實(shí)現(xiàn)肿男。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市却嗡,隨后出現(xiàn)的幾起案子舶沛,更是在濱河造成了極大的恐慌,老刑警劉巖窗价,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件如庭,死亡現(xiàn)場離奇詭異,居然都是意外死亡撼港,警方通過查閱死者的電腦和手機(jī)坪它,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帝牡,“玉大人往毡,你說我怎么就攤上這事“辛铮” “怎么了开瞭?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長罩息。 經(jīng)常有香客問我嗤详,道長,這世上最難降的妖魔是什么瓷炮? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任葱色,我火速辦了婚禮,結(jié)果婚禮上娘香,老公的妹妹穿的比我還像新娘苍狰。我一直安慰自己恐锣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布舞痰。 她就那樣靜靜地躺著,像睡著了一般诀姚。 火紅的嫁衣襯著肌膚如雪响牛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天赫段,我揣著相機(jī)與錄音呀打,去河邊找鬼。 笑死糯笙,一個胖子當(dāng)著我的面吹牛贬丛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播给涕,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼豺憔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了够庙?” 一聲冷哼從身側(cè)響起恭应,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耘眨,沒想到半個月后昼榛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡剔难,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年胆屿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片偶宫。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡非迹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纯趋,到底是詐尸還是另有隱情彻秆,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布结闸,位于F島的核電站唇兑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏桦锄。R本人自食惡果不足惜扎附,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望结耀。 院中可真熱鬧留夜,春花似錦匙铡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嚼摩,卻和暖如春钦讳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背枕面。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工愿卒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人潮秘。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓琼开,卻偏偏與公主長得像,于是被迫代替她去往敵國和親枕荞。 傳聞我的和親對象是個殘疾皇子柜候,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容