[TIL] Spring Skills - Membership Registration Implementation
This article summarizes the member entity, permission enum, administrator sign-up token, PasswordEncoder-based password encryption, and member sign-up API implementation flow.
한국어 원문은 여기에서 볼 수 있습니다.
What I studied
Membership registration implementation
Membership registration design
Implement @Entity class mapped to member DB
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)This is an annotation used when storing EnumType in a DB column.
If you use the
EnumType.STRINGoptions, the name of the enum is stored in the DB as is.USER(Authority.USER)→ USER
How to authorize administrator membership
Requires entry of ‘administrator sign-up token’: Use of randomly generated tokens
ex)"AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC"Usually, in the field, the above authority is not granted.
If a hacker steals the password, he or she can easily obtain administrator privileges.
Actually,
Implementation of an administrator page that can grant ‘administrator’ privileges
Implementation of approval process by approver → Grant administrator authority
Implemented like this:
- Understanding Password Encryption
When registering as a member, the ‘password’ must not be registered in the DB exactly as it is entered by the user. Password encryption is mandatory under the ‘Information and Communications Network Act and Personal Information Protection Act’
- After encryption, password storage is required.
Plaintext → (Encryption algorithm) → Ciphertext ex) “nobodynobody” → “$2a$10$..”
Even if a hacker steals Alice’s password information in the DB, the actual password cannot be known.
Therefore, decryption is impossible one-way algorithm must be used
- One-way encryption algorithm
Encryption: Plaintext → (Encryption algorithm) → Ciphertext- Decryption: Not possible
(ciphertext → (encryption algorithm) → plaintext)Then, does the user have to remember the encrypted password when logging in?
- Password confirmation procedure
- User enters “ID, password (plain text)” to log in → Request to log in to server
Encrypt the password (plaintext) on the server
Plaintext → (Encryption algorithm) → Ciphertext
- Check whether it matches the “ID, password (ciphertext)” stored in the DB
- User enters “ID, password (plain text)” to log in → Request to log in to server
- Password confirmation procedure
- One-way encryption algorithm
- Password Matching
Use the password encryption function provided by a framework called Spring Security. PasswordEncoder, which we saw as an example of manual bean registration, is a password encryption method provided by the security.
It is also widely used as it has a function that compares the password entered by the user with the encrypted and stored password to check whether it matches.
1 2 3 4 5
// 사용예시 // 비밀번호 확인 if(!passwordEncoder.matches("사용자가 입력한 비밀번호", "저장된 비밀번호")) { throw new IllegalAccessError("비밀번호가 일치하지 않습니다."); }
- boolean matches(CharSequence rawPassword, String encodedPassword);
rawPassword: Password entered by the user
encodedPassword: Password encrypted and stored in DB
- boolean matches(CharSequence rawPassword, String encodedPassword);
- After encryption, password storage is required.
Implementation of membership registration 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); }
Login implementation- Login API design
1

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); } }

