사실 개인으로 진행하는 프로젝트들은 사용자가 많지않은 이상 굳이 토큰을 이용한 인증방식을 이용할 필요 없이 세션을 활용하면 되지만, (보안을 크게 신경쓰지 않거나 사용자가 많지 않으면 딱히 토큰 인증방식을 채택할 이유는 없다고 한다.)
어디서 그런 글을 보았다. 요즘 실무에서 가장 많이 쓰이는 인증 방식이 토큰을 이용한 인증방식이라는 글을.. 개발에도 트렌드가 있다고 하더라. (물론 어디서 줏어들은거라 신뢰도는 높지 않다.)
기존에 프로젝트에 세션을 활용한 로그인처리를 스프링시큐리티를 통해서 간단하게 구현했었다. (사실 JSESSIONID 를 활용한 세션인증은 따로 내가 크게 신경쓸 부분 없이 스프링 시큐리티가 알아서 다 해준다고 봐도 무방하다.)
공부 + 나중에 어디선가에 쓰일 수 있기 때문에 미래를 대비해 내가 개인적으로 만들어놓은 게시판에도 JWT토큰 인증방식을 구현해보기로 했다.
회원가입기능은 단순히 사용자 계정 엔티티 클래스를 만든 후에, 입력된 값중에 비밀번호를 암호화해서 집어넣으면 끝이다.
@Getter
@ToString
@Table(indexes = {
@Index(columnList = "email", unique = true),
@Index(columnList = "createdAt"),
@Index(columnList = "createdBy")
})
@Entity
public class UserAccount extends AuditingFields {
@Id
@Column(length = 50)
@Setter
private String userId;
@Setter @Column(nullable = false) private String userPassword;
@Setter @Column(length = 100) private String email;
@Setter @Column(length = 100) private String nickname;
@Setter private String memo;
@Column(name = "role")
@Enumerated(EnumType.STRING)
private UserAccountRole role = UserAccountRole.USER;
public UserAccount() {}
private UserAccount(String userId, String userPassword, String email, String nickname, String memo) {
this.userId = userId;
this.userPassword = userPassword;
this.email = email;
this.nickname = nickname;
this.memo = memo;
}
public static UserAccount of(String userId, String userPassword, String email, String nickname, String memo) {
return new UserAccount(userId, userPassword, email, nickname, memo);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserAccount that)) return false;
return userId.equals(that.userId);
}
@Override
public int hashCode() {
return Objects.hash(userId);
}
}
따로 식별옹 Id 를 만들지 않고, 유저 아이디 자체를 primary key 로 설정했다.
(테이블 어노테이션에 입력되어있는 값중에 필드에 선언이 안된 이름이 보인다. 해당 컬럼값들은 따로 AuditingFields 인터페이스를 만들어 공통적으로 입력되는 값들은 밖으로 빼두었다.)
public interface UserAccountRepository extends JpaRepository<UserAccount,String> {
Optional<UserAccount> findByEmail(String email);
Optional<UserAccount> findByNickname(String nickname);
}
이렇게 생성된 엔티티를 관리할 레포지토리를 생성해준다. 따로 JpaRepository는 디테일한 엔티티 검색도 지원해준다. 따로 내가 재정의를 하지 않아도 이렇게 선언하는 것만으로 메소드를 구현해준다.
public record UserAccountDto(
String userId,
String userPassword,
String email,
String nickname,
String memo,
LocalDateTime createdAt,
String createdBy,
LocalDateTime modifiedAt,
String modifiedBy
) {
public static UserAccountDto of(String userId, String userPassword, String email, String nickname, String memo) {
return new UserAccountDto(userId, userPassword, email, nickname, memo, null,null,null,null);
}
public static UserAccountDto of(String userId, String userPassword, String email, String nickname, String memo, LocalDateTime createdAt, String createdBy, LocalDateTime modifiedAt, String modifiedBy) {
return new UserAccountDto(userId, userPassword, email, nickname, memo, createdAt, createdBy, modifiedAt, modifiedBy);
}
public static UserAccountDto from(UserAccount entity) {
return new UserAccountDto(
entity.getUserId(),
entity.getUserPassword(),
entity.getEmail(),
entity.getNickname(),
entity.getMemo(),
entity.getCreatedAt(),
entity.getCreatedBy(),
entity.getModifiedAt(),
entity.getModifiedBy()
);
}
public UserAccount toEntity() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return UserAccount.of(
userId,
userPassword,
email,
nickname,
memo
);
}
}
인텔리제이의 JPA BUDDY 를 사용하면 원하는 엔티티의 DTO를 따로 생성해준다.
그걸 토대로 주고받고싶은 값들을 디테일하게 만들어주면 된다.
public class LoginDto {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
그리고 로그인할때 넣어줄 RequestDto 도 생성해주자.
이렇게 가장 기초적인 부분을 먼저 작성했으면, 그 다음으로 시큐리티 디펜던시를 추가해준 후에 스프링 시큐리티 관련 설정 파일을 작성해준다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").permitAll()
.and()
.csrf().ignoringAntMatchers("/h2-console/**")
.and()
.headers()
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
;
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
} //Bcrypt방식으로 암호화를 해주는 메소드
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder());
} //로그인 시에 입력한 비밀번호를 암호화 한 후에 인증을 처리함
대충 필터체인은 Spring Security Config 작성해보기 을 참고하면 좋다.
스프링 시큐리티는 formlogin 을 활성화 하면 알아서 뷰를 제공해주기도 한다. 회원가입 폼만 따로 만든다면 로그인폼은 따로 작성하지 않아도 된다.
이제 로그인까지 가능하게 구조를 설계했으면 설계한 것들을 활용해 회원가입 기능을 구현해보자.
@Transactional
public void saveUserAccount(UserAccountDto user) {
if(userAccountRepository.findById(user.userId()).isPresent()){
throw new IllegalArgumentException("이미 존재하는 아이디입니다.");
}
if(userAccountRepository.findByEmail(user.email()).isPresent()){
throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
}
if(userAccountRepository.findByNickname(user.nickname()).isPresent()){
throw new IllegalArgumentException("이미 존재하는 닉네임입니다.");
}
String password = user.userPassword();
UserAccount account = userAccountRepository.save(user.toEntity());
account.setUserPassword(new BCryptPasswordEncoder().encode(password));
}
단순하게 이미 존재하는 아이디/이메일/닉네임을 입력하면 Exception을 발생하게 하고, 모든 조건에 만족했다면 비밀번호를 암호화해서 저장해준다.
DTO 대신 도메인 모델을 계층간 전달에 사용하면, UI 계층에서 도메인 모델의 메소드를 호출하거나 상태를 변경시킬 수 있다. 또한 UI화면마다 사용하는 도메인 모델의 정보는 상이하다. 하지만 도메인 모델은 UI에 필요하지 않은 정보까지 가지고 있다. 이런 모든 도메인 모델 속성이 외부에 노출되면 보안 문제가 발생할 수 있다. 즉, 도메인 모델을 캡슐화 하여 보호할 수 있다.
DTO의 개념과 사용범위
이전에 해당 부분을 정리하면서 참고했던 글인데, 참 좋은것같다 😇
비즈니스 로직을 설계했다면 컨트롤러를 설계하자
지금 당장 구현할 부분은 회원가입이기 때문에 회원가입 관련 부분만 신경쓰면 된다
@GetMapping("/signup")
public String signup(UserCreateForm userCreateForm) {
return "user/signup_form";
}
@PostMapping("/signup")
public String signup(UserAccountDto dto) {
userService.saveUserAccount(UserAccountDto.from(account));
return "redirect:/";
}
본인은 다른 방법으로 @Valid 어노테이션을 이용해 따로 폼을 만들어 해당 조건에 적합하지 않은 입력값이 들어오면 경고문을 출력하도록 설정했는데, 지금 할 필요는 없다. 구현이 목적이니 사용자 인터페이스는 나중에 생각하자!
다음 글에서 이어집니다.