Selaa lähdekoodia

feat:实现邮箱登录

chenjiayao 1 vuosi sitten
vanhempi
commit
5420b6da4a

+ 5 - 1
src/main/java/com/njuzr/eaibackend/config/SecurityConfig.java

@@ -50,7 +50,11 @@ public class SecurityConfig {
     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http
                 .authorizeHttpRequests(authorize -> authorize
-                        .requestMatchers("/api/user/login", "/api/user/register", "/api/user/verifyCode", "/api/onlyoffice/**", "/api/assignment/**").permitAll() // 允许公开访问的路径
+                        .requestMatchers("/api/auth/*",
+                                "/api/user/register",
+                                "/api/user/verifyCode",
+                                "/api/onlyoffice/**",
+                                "/api/assignment/**").permitAll() // 允许公开访问的路径
                         .anyRequest().authenticated() // 其他所有请求需要认证
                 )
                 .exceptionHandling(exception -> exception

+ 25 - 55
src/main/java/com/njuzr/eaibackend/controller/AuthenticationController.java

@@ -1,43 +1,20 @@
 package com.njuzr.eaibackend.controller;
 
-import com.njuzr.eaibackend.dto.user.UserLoginDTO;
-import com.njuzr.eaibackend.po.MyUserDetails;
-import com.njuzr.eaibackend.service.UserService;
-import com.njuzr.eaibackend.utils.JWTTokenUtil;
-import com.njuzr.eaibackend.utils.ModelMapperUtil;
-import com.njuzr.eaibackend.vo.UserLoginVO;
+import com.njuzr.eaibackend.dto.auth.EmailLoginDTO;
+import com.njuzr.eaibackend.dto.auth.UserLoginDTO;
+import com.njuzr.eaibackend.service.AuthenticationService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.BadCredentialsException;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.web.bind.annotation.*;
 
-/**
- * @author: Leonezhurui
- * @Date: 2024/2/20 - 00:10
- * @Package: EAI-Backend
- */
-
 @Slf4j
 @RestController
 @CrossOrigin
-@RequestMapping("/api/user")
+@RequestMapping("/api/auth")
 public class AuthenticationController {
 
-    private final AuthenticationManager authenticationManager;
-
-    private final JWTTokenUtil jwtTokenUtil;
-
     @Autowired
-    public AuthenticationController(AuthenticationManager authenticationManager, UserService userService, JWTTokenUtil jwtTokenUtil) {
-        this.authenticationManager = authenticationManager;
-        this.jwtTokenUtil = jwtTokenUtil;
-    }
+    private AuthenticationService authenticationService;
 
     /**
      * 用户登陆学号(officialNumber)和密码(password)进行登陆
@@ -46,34 +23,27 @@ public class AuthenticationController {
      */
     @PostMapping("/login")
     public MyResponse login(@RequestBody UserLoginDTO userLoginDTO) {
-        try {
-            Authentication authentication = authenticationManager.authenticate(
-                    new UsernamePasswordAuthenticationToken(
-                            userLoginDTO.getOfficialNumber(),
-                            userLoginDTO.getPassword()
-                    )
-            );
-
-            SecurityContextHolder.getContext().setAuthentication(authentication);
-
-            MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
-
-            log.info("用户认证通过,用户认证信息如下:" + userDetails.toString());
-
-            String jwt = jwtTokenUtil.generateToken(userDetails.getOfficialNumber());
-            log.info("生成jwt成功,token信息:" + jwt);
+        return authenticationService.login(userLoginDTO);
+    }
 
-            UserLoginVO userLoginVO = ModelMapperUtil.map(userDetails, UserLoginVO.class);
-            userLoginVO.setToken(jwt);
-            return MyResponse.success(userLoginVO);
+    /**
+     * 邮箱验证登录
+     * @param emailLoginDTO officialEmail、verifyCode
+     * @return 如果成功登陆,则返回登陆用户的身份信息和Token;如果失败,则返回具体原因。
+     */
+    @PostMapping("/loginByEmail")
+    public MyResponse loginByEmail(@RequestBody EmailLoginDTO emailLoginDTO) {
+        return authenticationService.loginByEmail(emailLoginDTO);
+    }
 
-        } catch (BadCredentialsException e) { // 密码出错抛出的异常
-            log.error("登陆失败,密码错误");
-            return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"用户名或密码错误");
-        } catch (UsernameNotFoundException e) {// 找不到用户抛出的异常
-            log.error("登陆失败,找不到该用户");
-            return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"用户名或密码错误");
-        }
+    /**
+     * 发送邮箱登录的验证码
+     */
+    @GetMapping("/loginVerifyCode")
+    public MyResponse sendLoginVerifyCode(@RequestParam(value = "officialEmail") String officialEmail) {
+        authenticationService.sendLoginVerifyCode(officialEmail);
+        return MyResponse.success("发送成功");
     }
 
-}
+
+}

