Framework & Library/Spring

[Spring] Spring Security PrincipalDetails 와 mustache template

마볼링 2023. 2. 5. 01:43

📍 Spring Security PrincipalDetails

  • 스프링 시큐리티는 /login 주소 요청이 오면 해당 요청을 낚아채서 로그인을 진행시킬 수 있다.
    => WebSecurityConfigurerAdapter를 상속받아 configure을 override 한후 다음과 같이 작성한다.
@EnableWebSecurity // 해당 파일로 시큐리티를 활성화
@Configuration // IoC
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
      // super 삭제 - 기존 시큐리티가 가지고 있는 기능이 다 비활성화됨
      // super.configure(http);
      http.authorizeRequests()
         .antMatchers("/","/user/**","/image/**","/subscribe/**","/comment/**","/api/**")
         .authenticated()
         .anyRequest().permitAll()
         .and()
         .formLogin()
         .loginPage("/auth/signin") // GET
         .loginProcessingUrl("/auth/signin") // POST -> 스프링 시큐리티가 로그인 프로세스 진행
         .defaultSuccessUrl("/");
   }
}
  • antMatchers("주소").authenticated() : 해당 주소들을 로그인 하지 않은 상태로 접근 불가능
  • anyRequest().permitAll() : 나머지 주소들을 접근 가능
  • formLogin() : 스프링 시큐리티 인증 실행
  • loginPage("/auth/signin") : "/auth/signin" 따로 만든 로그인 페이지로 가는 주소
  • loginProcessingUrl("/auth/signin") : POST 방식으로 접근하면 로그인 프로세스가 진행
  • defaultSuccessUrl("/") : 로그인 완료 후 이동되는 주소

"/auth/signin" 주소로 POST 방식의 데이터를 보내면 로그인 프로세스가 진행되므로

따로 컨트롤러를 만들 필요가 없다.

<form class="login__input" action="/auth/signin" method="post">
    <input type="text" name="username" placeholder="유저네임" required="required" />
    <input type="password" name="password" placeholder="비밀번호" required="required" />
    <button>로그인</button>
</form>

 

📍UserDetails, UserDetailsService

단순히 스프링 시큐리티에서 로그인을 구현하면 username과 password 두 개만 받아오게된다.

따라서, UserDetails과 UserDetailsService를 상속받은 각각의 class를 생성하여 커스텀 해준다.

 

UserDetailsService 란?

Spring Security에서 유저의 정보를 가져오는 인터페이스이다.

userRepository에서 username으로 찾은 엔티티를 PrincipalDetails 엔티티로 만들어서 리턴해준다.

@RequiredArgsConstructor
@Service
public class PrincipalDetailsService implements UserDetailsService {

   private final UserRepository userRepository;

   // 1. 패스워드는 알아서 체킹하니까 신경쓸 필요 없다.
   // 2. 리턴이 잘되면 자동으로 세션을 만든다.
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      User userEntity = userRepository.findByUsername(username);
      if(userEntity == null) {
         return null;
      } else {
         return new PrincipalDetails(userEntity);
      }
   }
}

 

UserDetails 란?

Spring Security에서 사용자의 정보를 담는 인터페이스이다.

User를 받는 생성자를 만들고 Override된 메소드들을 알맞게 작성해준다.

@Data
public class PrincipalDetails implements UserDetails {

   private User user;

   public PrincipalDetails(User user) {
      this.user = user;
   }

   // 권한 : 한개가 아닐 수 있음. (3개 이상의 권한)
   @Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
      Collection<GrantedAuthority> collection = new ArrayList<>();
      collection.add(new GrantedAuthority() {
         @Override
         public String getAuthority() {
            return user.getRole();
         }
      });
      return collection;
   }

   @Override
   public String getPassword() {
      return user.getPassword();
   }

   @Override
   public String getUsername() {
      return user.getUsername();
   }

   @Override
   public boolean isAccountNonExpired() {
      return true;
   }

   @Override
   public boolean isAccountNonLocked() {
      return true;
   }

   @Override
   public boolean isCredentialsNonExpired() {
      return true;
   }

   @Override
   public boolean isEnabled() {
      return true;
   }
}

 

📍 @AuthenticationPrincipal

  • 로그인이 성공하면 컨트롤러에서 @AuthenticationPrincipal를 이용하여 세션에 저장된 로그인 정보를 사용할 수 있다.
  • 사용예시 (프로필 정보 출력)
@GetMapping("/user/{pageUserId}")
public String profile(@PathVariable int pageUserId, Model model, @AuthenticationPrincipal PrincipalDetails principalDetails) {
   UserProfileDto dto = userService.회원프로필(pageUserId, principalDetails.getUser().getId());
   model.addAttribute("dto", dto);
   if(pageUserId == principalDetails.getUser().getId()) {
      model.addAttribute("check",true);
   } else {
      model.addAttribute("check", false);
   }
   return "user/profile";
}

@AuthenticationPrincipal PrincipalDetails principalDetails에서

아까 User를 받은 생성자가 있었으니 로그인 정보는 principalDetails.getUser()로 받을 수 있다.

 

📍 Mustache에서 문제 발생

header 부분에 로그인한 사용자 프로필페이지로 이동하는 버튼이 있어, 로그인한 사용자

즉, 세션 정보가 모든 페이지마다 필요하다.

그렇다면 위에  @AuthenticationPrincipal PrincipalDetails principalDetails를 모든 컨트롤러에서

사용해야 하는건가?

JSP는 태그 라이브러리를 사용하면 JSP에서 받아올 수 있지만 mustache는 비슷한 기능이 없다.

<sec:authorize access="isAuthenticated()">
    <sec:authentication property="principal" var="principal"/>
</sec:authorize>

 

> 임시 작업

로그인시 HttpSession을 이용하여 추가로 userId 세션을 생성한다.

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
   User userEntity = userRepository.findByUsername(username);
   if(userEntity == null) {
      return null;
   } else {
      session.setAttribute("userId", userEntity.getId());
      return new PrincipalDetails(userEntity);
   }
}

그리고 mustache에서 세션 정보를 받아올 수 있도록 

application.properties에 아래줄을 추가한다.

spring.mustache.expose-session-attributes=true

그러면 로그인시 어디서든 userId를 mustache에서 사용할 수 있다.

<header class="header">
    <div class="container">
        <a href="/" class="logo">
            <img src="/images/logo.jpg" alt="">
        </a>
        <nav class="navi">
            <ul class="navi-list">
                <li class="navi-item"><a href="/">
                    <i class="fas fa-home"></i>
                </a></li>
                <li class="navi-item"><a href="/image/popular">
                    <i class="far fa-compass"></i>
                </a></li>
                <li class="navi-item"><a href="/user/{{userId}}">
                    <i class="far fa-user"></i>
                </a></li>
            </ul>
        </nav>
    </div>
</header>