스프링 부트 - 로그인 및 사용자 인증 정보 참조

Spring

Posted by kwon on 2020-04-30

현재 인증된 사용자 정보 참조

  • 스프링 시큐리티의 스프링 웹 MVC 지원
    • @AuthenticationPrincipal : 핸들러 매개변수로 현재 인증된 Principal 을 참조할 수 있다.
    • Principal 은 인증 시 authentication에 들어있는 첫 번째 파라미터이다. 아래의 account.getNickname()에 해당.
  • AccountService.java
    1
    2
    3
    4
    5
    6
    7
    8
    public void login(Account account) {
    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( // 토큰 생성
    account.getNickname(),
    account.getPassword(),
    List.of(new SimpleGrantedAuthority("ROLE_USER")));
    // 로그인 처리
    SecurityContextHolder.getContext().setAuthentication(token);
    }
  • @AuthenticationPrincipal은 SpEL을 사용해서 Principal 내부 정보에 접근할 수도 있다.
    • @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
    • 익명 인증인 경우에는 null로 설정하고, 아닌 경우에는 account 프로퍼티를 조회해서 설정하라는 뜻.
  • @AuthenticationPrincipal를 사용하면 UserDetailsService에서 return한 객체를 파라미터로 직접 받아 사용할 수 있다.
  • @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")와 같이 매번 파라미터에 붙이기는 번거롭기 때문에 @CurrentUser라는 커스텀 어노테이션을 생성한다.
  • CurrentUser.java : 어노테이션 생성
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 런타임 까지 유지
    @Retention(RetentionPolicy.RUNTIME)
    // 타겟은 파라미터에만 붙이겠다.
    @Target(ElementType.PARAMETER)
    // 익명 사용자인 경우에는 null로, 익명 사용자가 아닌 경우에는 실제 account 객체로
    // Principal 을 다이나믹 하게 꺼내기 위해 @CurrentUser 생성
    @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
    public @interface CurrentUser {
    }
  • MainController.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Controller
    public class MainController {
    @GetMapping("/")
    // 익명 사용자인 경우에는 null 로, 익명 사용자가 아닌 경우에는 실제 account 객체로
    // Principal 을 다이나믹 하게 꺼내기 위해 @CurrentUser 생성
    public String home(@CurrentUser Account account, Model model){
    if(account != null){ // null 이 아니면 인증을 한 사용자
    model.addAttribute(account);
    }
    return "index";
    }
    }
  • 익명 사용자인 경우에는 null을, 익명 사용자가 아닌 경우에는 실제 account 객체를 가져온다.
  • 하지만 지금 로그인 할 때 사용한 Principal에는 Account라는 프로퍼티가 없다.
  • 따라서 Account라는 프로퍼티를 가지고 있는 중간 역할을 해줄 수 있는 객체가 필요하다.
  • 핸들러에서 현재 로그인한 유저의 정보가 필요할 때 위와 같이 사용할 수 있다.
  • UserAccount.java : 스프링 시큐리티가 다루는 유저 정보와 우리의 도메인에서 다루는 유저 정보 사이의 갭을 매꿔주는 일종의 어댑터 역할
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Getter
    public class UserAccount extends User { // 스프링 시큐리티의 User를 상속

    private Account account;
    // 스프링 시큐리티가 다루는 유저 정보를 우리가 가지고 있는 도메인의 유저 정보와 연동
    public UserAccount(Account account) {
    super(account.getNickname(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_USER")));
    this.account = account;
    }
    }
  • 스프링 시큐리티의 User를 상속받는다.
  • UserAccount의 멤버는 Account 객체만이 존재한다.
  • 생성자의 내부에서 User 클래스의 생성자를 호출하여 username, password, role을 세팅한다.
  • AccountService.java 의 Principal에 위에서 생성한 AccountUser 객체를 넣는다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void login(Account account) {

    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( // 토큰 생성
    new UserAccount(account), // Principal 객체설정
    account.getPassword(),
    List.of(new SimpleGrantedAuthority("ROLE_USER")));
    // 로그인 처리
    SecurityContextHolder.getContext().setAuthentication(token);
    }
  • 로그인을 하게 되면 new UserAccount(account)가 인증된 Principal로 간주된다.

로그인 / 로그아웃

  • 스프링 시큐리티 로그인/로그아웃 설정
  • SecurityConfig.java
    1
    2
    3
    4
    5
    http.formLogin()
    .loginPage("/login") // 커스텀한 로그인 페이지를 보여줄 url
    .permitAll(); // 로그인 폼에 대한 접근 권한
    http.logout()
    .logoutSuccessUrl("/"); // 로그아웃 성공 시 이동할 url
  • 위와 같이 설정하고 login.html의 form에서 th:action="@{/login}" method="post"와 같이 설정한다면 post로 가는 /login 요청을 formLogin을 처리하는 시큐리티가 처리해준다.
    • 따로 /login post를 처리하는 핸들러를 만들 필요가 없음.
      • PasswordEncoder도 Bean으로 등록이 되어있다면 자동으로 사용이 된다. (하나만 있는 경우)
    • 알아서 username과 password를 가지고 로그인을 처리한다. 이 때, 데이터베이스에 저장된 정보를 참조하여 인증을 하여야 하기 때문에 데이터베이스를 조회할 수 있는 UserDetailsService를 구현해야 한다.
  • UserDetailsService 구현
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Service
    @RequiredArgsConstructor
    public class AccountService implements UserDetailsService {

    private final AccountRepository accountRepository;

    @Override
    public UserDetails loadUserByUsername(String emailOrNickname) throws UsernameNotFoundException {
    Account account = accountRepository.findByEmail(emailOrNickname);
    if(account == null){ // 이메일로 찾지 못한 경우 닉네임으로 찾는다.
    account = accountRepository.findByNickname(emailOrNickname);
    }
    if(account == null){ // 닉네임으로도 찾지 못한다면 에러를 던짐
    throw new UsernameNotFoundException(emailOrNickname);
    }
    // Principal 에 해당하는 객체를 리턴한다.
    return new UserAccount(account);
    }
    }
  • loadUserByUsername()
    • 상세 정보를 조회하는 메서드이며, 사용자 계정정보와 권한을 갖는 UserDetails인터페이스를 반환해야 한다.
    • 매개변수는 로그인 시 입력한 아이디이다. 엔티티의 PK를 뜻하는 것이 아니고, 유저를 식별할 수 있는 어떠한 값을 의미한다. 스프링 시큐리티에서는 username라는 이름으로 사용한다.
      • 로그인 form에서 아이디에 해당하는 값의 name=”nsername”으로 요청해야 한다.
  • UserDetailsService타입의 빈이 하나만 있으면 스프링 시큐리티에 따로 설정이 필요없다. 자동으로 저 빈을 사용한다.
  • UserDetailsService에서 return하는 객체는 UserDetails타입이어야 한다.
  • 따라서 UserDetails를 구현하는 User클래스를 상속받은 UserAccount를 리턴한다.

참조
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard
https://pupupee9.tistory.com/137
https://victorydntmd.tistory.com/328