+ 10 - 0
src/main/java/com/njuzr/eaibackend/dto/auth/EmailLoginDTO.java

@@ -0,0 +1,10 @@
+package com.njuzr.eaibackend.dto.auth;
+
+import lombok.Data;
+
+@Data
+public class EmailLoginDTO {
+    String officialEmail;
+
+    String verifyCode;
+}

+ 1 - 1
src/main/java/com/njuzr/eaibackend/dto/user/UserLoginDTO.java → src/main/java/com/njuzr/eaibackend/dto/auth/UserLoginDTO.java

@@ -1,4 +1,4 @@
-package com.njuzr.eaibackend.dto.user;
+package com.njuzr.eaibackend.dto.auth;
 
 import lombok.Data;
 

+ 7 - 1
src/main/java/com/njuzr/eaibackend/mapper/UserMapper.java

@@ -23,7 +23,6 @@ public interface UserMapper extends BaseMapper<User> {
      */
     User selectByOfficialNumber(String officialNumber);
 
-
     /**
      * 用户信息更新
      * @param user
@@ -48,4 +47,11 @@ public interface UserMapper extends BaseMapper<User> {
      * @return 姓名
      */
     String selectNameById(Long id);
+
+    /**
+     * 通过邮箱读取用户
+     * @param officialEmail
+     * @return User
+     */
+    User selectByOfficialEmail(String officialEmail);
 }

+ 15 - 0
src/main/java/com/njuzr/eaibackend/service/AuthenticationService.java

@@ -0,0 +1,15 @@
+package com.njuzr.eaibackend.service;
+
+import com.njuzr.eaibackend.controller.MyResponse;
+import com.njuzr.eaibackend.dto.auth.EmailLoginDTO;
+import com.njuzr.eaibackend.dto.auth.UserLoginDTO;
+
+public interface AuthenticationService {
+
+    MyResponse login(UserLoginDTO userLoginDTO);
+
+    MyResponse loginByEmail(EmailLoginDTO emailLoginDTO);
+
+    void sendLoginVerifyCode(String officialEmail);
+
+}

+ 0 - 1
src/main/java/com/njuzr/eaibackend/service/CourseService.java

@@ -7,7 +7,6 @@ import com.njuzr.eaibackend.dto.course.CourseQueryDTO;
 import com.njuzr.eaibackend.dto.course.CourseUpdateDTO;
 import com.njuzr.eaibackend.dto.course.EnrollDTO;
 import com.njuzr.eaibackend.po.Course;
-import com.njuzr.eaibackend.po.Enrollment;
 import com.njuzr.eaibackend.po.MyUserDetails;
 import com.njuzr.eaibackend.po.User;
 import com.njuzr.eaibackend.vo.CourseVO;

+ 131 - 0
src/main/java/com/njuzr/eaibackend/service/impl/AuthenticationServiceImpl.java

