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