로그인 API를 개발할 것이다.
지금부터 개발하며 생긴 문제들을 나열하고,
그에 대한 해결과정과 결과를 기록할 것이다.
BCryptPasswordEncoder는 Spring Security에서 제공하는 클래스입니다. 이를 사용하려면 Spring Security 의존성을 추가해야 하며, 또한 @Bean으로 BCryptPasswordEncoder를 등록해줘야 자동 주입이 가능합니다
의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
// 기타 필요한 의존성들
}
빈 추가
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
빈의 이름은 BCryptPasswordEncoder 로 맞춰줘야 한다.
다음 코드에 주입될 빈과 위 컨피그에서 설정한 빈의 이름은 동일해야 할것이다.
이전에는 블러오는 객체는 같았지만 등록한 빈과 주입하는 빈의 이름을 다르게 했었다.
그러니 빈을 불러올 수 없다는 오류메시지가 나오더라.
그래서 등록한 빈과 주입하는 빈의 이름을 일치시켜줬더니.
그럼 다음과 같이 오류 없이 정상적으로 사용할 수 있다.
성공이다.


로그인 실패이다.
일단 403 forbidden이면 권한 문제이다.
내가 현재 설계한 부분 중에서 문제가 될만한 권한 부분이 어디에 있을까?
다음 코드는 Securityconfig.java 부분이다.
전의 프로젝트에서 긁어오느라 무권한 접근 부분에 대한 API 패스가 이번 프로젝트와 동일하지 않아서 생긴 문제라고 생각한다.

이번 프로젝트와 부합하게 API 루트를 수정해보자.

성공이다. 다음과 같이 200ok가 떴다.
원인은 시큐리티 부분이였다.
로그인을 한 후에는 JWT DTO가 날라와야 한다.
JWT DTO 안에는 엑세스 토큰과 리프레시 토큰이 포함된다.
로그인은 완료가 되었다.
200ok는 출력되었고, 그 다음단계인 JWT를 담당하는 부분을 일단 추적을 해봤더니

다음과 같이 두줄을 주석처리 하니까
정상적인 JWT 출력은 되는 것 같다.(아니 아직 모른다. 쟤도 정상이 아닐수도 있다..)

분명 둘다....이메일만 받는데 왜 토큰이 다르게 나오지? 라는 고민을 했지만,
만료시간이 다르기에 토큰값이 다르게 나오는 것이다.
이것은 지금 의심할 필요는 없는 것 같다.
그럼 위 2줄 중 무엇이 문제였을까..
맨 위에 줄만 주석을 풀어보자.

이렇게 되면 이메일을 통해 사용자를 찾고,
그 사용자가 없으면 null값을 주고,
있으면 member를 찾고, 이메일을 통해 jwt를 만들어 정상 반환해주는 것이다.
다만 이렇게 되면 정상적인 pw검증이 되지 않은 상태이다.
지금은 pw 검증이 되지 않았지만 정상적인 jwt 생성 및 반환은 성공된 상태인데,
그럼 아마 이전상황에서는 위 29번줄에 문제가 있었던 모양인것 같다.
PW 검증 부분에 대한 검토를 시작해보도록 하자.

다음과 같이 29번 줄을 수정했다.
만약 두개의 PW가 틀리면 정상적인 JWT가 아니라, x,x를 넣어서 반환하라는 if문을 만들어서 실행했더니,

다음과 같이 진짜로 x,x가 나왔다.
문제는 저 29번줄이였던것으로 확인이 된것이다.
제대로된 코드 분석을 하기 전에 ,로그를 찍어 봄으로써 미리 힌트를 얻고자 하였다.

로그의 결과는 다음과 같다.
로그를 분석함으로써 문제를 해결해보자.