@@ -0,0 +1,131 @@
+package com.njuzr.eaibackend.service.impl;
+
+import com.njuzr.eaibackend.controller.MyResponse;
+import com.njuzr.eaibackend.dto.auth.EmailLoginDTO;
+import com.njuzr.eaibackend.dto.auth.UserLoginDTO;
+import com.njuzr.eaibackend.exception.MyException;
+import com.njuzr.eaibackend.mapper.UserMapper;
+import com.njuzr.eaibackend.po.MyUserDetails;
+import com.njuzr.eaibackend.po.User;
+import com.njuzr.eaibackend.service.AuthenticationService;
+import com.njuzr.eaibackend.service.EmailService;
+import com.njuzr.eaibackend.utils.JWTTokenUtil;
+import com.njuzr.eaibackend.utils.ModelMapperUtil;
+import com.njuzr.eaibackend.vo.auth.UserLoginVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+public class AuthenticationServiceImpl implements AuthenticationService {
+
+    @Autowired
+    private AuthenticationManager authenticationManager;
+    @Autowired
+    private JWTTokenUtil jwtTokenUtil;
+    @Autowired
+    private EmailService emailService;
+    @Autowired
+    private UserMapper userMapper;
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    private static final long SEND_INTERVAL = 60; // 允许再次发送的时间间隔,单位秒
+
+    @Override
+    public MyResponse login(UserLoginDTO userLoginDTO) {
+        try {
+            Authentication authentication = authenticationManager.authenticate(
+                    new UsernamePasswordAuthenticationToken(
+                            userLoginDTO.getOfficialNumber(),
+                            userLoginDTO.getPassword()
+                    )
+            );
+
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+            MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
+
+            log.info("用户认证通过,用户认证信息如下:" + userDetails.toString());
+
+            String jwt = jwtTokenUtil.generateToken(userDetails.getOfficialNumber());
+            log.info("生成jwt成功,token信息:" + jwt);
+
+            UserLoginVO userLoginVO = ModelMapperUtil.map(userDetails, UserLoginVO.class);
+            userLoginVO.setToken(jwt);
+            return MyResponse.success(userLoginVO);
+
+        } catch (BadCredentialsException e) {
+            log.error("登陆失败,密码错误");
+            return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"用户名或密码错误");
+        } catch (UsernameNotFoundException e) {
+            log.error("登陆失败,找不到该用户");
+            return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"用户名或密码错误");
+        }
+    }
+
+    @Override
+    public MyResponse loginByEmail(EmailLoginDTO emailLoginDTO) {
+        // 从Redis中获取验证码
+        final String key = "loginVerifyCode:" + emailLoginDTO.getOfficialEmail();
+        String storedCode = (String) redisTemplate.opsForValue().get(key);
+        if (storedCode == null) {
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "验证码已过期");
+        }
+        // 验证码校验
+        if(!storedCode.equals(emailLoginDTO.getVerifyCode())) {
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "验证码错误");
+        }
+
+        User user = userMapper.selectByOfficialEmail(emailLoginDTO.getOfficialEmail());
+        if(user == null) {
+            log.error("登陆失败,找不到该用户");
+            return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"邮箱未注册");
+        }
+        String jwt = jwtTokenUtil.generateToken(user.getOfficialNumber());
+        log.info("生成jwt成功,token信息:" + jwt);
+        UserLoginVO userLoginVO = ModelMapperUtil.map(user, UserLoginVO.class);
+        userLoginVO.setToken(jwt);
+        return MyResponse.success(userLoginVO);
+    }
+
+    @Override
+    public void sendLoginVerifyCode(String officialEmail) {
+        // 检验邮箱合法性,只允许南京大学邮箱
+        if (!officialEmail.endsWith("@smail.nju.edu.cn") && !officialEmail.endsWith("@nju.edu.cn")) {
+            throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "只允许南京大学邮箱,请检查邮箱格式");
+        }
+
+        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
+        String intervalKey = "requestInterval:" + officialEmail;
+
+        // 检查是否已经发送过验证码并且时间间隔未过
+        if (ops.get(intervalKey) != null) {
+            throw new MyException(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase() + ":" + "需等待" + SEND_INTERVAL + "秒才能再次发送验证码");
+        }
+
+        // 生成验证码
+        String code = UUID.randomUUID().toString().substring(0, 6);
+        // 存储验证码到Redis,设置5分钟过期
+        int timeout = 5;
+        final String key = "loginVerifyCode:" + officialEmail;
+        // 存储验证码到Redis并设置过期时间
+        redisTemplate.opsForValue().set(key, code, timeout, TimeUnit.MINUTES);
+        // 设置请求间隔标记,防止频繁请求
+        redisTemplate.opsForValue().set(intervalKey, "requested", SEND_INTERVAL, TimeUnit.SECONDS);
+        emailService.sendVerifyCodeEmail(officialEmail, code, timeout);
+    }
+
+}

+ 1 - 1
src/main/java/com/njuzr/eaibackend/vo/UserLoginVO.java → src/main/java/com/njuzr/eaibackend/vo/auth/UserLoginVO.java

@@ -1,4 +1,4 @@
-package com.njuzr.eaibackend.vo;
+package com.njuzr.eaibackend.vo.auth;
 
 import com.njuzr.eaibackend.po.base.BaseUser;
 import lombok.Data;

+ 6 - 0
src/main/resources/mapper/UserMapper.xml

@@ -7,6 +7,12 @@
         where official_number = #{officialNumber}
     </select>
 
+    <select id="selectByOfficialEmail" resultType="com.njuzr.eaibackend.po.User">
+        select *
+        from users
+        where official_email = #{officialEmail}
+    </select>
+
     <update id="updateUser" parameterType="com.njuzr.eaibackend.po.User">
         UPDATE users
         <set>