dto > LoginDto.java 생성
import lombok.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/* 롬복 어노테이션 */
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginDto {
/* @Valid 관련 어노테이션 */
@NotNull
@Size(min = 3, max = 50)
private String username;
@NotNull
@Size(min = 3, max = 100)
private String password;
}
토큰 정보를 Response할 때 사용
dto > TokenDto.java 생성
import lombok.*;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TokenDto {
private String token;
}
회원가입 시에 사용
dto > UserDto.java 생성
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {
@NotNull
@Size(min = 3, max = 50)
private String username;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@NotNull
@Size(min = 3, max = 100)
private String password;
@NotNull
@Size(min = 3, max = 50)
private String nickname;
}
User 엔티티에 매핑됨
import com.example.tutorial.entity.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
// JpaRepository를 extends하면 findAll, save 등 Jpa의 기본 메소드 사용 가능
public interface UserRepository extends JpaRepository<User, Long> {
// @EntityGraph는 쿼리가 수행될 때 Lazy 조회가 아닌
// Eager 조회로 authorities 정보를 같이 가져오게 됨
@EntityGraph(attributePaths = "authorities")
// username을 기준으로 User 정보를 가져올 때 권한 정보도 같이 가져옴
Optional<User> findOneWithAuthoritiesByUsername(String username);
}
service 디렉토리 생성 > CustomUserDetailsService.java 생성
import com.example.tutorial.entity.User;
import com.example.tutorial.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Component("userDetailsService")
// 스프링 시큐리티에서 중요한 부분 중 하나인 UserDetailService를 implements
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
// UserRepository를 주입받음
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional
// UserDetailService의 loadUserByUsername 메소드를 오버라이딩 해서
// 로그인 시에 DB에서 유저 정보와 권한 정보를 가져오게 됨
public UserDetails loadUserByUsername(final String username) {
return userRepository.findOneWithAuthoritiesByUsername(username)
.map(user -> createUser(username, user))
.orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
}
// loadUserByUsername에 의해 받은 정보를 기반으로 유저가 활성화 상태면,
// 유저의 권한정보와 유저네임, 패스워드를 가지고 userdetails.User객체를 생성해서 리턴함
private org.springframework.security.core.userdetails.User createUser(String username, User user) {
if (!user.isActivated()) {
throw new RuntimeException(username + " -> 활성화되어 있지 않습니다.");
}
List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(user.getUsername(),
user.getPassword(),
grantedAuthorities);
}
}
controller > AuthController.java 생성
import com.example.tutorial.dto.LoginDto;
import com.example.tutorial.dto.TokenDto;
import com.example.tutorial.jwt.JwtFilter;
import com.example.tutorial.jwt.TokenProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/api")
public class AuthController {
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
// TokenProvider, AuthenticationManagerBuilder 주입 받음
public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {
this.tokenProvider = tokenProvider;
this.authenticationManagerBuilder = authenticationManagerBuilder;
}
// 로그인 API 경로 => /api/authenticate
@PostMapping("/authenticate")
// LoginDto의 username과 password를 파라미터로 받아
public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginDto loginDto) {
// UsernamePasswordAuthenticationToken을 생성함
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
// authenticationToken을 이용해서 authenticate 메소드가 실행될 때,
// CustomUserDetailsService에서 loadUserByUsername 메소드가 실행됨
// 실행된 결괏값을 가지고 Authentication 객체를 생성함
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// Authentication 객체를 SecurityContext에 저장하고
SecurityContextHolder.getContext().setAuthentication(authentication);
// Authentication 객체로 createToken 메소드를 통해 JWT 토큰을 생성함
String jwt = tokenProvider.createToken(authentication);
HttpHeaders httpHeaders = new HttpHeaders();
// JWT 토큰은 Response Header에도 넣어주고,
httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);
// TokenDto를 이용해서 Response Body에도 넣어서 리턴함
return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
}
}
http://localhost:8080/api/authenticate
으로 username
과 password
를 body에 담아 post 요청을 보내면, 다음과 같이 토큰이 정상적으로 리턴된다.
+) 포스트맨의 유용한 기능
Tests 탭에서 Response의 데이터를 전역변수에 저장하면 다른 Request에서 사용할 수 있다.var jsonData = JSON.parse(responseBody) pm.globals.set("jwt_tutorial_token", jsonData.token);
responseBody 에 있는 내용을 파싱해서 포스트맨(pm)의 전역변수에 설정해서 다른 요청에서도 해당 변수에 있는 값을 끌어서 쓸 수 있게 된다.