지난번엔 평문으로 비밀번호를 저장하고 JPA Entity를 이용해 제약조건을 설정하는 방법을 사용했다.
현업에서는 비밀번호를 그대로 저장하지 않고 암호화하여 대부분 저장하기에 암호화 된 형태로 저장하는 것으로 변경이 필요하다고 생각했다.
import jakarta.persistence.*;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
@Entity
@Data //getter, setter 자동생성
@NoArgsConstructor //기본 생성자 추가
@AllArgsConstructor //모든 필드에 대한 생성자 추가
@Table(name="USER_INFO")
@Builder
public class UserInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="seq_no")
private Integer seqNo;
@Column(name="user_name", nullable = false, unique = true)
//@Pattern(regexp = "[0-9a-z]+", message="Username Must Contain Only Lowered letters and Numbers") >> DTO로 변경
@Size(min=4, max=10, message="size 4~10")
private String userName;
@Column(nullable = false)
//@Pattern(regexp = "[0-9a-zA-Z]+", message="Username Must Contain Only Letters and Numbers") >> DTO로 변경
private String password;
@Column(name= "reg_id")
private String regId;
@Column(name= "reg_date")
private String regDate;
@Column(name="reg_ip")
private String regIp;
}
우선 @Pattern를 없앴다.
암호화 된 형태로 api를 호출하면 $2a$10$0w.AA39kcQyqZM57YR/ajeJNRtvj1QldzrPvwswdjFYzDnHEkPwvq 요런 형태로 저장하려하기 때문에 제약조건에서 걸려 401 에러를 뱉어낸다.
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(force = true)
public class UserDTO {
@NotBlank
@Pattern(regexp = "[0-9a-z]+", message="Username Must Contain Only Lowered letters and Numbers")
@Size(min=4, max=10, message="size 4~10")
private final String userName;
@NotBlank
@Pattern(regexp = "[0-9a-zA-Z]+", message="Username Must Contain Only Letters and Numbers")
@Size(min=8, max=15, message="size 8~15")
private final String password;
}
때문에 DTO로 변경하였다.
이 작업 때문에 Entity와 DTO의 차이에 대해 공부를 했는데 추후 포스팅 할 예정이다.
@RestController
@Slf4j
@RequestMapping("/auth")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final TokenMakerService tokenMakerService;
private final AuthenticationManager authenticationManager;
@PostMapping
public ResponseEntity<String> registerUser(@RequestBody @Valid UserDTO user){
log.info("Registering user: {}", user.getUserName());
if (userService.registerUser(user).equals("REQUEST_SUCCESS")) {
log.info("User registered successfully: {}", user.getUserName());
return ResponseEntity.ok( "User registered successfully!");
} else {
log.warn("Username already exists: {}", user.getUserName());
return ResponseEntity.badRequest().body("Username "+user.getUserName()+" already exists!");
}
}
@PostMapping("/login")
public ResponseEntity<?> userLogin(@RequestBody LoginDTO loginInfo, HttpServletResponse response){
log.info("Login attempt for user: {}", loginInfo.getUserName());
try {
//세션 등록
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginInfo.getUserName(), loginInfo.getPassword()));
//AuthenticationManager : 인증 논리를 구현하기 위해 인증 공급자를 이용하여 인증 처리
SecurityContextHolder.getContext().setAuthentication(auth);
String jwtToken = tokenMakerService.createToken2(auth);
log.info("Login Success for user: {}", loginInfo.getUserName());
return ResponseEntity.ok(new JwtResponse(jwtToken));
}
catch (Exception e){
log.warn("Invalid username or password for user: {}", loginInfo.getUserName());
return ResponseEntity.status(401).body("Invalid username or password");
}
//response.setHeader("Authorization", "Bearer" + jwtToken);
//사용자 인증 처리
}
}
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginInfo.getUserName(), loginInfo.getPassword()));
AbstractAuthenticationProcessingFilter를 상속받고있다.자꾸 여기 인증하는 부분에서 막혔는데
AuthenticationManager가 프록시로 감싸져 재귀 호출로 무한 루프에 빠지는 현상이 나타났다.
java.lang.StackOverflowError: null
at jdk.internal.reflect.GeneratedMethodAccessor25.invoke(Unknown Source) ~
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354) ~[spring-aop-6.1.11.jar:6.1.11]
at
계속 이런 오류가 뜸.
원인은 UserDetailsService의 구현이 필요했다.
UserDetailsService
=> 이걸 몰라서 거의 3-4시간은 헤맨듯 하다. ㅠㅠ
public String createToken2(Authentication authentication){
Date now = new Date();
Date validity = new Date(now.getTime() + accessTokenExpMilliseconds);
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return "Bearer " + Jwts.builder()
.setSubject(authentication.getName()) //주체설정
.setIssuedAt(now)//토큰 발행 시간
.setExpiration(validity) // 토큰 만료 시간
.signWith(secretKey) // 비밀키로 서명
.compact(); //JWT생성
}
이전 소스와의 차이점은 파라미터를 String username으로 받는게 아닌 Authentication로 받아 토큰을 만들 때에도 .setSubject(authentication.getName())으로 주체를 설정했다.
@Service
@Slf4j
public class UserService {
private final UserInfoRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
public UserService(UserInfoRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public String registerUser(UserDTO user){
String clientIp = "";
// String encodedPassword = passwordEncoder.encode(user.getPassword());
log.info("User registration started for: {}", user.getUserName());
log.info("Login attempt for user: {}", user.getUserName());
if(userRepository.findOneByUserName(user.getUserName()) != null)
return ReturnCodes.EXISTS_USER;
DateFormat now = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
log.info("password Encode : "+ passwordEncoder.encode(user.getPassword()));
userRepository.save(UserInfo.builder()
.userName(user.getUserName())
.password(passwordEncoder.encode(user.getPassword()))
.regDate(now.format(new Date()))
.regIp(clientIp)
.build());
//userRepository.save(user);
return "REQUEST_SUCCESS"; //회원가입 성공
//비밀번호 암호화 한 뒤 전송 > 비교
}
public boolean validateUser(String username, String rawPassword) {
Optional<UserInfo> optionalUser = userRepository.findByUserName(username);
if (optionalUser.isPresent()) {
UserInfo user = optionalUser.get();
log.info("User found: {}", username);
log.info("Comparing passwords: rawPassword = {}, storedPassword = {}", rawPassword, user.getPassword());
return passwordEncoder.matches(rawPassword, user.getPassword());
} else {
log.warn("User not found: {}", username);
return false;
}
}
}
password를 인코딩하였다.passwordEncoder.encode(user.getPassword())@Service
public class CustomUserDetailService implements UserDetailsService {
private final UserInfoRepository userInfoRepository;
public CustomUserDetailService(UserInfoRepository userInfoRepository){
this.userInfoRepository = userInfoRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo userInfo = userInfoRepository.findByUserName(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return User.builder()
.username(userInfo.getUserName())
.password(userInfo.getPassword()) // 이때 비밀번호는 암호화된 값이어야 함
.roles("USER") // 권한 설정
.build();
}
}

정상적으로 토큰값을 불러오고있다.