스프링 시큐리티 로그인 코드 작성
로그인 완료 버튼을 폼 안쪽에 넣었기 때문에 버튼을 누르면 action에 있는 주소가 실행된다.
즉 여기서는 (name) username과 password 이 두 개의 값을 가지고 /auth/loginProc의 주소로 이동한다.
+ 추가
위 사진의 밑줄 친 loginPro를 loginProc로 수정한다.
그런데 현재 UserApiController.java에 /auth/loginPro가 만들어져 있지 않다.
여기서 /auth/loginPro를 만들지 않는 이유는 스프링 시큐리티가 이 로그인 요청을 가로채게 할 것이기 때문이다.
그걸 이제 어떻게 할 것인가?
SecurityConfig.java로 가서 다음과 같이 코드를 작성하자
스프링 시큐리티가 로그인을 가로채서 대신 로그인을 하고, 성공 시 해당 링크를 이동하게 한다.
사용자가 요청한 username과 password를 가로채서 로그인을 해야되는데,
UserDetails를 가지고 있는 유저 오브젝트를 하나 만들어야 한다. (UserDetails 타입의 유저 오브젝트)
그 이유는 얘가 로그인 요청을하고 세션을 등록해주는데
그 때 유저 오브젝트를 등록할 수가 없다. 타입이 맞지 않기 때문이다.
타입이 UserDetails 타입이여야 한다. 따라서 이걸 만들어보자
config 패키지 아래에 auth 패지키를 생성하고 그 안에 PrincipalDetail 클래스를 작성한다.
다음과 같이 implements UserDetails를 작성하고,
( extends : 부모에서 선언 / 정의를 모두하며 자식은 메소드 / 변수를 그대로 사용할 수 있다.
implements (interface구현) : 부모 객체는 선언만 하며 정의(내용)은 자식에서 오버라이딩(재정의) 해서 사용해야 한다. )
해당 커서에서 Source - Override/Implement Methods... 를 클릭한다.
모두 체크되어있는지 확인하고 OK를 누른다.
얘는 일단 잘라내기하고 맨 아래쪽으로 이동시키자
그리고 null을 지우고 제대로 리턴시켜주자
PrincipalDetail.java
package com.cos.blog.config.auth;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.cos.blog.model.User;
// 스프링 시큐리티가 로그인 요청을 가로채서 로그인을 진행하고 완료되면 UserDetails 타입의 오브젝트를
// 스프링 시큐리티의 고유한 세션저장소에 저장을 해준다.
public class PrincipalDetail implements UserDetails {
private User user; // 콤포지션
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// 계정이 만료되지 않았는지 리턴한다. (true : 만료안됨)
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정이 잠겨있지 않은지 리턴한다. (true : 안잠겨있음)
@Override
public boolean isAccountNonLocked() {
return true;
}
// 비밀번호가 만료되지 않았는지 리턴한다. (true : 만료안됨)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정이 활성화(사용가능)인지 리턴한다. (true : 활성화)
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return true;
}
// GrantedAuthority 을 상속한 Collection 타입
// 계정이 갖고 있는 권한 목록을 리턴한다. (권한이 여러개 있을 수 있어서 루프를 돌아야한다. 여기선 한개만..)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>();
collectors.add(()->{ return "ROLE_"+user.getRole();});
// 이거 "ROLE_"인데 "Role_"로 적었다가 에러 떠서 30분간 찾았음.. 대소문자 항상 조심ㅠ
return collectors;
}
/*
* collectors.add(new GrantedAuthority() { // 자바는 오브젝트는 담을 수 있지만 메서드만 넘길 순 없다.
* // 따라서 new GrantedAuthority() 필요 // 익명 클래스가 만들어지고
*
* @Override // 추상 메서드 오버라이딩 public String getAuthority() { return
* "Role_"+user.getRole(); // ROLE_USER } });
*/
}
이렇게 람다식을 사용하면 상당히 편하다..
SecurityConfig.java로 가자
해당 password가 뭘로 해쉬가 되어 회원가입 되었는지를 알아야
같은 해쉬로 암호화해서 DB에 있는 해쉬랑 비교할 수가 있으니까 해당 코드를 작성해보자
그런데 이 null 자리에 들어갈 오브젝트를 아직 만들지 않았다.
따라서 이 오브젝트를 만들어보자
이렇게 loadUserByUsername 함수를 Override 해줘야한다.
loadUserByUsername 함수는 username이 DB에 있는지 확인하는 처리를 한다.
username이 DB에 있는지 확인을 해야하는데 현재 userRepository에 그런 함수가 없다.
findById는 id를 말하는거고 여기선 findByUsername이 필요하다.
그럼 UserRepository에 가서 findByUsername 함수를 만들어보자
왜 이름이 findByUsername으로 하냐면 findBy~로 작성하면 JPA 네이밍 전략을 사용할 수 있기 때문이다.
UserRepository.java
userRepository에 DB에서 username을 확인해 조회하는 findByUsername 함수를 작성했으니
다시 PrincipalDetailService로 돌아와서,
PrincipalDetailService.java
아까 작성했던 findByUsername를 사용한다.
orElseThrow()를 사용해 다음과 같이 예외처리를 한다.
UserDetails 타입이니 user를 리턴할 수는 없고, PrincipalDetail 타입을 리턴해준다.
그런데 PrincipalDetail을 가보면
현재 user가 NULL이라 생성자를 만들어줘야 한다.
그리고 다시 PrincipalDetailService로 돌아가서 principal을 넣어준다.
그러면 이제 로그인이 진행될 때 loadUserByUsername 함수가 자동으로 실행되면서
findByUsername에서 username을 찾고 만약에 해당 유저를 찾았다면 principal에 User오브젝트가 리턴이되고
그것을 밑줄친 principal에 담는다. 그렇게 PrincipalDetail을 리턴한다.
이 때 시큐리티의 세션에 유저 정보가 저장이 된다. 그때가 UserDetails 타입이여야 한다.
이제 loadUserByUsername을 통해서 로그인을 하게된다.
만약 오버라이딩해서 이 부분을 안 만들었다면 우리가 들고있는 유저 정보를
여기다가 담아줄 수가 없다.
즉, 위에서 loadUserByUsername 이 부분을 작성하지 않으면
우리가 작성하는 유저가 아니라 시큐리티에서 기본적으로 제공하는
아이디인 user와 콘솔창에서 뜨는 암호가 아이디와 비밀번호가 된다.
이제 SecurityConfig로 가보자
그럼 이제 이 null은 뭐라고 바꿔서 작성해주어야할까?
정답은 principalDetailService이다.
principalDetailService를 통해서 로그인할 때 password를 encodePWD()로 인코드해서
해당 코드로 비밀번호를 비교하게 된다.
이 코드를 작성하지 않으면 password 비교를 못한다.
스프링 시큐리티 로그인 과정 정리
우선 로그인 요청이 오는 순간
SecurityConfig.java에서 해당 주소("/auth/loginProc")를 스프링 시큐리티가 가로채게 된다.
가로챈 username과 password 정보를 PrincipalDetailService에 있는 loadUserByUsername로 던진다.
1) 던져서 username이 들어와서
2) 비교를 해서
3) 리턴한다.
그런데, 이렇게 해당 오브젝트(principal)을 PrincipalDetail에 담아서 이렇게 리턴할 때
등록해둔 이것을 통해서 principalDetailService가 로그인 요청을 하고
loadUserByUsername에서 new PrincipalDetail(principal)이 리턴이 되면
passwordEncoder를 해서 사용자가 적은 password를 다시 encodePWD()로 암호화해서 DB랑 비교하고
비교가 끝나고 username과 password가 DB에 있는 것을 확인하면
스프링 시큐리티 영역에 유저 정보가 저장된다.
이 유저 정보가 PrincipalDetail로 감싸서 저장된다.
이렇게 로그인이 완료되면
"/"로 이동한다.
UserController에서는 이렇게 세션을 찾을 수가 있다.
이제 잘 되는지 실행을 해보자
로그인 오류 해결
다음과 같은 오류가 발생한다.
??
뭔가 잘못된 것 같으니 PrincipalDetail에 가보니까
"ROLE_" 이라고 적어야하는데 "Role_"이라고 오타를 내서 발생한 오류였나보다 ㅠㅠ
이걸 "ROLE_"로 수정하고
이제 다시 실행해보면
저한테 왜이러시죠
아 1시간 동안 찾았네
...
너무 복잡하게 생각했던 것 같다.
PrincipalDetailService에서 콘솔창 찍어보니 아예 전달이 안되고 있다는 것을 깨닫고
로그인폼을 확인해보니 form action에서 오타 한글자 때문이였다.
변수나 주소 오타안내게 진짜 꼼꼼하게 확인하면서 코드짜야겠다는 데이터를 얻었다.
시큐리티 로그인 실행 결과
현재 DB에 daramG / 1234 저장되어있다.
두근두근
성공적으로 시큐리티 로그인이 동작하는 것을 확인할 수 있다.

+ 추가
이제 홈페이지로 갈 때는 굳이 로그인이 필요 없으므로 위의 테스트가 끝났다면
괄호안의 @AuthenticationPrincipal PrincipalDetail principal를 다시 지운다.
학습자료 : https://youtu.be/pHp2LGuukls
'자바 스프링 > 부트 블로그 JPA 프로젝트' 카테고리의 다른 글
#25 CRUD 게시판 - Read (목록,페이징,상세보기) (2) | 2022.05.16 |
---|---|
#24 CRUD 게시판 - Create (글 작성하기) (0) | 2022.05.15 |
#22 스프링 시큐리티2 (0) | 2022.05.13 |
#21 스프링 시큐리티 (0) | 2022.05.12 |
#20 기존 방식의 로그인 (0) | 2022.05.12 |