UserDetails
- UserDetails는 Spring Security에서 사용자를 나타내는 인터페이스이다.
- 사용자 이름, 비밀번호, 권한 등 사용자 인증 및 권한 부여에 필요한 정보를 제공한다.
- Spring Security는 UserDetails 인터페이스를 구현한 클래스를 사용하여 사용자를 인증하고 권한을 부여한다.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
- getPassword(): 사용자의 비밀번호를 반환
- getUsername(): 사용자 이름을 반환
- getAuthorities(): 사용자에게 부여된 권한을 반환
- isAccountNonExpired(): 사용자 계정이 만료되었는지 확인
- isAccountNonLocked(): 사용자 계정이 잠겨 있는지 확인
- isCredentialsNonExpired(): 사용자의 비밀번호가 만료되었는지 확인
- isEnabled(): 사용자 계정이 활성화되어 있는지 확인
Serializable 인터페이스
- Serializable은 객체를 직렬화하여 바이트 배열로 변환하고, 이를 역직렬화하여 다시 객체로 복원할 수 있도록 하는 Java 인터페이스이다.
- 객체를 네트워크를 통해 전송하거나 파일에 저장하는 데 사용된다.
객체의 상태를 영구적으로 저장하거나 다른 JVM에서 사용할 수 있도록 한다. - Spring Security에서 Serializable을 사용하는 이유:
사용자 세부 정보(UserDetails)를 세션에 저장하거나 HTTP 쿠키로 전송할 때 직렬화가 필요하다.
Spring Security는 사용자 세부 정보를 인증 및 권한 부여에 사용하며, 이를 위해 Serializable 인터페이스를 사용하여 객체를 직렬화하고 역직렬화한다.
User 엔티티
UserDetails
를 구현한User
엔티티는 다음과 같다.
@Entity
@Getter
@Builder
@Table
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String uid;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<String> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public String getUsername() {
return this.uid;
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isEnabled() {
return true;
}
}
@ElementCollection
- @ElementCollection 어노테이션은 엔티티 클래스에 컬렉션 타입의 값을 저장하도록 하는 JPA 어노테이션이다.
- @ElementCollection를 사용하면 컬렉션 값을 별도의 테이블에 저장하여 데이터베이스 정규화를 유지한다.
@Builder.Default:매개변수에 명시적으로 값을 지정하지 않아도 기본값을 사용할 수 있다.
getAuthorities()
- Collection<GrantedAuthority> 타입의 객체를 반환하여 사용자의 권한 목록을 제공한다.
- 스프링 시큐리티는 getAuthorities() 메서드를 통해 반환된 권한 정보를 기반으로 사용자 인증 및 권한 부여를 수행한다.
- 각 권한은 GrantedAuthority 인터페이스를 구현하는 객체로 표현된다.
GrantedAuthority
- GrantedAuthority 인터페이스는 Spring Security에서 사용되는 사용자 권한 정보를 표현하는 데 사용되는 인터페이스이다.
- 기본 권한 객체로는 SimpleGrantedAuthority 클래스가 있다.
SimpleGrantedAuthority
public final class SimpleGrantedAuthority implements GrantedAuthority {
private static final long serialVersionUID = 620L;
private final String role;
public SimpleGrantedAuthority(String role) {
Assert.hasText(role, "A granted authority textual representation is required");
this.role = role;
}
public String getAuthority() {
return this.role;
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof SimpleGrantedAuthority) {
SimpleGrantedAuthority sga = (SimpleGrantedAuthority)obj;
return this.role.equals(sga.getAuthority());
} else {
return false;
}
}
public int hashCode() {
return this.role.hashCode();
}
public String toString() {
return this.role;
}
}
SignService
- 로그인, 회원가입 기능을 구현할 서비스이다.
public interface SignService {
SignUpResultDto signUp(String id, String password, String name, String role);
SignInResultDto signIn(String id, String password) throws RuntimeException;
}
SignUpResultDto
- 회원가입 성공 시 사용할 DTO 객체이다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignUpResultDto {
private boolean success;
private int code;
private String msg;
}
SignInResultDto
- 로그인 성공 시 사용할 DTO 객체이다.
- SignUpResultDto를 상속받고, token을 갖고있다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignInResultDto extends SignUpResultDto {
private String token;
@Builder
public SignInResultDto(boolean success, int code, String msg, String token) {
super(success, code, msg);
this.token = token;
}
}
PasswordEncoder
- Spring Security는 PasswordEncoder 인터페이스를 제공한다.
- PasswordEncoder는 다음과 같은 역할을 수행한다:
비밀번호 해시화: 사용자 입력 비밀번호를 안전한 해시 값으로 변환한다. 해시 값은 비밀번호를 직접 저장하지 않고, 비교를 위해 사용된다.
비밀번호 비교: 입력된 비밀번호의 해시 값을 저장된 해시 값과 비교하여 일치 여부를 확인한다.
솔트 사용: 비밀번호 해시화 과정에서 솔트(salt)를 사용하여 브루트포스 공격(brute-force attack)의 어려움을 높인다. 솔트는 각 사용자마다 고유하게 생성되는 임의의 값이다.
@Configuration
public class PasswordEncoderConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
- PasswordEncoderFactories.createDelegatingPasswordEncoder(): 이 코드는 PasswordEncoderFactories 클래스의 createDelegatingPasswordEncoder() 메소드를 호출하여 PasswordEncoder Bean을 생성한다.
- DelegatingPasswordEncoder란, Spring Security에서 여러 해시 알고리즘을 지원하는 유연한 비밀번호 Encoder로 다음과 같은 역할을 한다:
다양한 해시 알고리즘 지원: BCrypt, Pbkdf2, SCrypt 등 다양한 해시 알고리즘을 지원한다.
자동 알고리즘 선택: 요청에 따라 적절한 해시 알고리즘을 자동으로 선택한다.
CommonResponse
- CommonResponse는 성공 또는 실패 시 설정할 코드와 메세지 정보를 포함하는 enum 클래스이다.
- enum 클래스는 열거형 상수를 정의하는 데 사용되는 특수한 클래스이다.
package org.spring.study.common;
import lombok.Getter;
public enum CommonResponse {
SUCCESS(0, "Success"), FAIL(-1, "Fail");
int code;
String msg;
CommonResponse(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
회원가입 메서드 구현하기
- signUp 메서드는 회원가입 기능을 구현한다.
@Override
public SignUpResultDto signUp(String id, String password, String name, String role) throws RuntimeException {
LOGGER.info("[getSignUpResult] 회원 가입 정보 전달");
User user;
if (role.equalsIgnoreCase("admin")) {
user = User.builder()
.uid(id)
.name(name)
.password(passwordEncoder.encode(password))
.roles(Collections.singletonList("ROLE_ADMIN"))
.build();
} else {
user = User.builder()
.uid(id)
.name(name)
.password(passwordEncoder.encode(password))
.roles(Collections.singletonList("ROLE_USER"))
.build();
}
User savedUser = userRepository.save(user);
SignUpResultDto signUpResultDto = new SignUpResultDto();
LOGGER.info("[getSignUpResult] userEntity 값이 들어왔는지 확인 후 결과값 주입");
if (!savedUser.getName().isEmpty()) {
LOGGER.info("[getSignUpResult] 정상 처리 완료");
setSuccessResult(signUpResultDto);
} else {
LOGGER.info("[getSignUpResult] 실패 처리 완료");
setFailResult(signUpResultDto);
}
return signUpResultDto;
}
- role 파라미터 값에 따라 User 객체의 roles를 설정한다.
- User.builder()를 사용하여 사용자 정보를 담은 객체 생성한다.
- passwordEncoder 객체를 사용하여 입력된 비밀번호를 해시화한다.
- userRepository 를 사용하여 사용자 정보를 데이터베이스에 저장한다.
- SignUpResultDto 객체를 생성하여 리턴한다.
equalsIgnoreCase
- equalsIgnoreCase 메서드는 두 문자열을 비교할 때 대소문자를 무시하고 일치 여부를 확인하는 메서드이다.
Collections.singletonList
- 단일 요소로 구성된 리스트를 생성하며, 이때 생성된 리스트는 불변이다.
- 따라서 요소를 추가하거나 제거할 수 없다.
- 멀티스레드 환경에서 안전하게 사용할 수 있다.
로그인 메서드 구현하기
- signIn 메서드는 로그인 기능을 수행한다.
@Override
public SignInResultDto signIn(String id, String password) throws RuntimeException {
LOGGER.info("[getSignInResult] signDataHandler 로 회원 정보 요청");
User user = userRepository.getByUid(id);
LOGGER.info("[getSignInResult] Id : {}", id);
LOGGER.info("[getSignInResult] 패스워드 비교 수행");
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException();
}
LOGGER.info("[getSignInResult] 패스워드 일치");
LOGGER.info("[getSignInResult] SignInResultDto 객체 생성");
SignInResultDto signInResultDto = SignInResultDto.builder()
.token(jwtTokenProvider.createToken(String.valueOf(user.getUid()),
user.getRoles()))
.build();
LOGGER.info("[getSignInResult] SignInResultDto 객체에 값 주입");
setSuccessResult(signInResultDto);
return signInResultDto;
}
- 사용자 정보 요청: User user = userRepository.getByUid(id); 이 부분에서는 사용자가 입력한 ID를 이용해 사용자 정보를 데이터베이스에서 가져온다.
- 패스워드 비교: if (!passwordEncoder.matches(password, user.getPassword())) 이 부분에서 사용자가 입력한 패스워드와 데이터베이스에 저장된 패스워드를 비교한다. 패스워드가 일치하지 않으면 RuntimeException을 발생시킨다.
- 토큰 생성: 로그인에 성공한 사용자에 대해 JWT(Json Web Token)를 생성한다. 이 토큰은 사용자의 ID와 역할 정보를 포함한다.
- 로그인 결과 반환: 마지막으로, 생성된 토큰을 포함하는 SignInResultDto 객체를 반환한다.
참고
- Google Gemini
- 스프링 부트 핵심 가이드 "스프링 부트를 활용한 애플리케이션 개발 실무" , 장정우, 2022
- https://github.com/wikibook/springboot/tree/main/chapter13_security
'Development > Spring' 카테고리의 다른 글
[Spring] 테스트 환경을 독립적으로 만들어보자 (0) | 2024.04.22 |
---|---|
[Spring] Spring Security에서 지원하는 Logout과 JWT 기반 Logout 구현의 차이 (0) | 2024.04.21 |
[Spring] Spring Security + JWT: SecurityFilterChain, AccessDeniedHandler, AuthenticationEntryPoint (0) | 2024.04.07 |
[Spring] Spring Security + JWT: JwtAuthenticationFilter (0) | 2024.04.06 |
[Spring] Spring Security + JWT: JwtTokenProvider (0) | 2024.04.06 |