UserDetailsManager, JWT

calis_ws·2023년 7월 30일
0

UserDetailsManager Custom

UserDetailsManager : Spring Security에서 제공하는 인터페이스

User 계정의 생성, 조회, 수정, 삭제 등의 작업을 수행하는 메서드를 정의한다.

JPA와 SQLite Dependency 추가

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'

SQLite 사용을 위한 application.yaml 작성

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

UserEntity → 저장하고 싶은 사용자 정보 추가

@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;
}

JpaUserDetailsManager 구현

UserDetailsService란?

스프링 시큐리티 프레임워크에서 인증(Authentication) 과정에서 사용되는 인터페이스이다.

UserDetailsService를 구현하는 클래스는 사용자 정보를 어디에서 가져올지를 정의하고 해당 사용자의 상세 정보를 담은 UserDetails 객체를 생성해야 한다.

UserDetails 객체는 일반적으로

  • 사용자의 아이디(username)
  • 비밀번호(password)
  • 계정이 활성화되었는지 여부(enabled)
  • 계정이 만료되었는지 여부(expried)
  • 권한(authorities)

등의 정보 (사용자의 실제 인증에 필요한 정보) 를 제공한다.

스프링 시큐리티는 UserDetailsService 를 사용하여 사용자 인증에 필요한 정보를 가져오고, 가져온 정보를 기반으로 인증을 수행한다.

인증 과정에서 사용자가 제공한 아이디와 비밀번호를 확인하고, 사용자의 권한을 확인하여 접근을 허용하거나 거부한다.

UserRepository → UserDetailsManager를 구현하기 위한 메소드 추가

public interface UserRepository extends JpaRepository <UserEntity, Long>
{
		Optional<UserEntity> findByUsername(String username);
		
		boolean existsByUsername(String username);
}

UserDetailsService → 사용자 정보를 확인하기 위해 사용하는 Interface

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);
}

JpaUserDetailsManager

@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) {

}

Test 임의의 User 작성 →테스트 목적의 사용자를 하나 추가

public JpaUserDetailsManager(UserRepository userRepository, PasswordEncoder passwordEncoder) {
		this. userRepository = userRepository;
		createUser(CustomUserDetails.builder()
				.username("user")
				.password(passwordEncoder.encode("password"))
				.email("user@gmail.com")
				.build();
		)
}

CustomUserDetails

인증 및 권한 부여에 사용되는 인터페이스
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() {}
}

CustomUserDetails를 활용하도록 JpaUserDetailsManager 작성

// 실제로 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);
		}
}

JWT

JSON으로 표현된 정보를 안전하게 주고받기 위한 Token의 일종

  • 사용자 확인을 위한 인증 정보
  • 위변조 확인이 용이 → 위변조가 어려움
  • 토큰 기반 인증 시스템 (세션 x) 에서 많이 사용

토큰 기반 인증 시스템 (Token Based Authentication)

세션을 저장하지 않고 토큰의 소유를 통해 인증 판단

  • 상태를 저장하지 않기 때문에 서버에서 세션관리를 하지 않아도 된다.

  • 세션 소유가 곧 인증

  • 쿠키는 요청을 보낸 클라이언트에 종속되지만, 토큰은 쉽게 첨부가 가능하다. (주로 header에 첨부한다.)

  • (단점) 로그인 상태라는 개념이 사라져 로그아웃이 불가능하다.

구성 요소

각 구성 요소는 Base64 인코딩으로 표현되어 점으로 구분되는 문자열 형태로 표현된다.

Header(헤더)

  • JWT의 부수적인 정보
  • JWT의 유형과 암호화 알고리즘을 포함
  • 일반적으로 JSON 형식으로 표현된다.
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload(내용)

  • JWT로 전달하고자 하는 정보가 담긴 부분
  • JWT에 포함될 클레임(claim) 정보를 담고 있다
{
  "sub": "1234567890",	// "sub” : 주제(Subject)
  "name": "John Doe",	// "name" : 사용자 이름
  "exp": 1516239022		// "exp" : 토큰의 만료 시간
}

Signature(서명)

  • JWT의 위변조 판단을 위한 부분
  • 헤더, 페이로드, 비밀 키를 사용하여 생성된다.
  • 서명은 토큰의 무결성을 검증하는 데 사용되며, 토큰이 변조되지 않았음을 확인한다.
  • 서명은 암호화된 헤더와 페이로드, 비밀 키를 조합한 결과

JWT 발급

JWT Dependency 추가

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

JWT 관련 기능들을 넣어두기 위한 기능성 클래스 JwtTokenUtils 추가

@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();
	}
}

JwtRequestDto 추가

@Data
public class JwtRequestDto {
    private String username;
    private String password;
}

JwtTokenDto 추가

@Data
public class JwtTokenDto {
    private String token;
}

Controller

@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;
	}

}

WebSecurityConfig 수정

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))
);

JWT에 대한 자세한 내용 알아보기

https://velog.io/@vamos_eon/JWT란-무엇인가-그리고-어떻게-사용하는가-1

출처 : 멋사 5기 백엔드 위키 5팀 '오'로나민 C

profile
반갑습니다람지

0개의 댓글