생각보다 더 어려운 문제인것 같다.
입력한 이메일,비밀번호가 DB에 저장된 이메일,비밀번호와 동일한 것이 로그로 확인이 되었는데,
문제가 아직 해결되지 않은것이다.
하지만 로그의 중간에 WARN부분을 보면 무언가 힌트가 있을 것 같다.
1.오류 분석
o.s.s.c.bcrypt.BCryptPasswordEncoder:
Spring Security의 BCryptPasswordEncoder 클래스에서 발생.
2.오류분석
"Encoded password does not look like BCrypt":
입력된 비밀번호가 BCrypt 방식으로 암호화되지 않았거나, 잘못된 형식임을 의미.
이유를 알것 같다.
로그를 보니, 비밀번호가 암호화 되지않고 그대로 저장이 된 것 같다.
회원 정보는 다음과 같이 출력되었다.
Member(userID=null, role=USER, registerDate=null, username=rsy5, email=rsy5@naver.com, password=eric1225, birth=1999-12-25)
결국 현재 문제점은 암호화되지 않은 비밀번호이며,
이것은 결국 AuthService.java의 다음코드에서 문제를 일으키는 것이라고 예상을 할 수 있다.
if (!passwordEncoder.matches(password, member.getPassword()))
{return new JwtTokenDto("x","x");}
왜냐하면 member.getPassword가 암호화 되지 않은 것이기에,
passwordEncoder.matches(rawPassword, encodedPassword) 메서드는 평문 비밀번호(rawPassword)를 암호화한 후, 그것과 암호화된 비밀번호(encodedPassword)를 비교한다.
따라서, member.getPassword()가 암호화되지 않은 상태라면, 비교 자체가 실패하여 false가 반환됩니다.
암호화가 되어 있지 않기 때문에,
email을 암호화한 값과 암호화가 되지 않은 email을 비교하니 당연히 항상 false가 나오는 것이다.
결론은 암호화만 하면 될 것이라는 예측을 할 수 있는 것이다.
그럼 암호화를 진행해보자.
다음과 같이 register 메서드를 구성해준다.
@Override
public Long register(MemberRequest request) {
Member member = request.toEntity();
// 비밀번호 암호화
String encodedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encodedPassword);
memberMapper.register(member);
return member.getUserID();
}
중요한 것은 비밀번호만 미리 암호화를 시켜준 후 set을 사용해서 비밀번호를 바꿔주고,
그 다음에 회원가입 로직을 Mybatis를 사용하여 진행하는 것이다.
이제 메서드를 실행해보자.
암호화 성공이다.

이전에 순전한 string값으로 저장되던 것들과 달리 $2a로 시작하는 암호들이 만들어지는 것을 알 수 있다.
그럼 이제 로그인을 진행해보자.


성공이다!!!
기존 로그인 메서드에서 추가할 것이 별게 없다.
우선 redistemplete를 가져와야 하기 때문에,
@Autowired
private RedisTemplate<String, String> redisTemplate;
로 템플릿을 autowired하고 ,
이메일 기반 로그인 서비스와
아이디 기반 로그인 서비스코드를 다음과 같이 수정해주면 된다.
/**
* 이메일 로그인 메서드
* @param email 이메일
* @param password 비밀번호
* @return JWT 토큰
*/
public JwtTokenDto email_Login(String email, String password) {
// Find member
Member member = memberMapper.findMemberByEmail(email);
// validation
if (member == null) {return new JwtTokenDto("no member","x");}
if (!passwordEncoder.matches(password, member.getPassword())) {return new JwtTokenDto("PW not match","x");}
// Generate JWT token
String AccessToken = jwtService.generateAccessToken(email);
String RefreshToken =jwtService.generateRefreshToken(email);
// Input AccessToken to Redis
redisTemplate.opsForValue().set("auth:" + member.getUserNumber(), AccessToken, Duration.ofHours(1));
// return JWT
return new JwtTokenDto(AccessToken,RefreshToken);
}
/**
* 아이디 로그인 메서드
* @param nickName 아이디
* @param password 비밀번호
* @return JWT 토큰
*/
public JwtTokenDto id_Login(String nickName, String password) {
// Find member
Member member = memberMapper.findMemberByNickname(nickName);
// validation
if (member == null) {return new JwtTokenDto("no member","x");}
if (!passwordEncoder.matches(password, member.getPassword())) {return new JwtTokenDto("PW not match","x");}
// Generate JWt token
String AccessToken = jwtService.generateAccessToken(member.getEmail());
String RefreshToken =jwtService.generateRefreshToken(member.getEmail());
// Input AccessToken to Redis
redisTemplate.opsForValue().set("auth:" + member.getUserNumber(), AccessToken, Duration.ofHours(1));
// return JWT
return new JwtTokenDto(AccessToken,RefreshToken);
}
redisTemplate.opsForValue().는 redis에 데이터를 set.get하는 기능을 지원한다.
포스트맨에서 다음과 같이 정상적으로 로그인 됨을 확인 가능하다.

그리고 Redis CLI에서 다음과 같이 정상적으로 JWT가 저장된 것을 확인할 수 있다.

혹시나 해서 여러명을 로그인 시켜보기도 하였다!

문제 없이 잘 작동이 된다.
정상 로그인 + Redis에 JWT정상 저장이다.