엔티티 정보를 Principal에 포함하기
UserDetails 구현체
@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;
}
}
엔티티 정보를 포함해 AuthenticationToken의 Principal로 들어감
@CurrentAccount
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public @interface CurrentAccount {
}
Spring sqEl을 통해 SecurityContext에서 로그인 유무 판단해 AuthenticationToken에서 null 이나 엔티티 반환
컨트롤러
@GetMapping("/check-email")
public String checkEmail(@CurrentAccount Account account, Model model){
model.addAttribute("email", account.getEmail());
return "account/check-email";
}
로그인 시에만 접근가능한 컨트롤러에서 Custom UserDetails 구현체와 Custom Annotation으로 AuthenticationToken 내에 엔티티 정보를 저장하고 꺼내와서 사용할 수 있다.
Spring 테스트시에 생성자 주입 X, 필드 주입 O
생성자로 의존성 주입시에 JUnit이 의존성에 끼어들게 된다. 스프링을 이용한 테스트를 할 때는 필드주입으로
RememberMe 쿠키
사용자가 서버에 인증시에 웹브라우저에 세션과 달리 저장해 이후 세션이 만료되거나 삭제했을 때도 사용자를 기억할 수 있게 한다.
암호화 시그니처를 사용한 토큰 기반의 방식과 데이터베이스를 사용한 영구 토큰 기반의 방식이 있는데 후자를 선택한다.
영구 토큰 기반의 방식은 발급받을 때마다 일정한 고정 값과 달라지는 랜덤값 함께 사용해 공격자가 쿠키를 탈취해서 로그인할 때 쿠키가 달라지고, 이후에 원래 사용자가 이전의 쿠키를 통해 로그인을 하면 서버에서는 일정한 고정값을 통해 모든 쿠키를 삭제한다. 이후는 해커든 사용자든 폼로그인을 통해 로그인을 해야해서 비교적 높은 보안성을 가진다.
기본적으로 커스텀을 하지 않으면 SpringBoot와 Spring Security의 조합으로 SeuciryConfig 설정과 Token Entity, UserDetailService 구현하면 사용할 수 있다.
1. Security Config
// Security Configuration
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//...
http.formLogin()
.loginPage("/login").permitAll();
http.rememberMe()
.userDetailsService(accountService)
.tokenRepository(tokenRepository());
return http.build();
}
@Bean
public PersistentTokenRepository tokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
2. PersistenceLogins Entity
@Table(name = "persistent_logins")
@Entity
@Getter
@Setter
public class PersistentLogins {
@Id
@Column(length = 64)
private String series;
@Column(nullable = false, length = 64)
private String username;
@Column(nullable = false, length = 64)
private String token;
@Column(name = "last_used", nullable = false, length = 64)
private LocalDateTime lastUsed;
}
3. UserDetailService
public class AccountService implements UserDetailsService {
private final AccountRepository accountRepository;
// ...
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String nicknameOrEmail) throws UsernameNotFoundException {
Account account = accountRepository.findByEmail(nicknameOrEmail);
if(account == null){
account = accountRepository.findByNickname(nicknameOrEmail);
}
if(account == null){
throw new UsernameNotFoundException(nicknameOrEmail);
}
return new UserAccount(account);
}
4. Login View RememberMe CheckBox 추가
<form> // Login Form
// ..
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="rememberMe" name="remember-me" checked>
<label class="form-check-label" for="rememberMe" aria-describedby="rememberMeHelp">로그인 유지</label>
</div>
</form>
참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard