[TIL] 스프링 숙련 - 회원가입 구현
회원 엔티티와 권한 enum, 관리자 가입 토큰, PasswordEncoder 기반 비밀번호 암호화와 회원가입 API 구현 흐름을 정리한 글입니다.
For the English version of this post, see here.
공부한 내용
회원가입 구현
회원가입 설계
회원 DB에 매핑되는 @Entity 클래스 구현
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
package com.sparta.springauth.entity; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Entity @Getter @Setter @NoArgsConstructor @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String username; @Column(nullable = false) private String password; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) @Enumerated(value = EnumType.STRING) private UserRoleEnum role; }
@Enumerated(value = EnumType.STRING)EnumType을 DB 컬럼에 저장할 때 사용하는 애너테이션입니다.
EnumType.STRING옵션을 사용하면 Enum의 이름을 DB에 그대로 저장합니다.USER(Authority.USER)→ USER
관리자 회원 가입 인가 방법
‘관리자 가입 토큰’ 입력이 필요하도록: 랜덤하게 생성된 토큰 사용
ex)"AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC"보통 현업에서는 위와 같이 권한을 부여하지 않음
해커가 해당 암호를 갈취하게 되면, 관리자 권한을 쉽게 획득할 수 있기 때문
실제로는,
‘관리자’ 권한을 부여할 수 있는 관리자 페이지 구현
승인자에 의한 결재 과정 구현 → 관리자 권한 부여
처럼 구현하게 됨
- 패스워드 암호화 이해
회원 등록 시, ‘비밀번호’는 사용자가 입력한 문자 그대로 DB에 등록하면 안됨 ‘정보통신망법, 개인정보보호법’에 의해 비밀번호 암호화(Encryption)가 의무임
- 암호화 후, 패스워드 저장이 필요함
평문 → (암호화 알고리즘) → 암호문 ex) “nobodynobody” → “$2a$10$..”
만약 해커가 DB에 있는 앨리스의 패스워드 정보를 갈취하더라도 실제 암호를 알 수 없음
그래서 복호화가 불가능한 단방향 알고리즘 사용이 필요함
- 단방향 암호 알고리즘
암호화: 평문 → (암호화 알고리즘) → 암호문
복호화: 불가
(암호문 → (암호화 알고리즘) → 평문)그럼 사용자가 로그인 할 때는 암호화된 패스워드를 기억해야하는지
- Password 확인 절차
- 사용자가 로그인을 위해 “아이디, 패스워드 (평문)” 입력 → 서버에 로그인 요청
서버에서 패스워드 (평문)을 암호화
평문 → (암호화 알고리즘) → 암호문
- DB에 저장된 “아이디, 패스워드(암호문)”와 일치 여부 확인
- 사용자가 로그인을 위해 “아이디, 패스워드 (평문)” 입력 → 서버에 로그인 요청
- Password 확인 절차
- 단방향 암호 알고리즘
- Password Matching
Spring Security라는 프레임워크에서 제공하는 비밀번호 암호화 기능을 사용 Bean 수동등록 예제로 봤던 PasswordEncoder가 해당 Security에서 제공하는 비밀번호 암호화 메서드임
1
사용자가 입력한 비밀번호가 암호화되어 저장된 비밀번호와 비교하여 일치여부를 확인해주는 기능도 가지고 있어 많이 사용됨
1 2 3 4 5
// 사용예시 // 비밀번호 확인 if(!passwordEncoder.matches("사용자가 입력한 비밀번호", "저장된 비밀번호")) { throw new IllegalAccessError("비밀번호가 일치하지 않습니다."); }
- boolean matches(CharSequence rawPassword, String encodedPassword);
rawPassword : 사용자가 입력한 비밀번호
encodedPassword : 암호화되어 DB 에 저장된 비밀번호
- boolean matches(CharSequence rawPassword, String encodedPassword);
- 암호화 후, 패스워드 저장이 필요함
회원가입 API 구현
SignupRequestDto
1 2 3 4 5 6 7 8 9 10 11 12 13 14
package com.sparta.springauth.dto; import lombok.Getter; import lombok.Setter; @Getter @Setter public class SignupRequestDto { private String username; private String password; private String email; private boolean admin = false; private String adminToken = ""; }
UserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
package com.sparta.springauth.service; import java.util.Optional; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import com.sparta.springauth.dto.SignupRequestDto; import com.sparta.springauth.entity.User; import com.sparta.springauth.entity.UserRoleEnum; import com.sparta.springauth.repository.UserRepository; @Service public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } // ADMIN_TOKEN private final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC"; public void signup(SignupRequestDto requestDto) { String username = requestDto.getUsername(); String password = passwordEncoder.encode(requestDto.getPassword()); // 회원 중복 확인 Optional<User> checkUsername = userRepository.findByUsername(username); if (checkUsername.isPresent()) { throw new IllegalArgumentException("중복된 사용자가 존재합니다."); } // email 중복확인 String email = requestDto.getEmail(); Optional<User> checkEmail = userRepository.findByEmail(email); if (checkEmail.isPresent()) { throw new IllegalArgumentException("중복된 Email 입니다."); } // 사용자 ROLE 확인 UserRoleEnum role = UserRoleEnum.USER; if (requestDto.isAdmin()) { if (!ADMIN_TOKEN.equals(requestDto.getAdminToken())) { throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다."); } role = UserRoleEnum.ADMIN; } // 사용자 등록 User user = new User(username, password, email, role); userRepository.save(user); } }
UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
package com.sparta.springauth.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import lombok.RequiredArgsConstructor; import com.sparta.springauth.dto.SignupRequestDto; import com.sparta.springauth.service.UserService; @Controller @RequestMapping("/api") @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping("/user/login-page") public String loginPage() { return "login"; } @GetMapping("/user/signup") public String signupPage() { return "signup"; } @PostMapping("/user/signup") public String singup(SignupRequestDto requestDto) { userService.signup(requestDto); return "redirect:/api/user/login-page"; } }
UserRepository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
package com.sparta.springauth.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.sparta.springauth.entity.User; public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); Optional<User> findByEmail(String email); }
로그인 구현
LoginRequestDto
1 2 3 4 5 6 7 8 9 10 11
package com.sparta.springauth.dto; import lombok.Getter; import lombok.Setter; @Setter @Getter public class LoginRequestDto { private String username; private String password; }
UserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
package com.sparta.springauth.service; import java.util.Optional; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; import com.sparta.springauth.dto.LoginRequestDto; import com.sparta.springauth.dto.SignupRequestDto; import com.sparta.springauth.entity.User; import com.sparta.springauth.entity.UserRoleEnum; import com.sparta.springauth.jwt.JwtUtil; import com.sparta.springauth.repository.UserRepository; @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; // ADMIN_TOKEN private final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC"; public void signup(SignupRequestDto requestDto) { String username = requestDto.getUsername(); String password = passwordEncoder.encode(requestDto.getPassword()); // 회원 중복 확인 Optional<User> checkUsername = userRepository.findByUsername(username); if (checkUsername.isPresent()) { throw new IllegalArgumentException("중복된 사용자가 존재합니다."); } // email 중복확인 String email = requestDto.getEmail(); Optional<User> checkEmail = userRepository.findByEmail(email); if (checkEmail.isPresent()) { throw new IllegalArgumentException("중복된 Email 입니다."); } // 사용자 ROLE 확인 UserRoleEnum role = UserRoleEnum.USER; if (requestDto.isAdmin()) { if (!ADMIN_TOKEN.equals(requestDto.getAdminToken())) { throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다."); } role = UserRoleEnum.ADMIN; } // 사용자 등록 User user = new User(username, password, email, role); userRepository.save(user); } public void login(LoginRequestDto requestDto, HttpServletResponse res) { String username = requestDto.getUsername(); String password = requestDto.getPassword(); // 사용자 확인 User user = userRepository.findByUsername(username).orElseThrow( () -> new IllegalArgumentException("등록된 사용자가 없습니다.") ); // 비밀번호 확인 if(!passwordEncoder.matches(password, user.getPassword())) { throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); } // JWT 생성 및 쿠키에 저장 후 Response 객체에 추가 String token = jwtUtil.createToken(user.getUsername(), user.getRole()); jwtUtil.addJwtToCookie(token, res); } }


