UserDetailsManager : Spring Security에서 제공하는 인터페이스
User 계정의 생성, 조회, 수정, 삭제 등의 작업을 수행하는 메서드를 정의한다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.xerial:sqlite-jdbc:3.41.2.2'
runtimeOnly 'org.hibernate.orm:hibernate-community-dialects:6.2.4.Final'
spring:
datasource:
url: jdbc:sqlite:db.sqlite
driver-class-name: org.sqlite.JDBC
username: sa
password: password
jpa:
hibernate:
ddl-auto: create
database-platform: org.hibernate.community.dialect.SQLiteDialect
show-sql: true
@Entity
@Table(name = "user")
@Data
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
private String password;
private String email;
private String phone;
}
UserDetailsService란?
스프링 시큐리티 프레임워크에서 인증(Authentication) 과정에서 사용되는 인터페이스이다.
UserDetailsService를 구현하는 클래스는 사용자 정보를 어디에서 가져올지를 정의하고 해당 사용자의 상세 정보를 담은 UserDetails 객체를 생성해야 한다.
UserDetails 객체는 일반적으로
(username)(password)(enabled)(expried)(authorities) 등의 정보 (사용자의 실제 인증에 필요한 정보) 를 제공한다.
스프링 시큐리티는 UserDetailsService 를 사용하여 사용자 인증에 필요한 정보를 가져오고, 가져온 정보를 기반으로 인증을 수행한다.
인증 과정에서 사용자가 제공한 아이디와 비밀번호를 확인하고, 사용자의 권한을 확인하여 접근을 허용하거나 거부한다.
public interface UserRepository extends JpaRepository <UserEntity, Long>
{
Optional<UserEntity> findByUsername(String username);
boolean existsByUsername(String username);
}
public interface UserDetailsService {
// Spring Security 내부에서 사용자 인증 과정에서 활용하는 메소드
// 따라서... 반드시 정상 동작해야된다.
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException;
}
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}
@Slf4j
@Service
public class JpaUserDetailsManager implements UserDetailsManager {
private final UserRepository userRepository;
public JpaUserDetailsManager(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserEntity> optionalUser
= userRepository.findByUsername(username);
if (optionalUser.isEmpty()) throw new UsernameNotFoundException(username);
return User.withUsername(username).build();
}
@Override
public void createUser(UserDetails user) {
if (this.userExists(user.getUsername()))
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
UserEntity userEntity = new UserEntity();
userEntity.setUsername(user.getUsername());
userEntity.setPhone(user.getPassword());
this.userRepository.save(userEntity);
}
@Override
public boolean userExists(String username) {
return this.userRepository.existsByUsername(username);
}
// 추후 개발 해보기
@Override
public void updateUser(UserDetails user) {
}
@Override
public void deleteUser(String username) {
}
@Override
public void changePassword(String oldPassword, String newPassword) {
}
public JpaUserDetailsManager(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this. userRepository = userRepository;
createUser(CustomUserDetails.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.email("user@gmail.com")
.build();
)
}
인증 및 권한 부여에 사용되는 인터페이스
Spring Security의 사용자 정보를 캡슐화하는데 사용된다.
// 사용자 정보에 추가적인 정보를 포함하고 싶다면…
// UserDetails 인터페이스를 구현하는 클래스 (CustomUserDetails 작성)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {
private Long id;
private String username;
private String password;
private String email;
private String phone;
public static CustomUserDetails fromEntity(UserEntity entity) {
CustomUserDetails details = new CustomUserDetails();
details.id = entity.getId();
details.password = entity.getPassword();
details.email = entity.getEmail();
details.phone = entity.getPhone();
return details;
}
public UserEntity newEntity() {
UserEntity entity = new UserEntity();
entity.setUsername(username);
entity.setPassword(password);
entity.setEmail(email);
entity.setPhone(phone);
return entity;
}
}
public class CustomUserDetails implements UserDetails {
//권한 설정을 위한 메서드 ex) 사용자, 관리자
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {}
@Override
public String getPassword() {}
@Override
public String getUsername() {}
// --- 유효한 사용자인지 판단하기 위한 메서드 ---
/** 사용자 계정이 만료되었는지 여부 */
@Override
public boolean isAccountNonExpired() {}
/** 사용자의 자격 증명(비밀번호)이 만료되었는지 여부 */
@Override
public boolean isAccountNonLocked() {}
/** 사용자의 자격 증명(비밀번호)이 만료되었는지 여부 */
@Override
public boolean isCredentialsNonExpired() {}
/** 사용자 계정이 활성화되었는지 여부 */
@Override
public boolean isEnabled() {}
}
// 실제로 Spring Security 내부에서 사용하는 메서드
// loadUserByUsername -> 반드시 구현해야 정상적으로 동작한다.
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
if (optionalUser.isEmpty())
throws new UsernameNotFoundException(username);
return CustomUserDetails.fromEntity(optionalUser.get());
}
@Override
public void createUser(UserDetails user) {
if (this.userExists(user.getUsername()))
throws new ResponseStatusException(HttpStatus.CONFLICT);
try {
this.userRepository.save(
((CustomUserDetails) user).newEntity());
} catch (ClassCastException e) {
log.error("failed to cast to {}", CustomUserDetails.class);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
JSON으로 표현된 정보를 안전하게 주고받기 위한 Token의 일종

세션을 저장하지 않고 토큰의 소유를 통해 인증 판단
상태를 저장하지 않기 때문에 서버에서 세션관리를 하지 않아도 된다.
세션 소유가 곧 인증
쿠키는 요청을 보낸 클라이언트에 종속되지만, 토큰은 쉽게 첨부가 가능하다. (주로 header에 첨부한다.)
(단점) 로그인 상태라는 개념이 사라져 로그아웃이 불가능하다.
각 구성 요소는 Base64 인코딩으로 표현되어 점으로 구분되는 문자열 형태로 표현된다.
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890", // "sub” : 주제(Subject)
"name": "John Doe", // "name" : 사용자 이름
"exp": 1516239022 // "exp" : 토큰의 만료 시간
}
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
@Slf4j
@Component
//JWT 관련 기능들을 넣어두기 위한 기능성 클래스
public class JwtTokenUtils {
private final Key signingKey;
public JwtTokenUtils(
@Value("${jwt.secret}")
String jwtSecret
) {
this.signingKey
= Keys.hmacShaKeyFor( Decoders.BASE64.decode(jwtSecret));
}
//주어진 사용자 정보를 바탕으로 JWT를 문자열로 생성
public String generateToken(UserDetails userDetails){
//Claims : JWT 에 담기는 정보의 단위를 Claim 이라고 부른다.
Claims jwtClaims = Jwts.claims()
.setSubject(userDetails.getUsername())
.setIssuedAt(Date.from(Instant.now()))
.setExpiration(Date.from(Instant.now().plusSeconds(3600)));
return Jwts.builder()
.setClaims(jwtClaims)
.signWith(signingKey)
.compact();
}
}
@Data
public class JwtRequestDto {
private String username;
private String password;
}
@Data
public class JwtTokenDto {
private String token;
}
@Slf4j
@RestController
@RequestMapping("token")
public class TokenController {
//UserDetailsManager : 사용자 정보 회수
//PasswordEncoder : 비밀번호 대조용 인코더
private final UserDetailsManager userDetailsManager;
private final PasswordEncoder passwordEncoder;
private final JwtTokenUtils jwtTokenUtils;
public TokenController(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder,
JwtTokenUtils jwtTokenUtils) {
this.userDetailsManager = userDetailsManager;
this.passwordEncoder = passwordEncoder;
this.jwtTokenUtils = jwtTokenUtils;
}
// /token/issue: JWT 발급용 Endpoint
@PostMapping("/issue")
public JwtTokenDto issueJwt(@RequestBody JwtRequestDto dto) {
// 사용자 정보 회수
UserDetails userDetails
= userDetailsManager.loadUserByUsername(dto.getUsername());
// 기록된 비밀번호와 실제 비밀번호가 다를때
if (!passwordEncoder.encode(dto.getPassword())
.equals(userDetails.getPassword()))
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
JwtTokenDto response = new JwtTokenDto();
response.setToken(jwtTokenUtils.generateToken(userDetails));
return response;
}
}
http
//CSRF : 사이트 사이간 위조 방지 해제 (disable)
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
authHttp -> authHttp
//requestMatchers -> 어떤 URL 로 오는 요청에 대하여 설정하는지
//permitAll() -> 누가 요청해도 허가한다.
.requestMatchers(
"/no-auth",
"/token/issue"
).permitAll()
.requestMatchers("/re-auth", "/users/my-profile").authenticated() //인증된 사용자만 허가
.requestMatchers(
"/",
"users/register"
).anonymous() //인증이 되지 않은 사용자만 허가
)
.sessionManagement(
sessionManagerment -> sessionManagerment
.sessionCreationPolicy((SessionCreationPolicy.STATELESS))
);
https://velog.io/@vamos_eon/JWT란-무엇인가-그리고-어떻게-사용하는가-1
출처 : 멋사 5기 백엔드 위키 5팀 '오'로나민 C