principal : Allows direct access to the principal object representing the current user
Authentication라는 객체가 들어가면 들어간 순간부터 세션에 값이 저장된다. principal는 Authentication라는 객체를 가져오는 것이다. 사용자가 로그인 요청을 하게 되면 AuthenticationFilter를 거치게 된다. 로그인 요청이 오면 이 필터가 제일 먼저 작동한다. 그래서 로그인 요청을 할때는 httpbody에 무엇을 담고 올까? username과 password를 당연히 가져올 것이다. 이 두가지를 갖고 UsernamePasswordAuthenticationToken을 만든다. 이 토큰을 갖고 AuthenticationManager는 그냥 username과 password를 받으면 안된다. AuthenticationToken을 UserDetailService에게 던진다. 그럼 UserDetailService는 username을 가지고 데이터베이스에서 해당 유저 네임으로 있는지 확인하고, 있으면 Authentication 객체를 만든다. 없으면 객체를 만들지 않는다. AuthenticationManger 가 pasword를 인코딩해서 비교하고 userDetailService에서 해당 user를 찾아서 해당 유저가 있는지를 확인을 해서 세션에 저장하는 역할을 한다. 세션이라는 메모리 공간이 있다. 세션에다가 유저 정보를 저장한다. 스프링 시큐리티는 세션에 어떤 특정 공간을 만들어서 스프링 시큐리티 컨텍스트라고 부른다. 여기다가 저장할 수 있는게 유저 오브젝트가 아니라 Authentication라는 객체를 저장해서 관리한다. 이 객체는 누가 만들냐면 AuthenticationManger라는 얘가 만들어준다. Manger가 Authentication 객체를 만들기 위해서는 유저 네임과 패스워드를 알아야 데이터베이스와 비교해보고 확인을 할 것이다.
첫번째로는 사용자가 로그인 요청을 한다. 로그인 요청을 하게 되면
username: lala
password:1234
위 정보를 날리면 필터가 가로챈다. 필터는 usernamePasswordAuthenticationToken을 만들어준다. usernamePasswordAuthenticationToken는 lala라는 username와 1234를 통해 만들어진다. 왜 굳이 만들까? 이 토큰을 만들어 Manger에게 던지면 Manger가 세션을 만들어주기 때문이다. 세션을 만들어주기 위한 조건이 있다. Manger는 username, lala 를 userDetailService에게 던진다. id와 password를 받는다. 그럼 필터가 낚아채서 토큰을 만든다. (이 토큰은 lala라는 아이디와 1234라는 패스워드를 토대로 만듦.) 그리고 이 토큰을 Manger에게 던진다. 던지는 목적은 Authentication 객체를 만들기 위해서이다. 이 객체를 만들기 위해서는 조건이 필요하다. 일단 lala라는 아이디가 데이터베이스에 있는지 확인해야 한다. lala라는 아이디가 있음을 확인하면 비밀번호까지 체크한다. 비밀번호는 1234 로 체크하면 안된다. 디크립트로 인코딩해서 해쉬로 암호화 된것을 확인한다. 확인하고 정확하게 있을 경우 Authentication 객체를 만든다. 만들어서 세션에 저장한다. 이 세션에 시큐리티 컨텍스트에 있는 유저 오브젝트를 저장하지 못하니까 (Authentication 객체만 저장할 수 있음) 이 과정은 Authentication 객체를 만들어서 세션에 저장하기 위한 흐름이다. 강제로 세션을 만들기 위해서는 이 로직을 타야 한다.
AuthenticationPrincipal 어노테이션을 스프링 시큐리티가 제공해준다. 이 어노테이션을 사용해서 현재 인증된 사용자의 Principal 정보를 참조할 수 있다. 여기서 말하는 Principal은 우리가 인증할 때 Authentication 에 들어있는 첫번째 파라미터 (account.getNickname())이다.
public void login(Account account) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
account.getNickname(),
account.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_USER")));
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(token);
}
Token 자체가 Authentication 으로 바뀌게 되는데 첫번째로 넘겨준 파라미터가 Principal 이다. 우리는 Principal 안에다가 account를 넣어주고 싶은 것이다.
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
로그인을 안한 상태에서 인증을 안한 상태에서 접근하는 경우에는 Principal이 anonymousUser라는 문자열이다. 이 경우에는 AuthenticationPrincipal를 사용하는 파라미터에 null을 넣어주고 실제로 인증된 사용자가 있는 경우에는 account 정보를 꺼내서 넣어주고 싶은 것이다.
첫 페이지로 가는 요청, 핸들러 만들기
package com.goodmoim.main;
import com.goodmoim.account.CurrentUser;
import com.goodmoim.domain.Account;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class MainController {
@GetMapping("/")
public String home(@CurrentUser Account account, Model model) {
if (account != null) {
model.addAttribute(account);
}
return "index";
}
}
CurrentUser 어노테이션으로 Account 타입을 받고 싶은 것이다. 스프링 시큐리티가 제공하는 기다란 AuthenticationPrincipal 어노테이션을 바로 사용하는 게 아니다. 단순히 Principal 만 꺼내는 게 아니라 Principal 을 약간 다이나믹하게 꺼내고 싶은 것이다. 익명 인증인 경우에는 null로, 아닌 경우에는 실제 account 객체로!
그래서 handler 안에서 null 체크하는 옵션을 넣어줄 수 있다. account가 null이 아니면 인증을 한 사용자이므로 model에다가 account를 넣어주고 아니면 넣어주지 않는다. view에서는 null인지 아닌지만 체크하면 된다.
커스텀 어노테이션이란?
프로그램에 관한 데이터를 제공하거나 코드에 정보를 추가할 때 사용하는 것을 어노테이션이라고 한다. 대표적인 어노테이션으로는 @Controller, @SpringBootApplication등이 있다. 하지만 위 예시 어노테이션들은 이미 만들어진 어노테이션들이고, 직접 커스텀해서 어노테이션을 만들 수 있는데, 이것을 커스텀 어노테이션이라고 한다.
CurrentUser.java
package com.goodmoim.account;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public interface CurrentUser {
}
public void login(Account account) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
account.getNickname(),
account.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_USER")));
SecurityContextHolder.getContext().setAuthentication(token);
}
하지만 지금 우리가 로그인 할때 사용한 Principal 에는 account라는 프로퍼티가 없다. 따라서 account라는 프로퍼티를 들고 있는 중간 역할을 해줄 수 있는 객체가 필요하다. 스프링 시큐리티가 다루는 유저 정보와 도메인에서 다루는 유저 정보의 사이의 갭을 매꿔주는 UserAccount를 만들자. 어뎁터라고 생각!
UserAccount
package com.goodmoim.account;
import com.goodmoim.domain.Account;
import lombok.Getter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.List;
@Getter
public class UserAccount extends User {
private Account account; // 우리가 갖고 있는 정보
public UserAccount(Account account) {
super(account.getNickname(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_USER")));
this.account = account;
}
}
extends User 는 스프링 시큐리티에서 오는 것이다. 스프링 시큐리티가 다루는 유저 정보를 우리가 갖고 있는 유저 정보와 연동을 해줄 것이다. UserAccount 를 Principal 객체로 사용할 것이다. 그래서 로그인 할때 사용하는 로직을 수정해준다.
AccountService.java
public void login(Account account) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
new UserAccount(account),
account.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_USER")));
SecurityContextHolder.getContext().setAuthentication(token);
}
이제 new UserAccount(account) 자체가 Principal 객체가 된다. 로그인에 성공했다면 new UserAccount(account) 이게 인증된 Principal로 간주가 될 것이다. 그럼 다시 MainController로 가서 CurrentUser 어노테이션을 보면 , 이 어노테이션을 보고 현재 사용자가 익명 사용자라면 anonymousUser 라는 문자열이 된다. 그래서 null로 간주된다. 그게 아니라면 UserAccount의 객체라는 것이다.
❔❔ MainController에서 UserAccount로 받거나, 도메인 타입으로 받을 경우에는 UserAccount.java에서 도메인 타입 (private AAccount acocunt;)를 getter를 통해서 꺼내야 한다.
<div class="alert alert-warning" role="alert" th:if="${account != null && !account.emailVerified}">
스터디올레 가입을 완료하려면 <a href="#" th:href="@{/check-email}" class="alert-link">계정 인증 이메일을 확인</a>하세요.
</div>
위 화면은 account 정보가 null 이므로 (뷰에 전달된 account 정보가 없으므로 ) alert 창이 뜨지 않는다.
가입 후 로그인 시 alert 창이 뜨는 것을 확인할 수 있다.
이메일 인증을 하고 다시 메인 페이지로 돌아가면 다음과 같은 alert 화면이 없어진 모습을 볼 수 있다.
원래는 이메일 체크를 안했을 때 경고창에서 어떤 일이? 링크를 클릭하면 이메일을 확인하라는 알림창으로 가야하고, 거기서 이메일 다시 보내기 버튼이 있어야 한다. 이메일을 재전송하는 기능이 있어야 한다. 지금 하지 않고 개인 과제로 남기겠다.
checked-email.html과 비슷함, 인증 메일을 보냈으니, 인증 메일을 확인하세요 라는 뷰 + 이메일 재전송 버튼 필요
출처 : 인프런 백기선님의 스프링과 JPA 기반 웹 애플리케이션 개발
유튜버 데어 프로그래밍님의 Springboot - 나만의 블로그 만들기
https://shinsunyoung.tistory.com/83