본문 바로가기

JWT

4. [SpringBoot JWT 튜토리얼] DTO, Repository, 로그인

외부와의 통신에 사용할 DTO 패키지 및 클래스를 생성

Repository 관련 코드 생성

 

# DTO 클래스 생성

 

- LoginDto 클래스

 

로그인시 사용할 DTO

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginDto {

    @NotNull
    @Size(min = 3, max = 50)
    private String username;

    @NotNull
    @Size(min = 3, max = 100)
    private String password;
}

 

Lombok 어노테이션(Get, Set 등)이 추가되었고 @Valid 관련 어노테이션을 추가했습니다.

로그인 할 이용자의 아이디, 비밀번호를 담을 username, password 필드를 가집니다.

 

- TokenDto 클래스

 

Token 정보를 Response 할때 사용할 TokenDto를 만들겠습니다.

 

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TokenDto {

    private String token;
}

 

- UserDto 클래스

 

회원가입시에 사용할 UserDto 클래스도 미리 만들어주겠습니다.

 

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

 

 

# Repository 관련 코드 작성

 

이제 Repository들을 만들어주기 위해 repository 패키지를 생성합니다.

 

- UserRepository

 

이전에 만들었던 User 엔티티에 매핑되는 UserRepository 인터페이스를 만들겠습니다.

 

public interface UserRepository extends JpaRepository<User, Long> {
    @EntityGraph(attributePaths = "authorities")
    Optional<User> findOneWithAuthoritiesByUsername(String username);
}

 

  • EntityGraph : 쿼리가 수행될때 Lazy 조회가 아니고 Eager조회로 authorities 정보를 같이가져옵니다.
    • Lazy, Eager : 지연로딩(lazy), 즉시로딩(eager) 연관관계의 데이터를 어떻게 가져올지 (fetch)

 

JpaRepository를 extends 하면 findAll, save 등의 메소드를 기본적으로 사용할 수 있습니다.

findOneWithAuthoritiesByUsername 메소드는 username을 기준으로 User 정보를 가져올때 권한 정보도 같이 가져오게됩니다.

 

# 로그인 API, 관련 로직 생성

 

- CustomUserDetailsService 클래스

 

Spring Security에서 중요한 부분중 하나인 UserDetailsService를 구현한 CustomUserDetailsService 클래스를 생성하겠습니다.

먼저 service 패키지를 만들어고 해당 패키지에 클래스를 생성합니다.

 

@Component("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    @Transactional
    public UserDetails loadUserByUsername(final String username) {
        return userRepository.findOneWithAuthoritiesByUsername(username)
                .map(user -> createUser(username, user))
                .orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
    }

    private org.springframework.security.core.userdetails.User createUser(String username, User user) {
        if (!user.isActivated()) { 
            throw new RuntimeException(username + " -> 활성화되어 있지 않습니다.");
        }
        // 유저가 활성화상태라면 유저의 권한정보(name,password)를 User 객체로 리턴해준다
        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);
    }
}

 

UserDetailsService를 implements하고 UserRepository를 주입받습니다. 

loadUserByUsername 메소드를 오버라이드해서 로그인시에 DB에서 유저정보와 권한정보를 가져오게됩니다.

해당 정보를 기반으로 해서 userdetails.user 객체를 생성해서 리턴합니다.

 

권한은 MyBatis를 통해 String으로 가져왔으므로 GrantedAuthority 인터페이스에 맞게 SimpleGrantedAuthority로 변환해서 리스트를 만든 후 리턴해준다.

 

.map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName())) : 

위의 코드는 authority 엔티티에서 MyBatis에 저장된 값을 가져와서 저장된 값을 SimpleGrantedAuthority클래스로 해당 값에 권한을 주는 것

authority에 저장된 값을 map을 돌려 SimpleGrantedAuthority에 저장시키는 코드, 한마디로 저장된 String값을 시큐리티 내에서 권한이 유효한 값으로 바꾸는 것

 

 

- AuthController 클래스

 

로그인 API를 추가하기 위해서 AuthController 클래스를 만들겠습니다.

 

@RestController
@RequestMapping("/api")
public class AuthController {
    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;

    public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {
        this.tokenProvider = tokenProvider;
        this.authenticationManagerBuilder = authenticationManagerBuilder;
    }

    @PostMapping("/authenticate")
    public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginDto loginDto) {

        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());

        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        String jwt = tokenProvider.createToken(authentication);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);

        return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
    }
}

 

이전에 만들었던 TokenProvider, AuthenticationManagerBuilder 를 주입받습니다.

로그인 API 경로는 /api/authenticate 경로이고 POST 요청을 받습니다.

 

LoginDto의 이름과 패스워드를 파라미터로 받고 받은 정보로 authenticationToken생성

 

authenticationToken을 이용해서 authenticate메소드가 실행될때

CustomUserDetailsService에서 loadUserByUsername 메소드가 실행

 

실행된 결과값을 가지고 authentication 객체를 생성 > 이 객체를 Securitt객체(SecurityContextHolder)에 저장

> 인증 기준을 가지고 tokenProvider에서 만든 createToken 메소드를 통해 JWT 토큰을 생성

> 생성된 JWT Token 을 Response Header에 넣어주고 TokenDto를 이용해서 Response Body에도 넣어서 리턴

 


 

Q) authenticationToken을 이용해서 CustomUserDetailsService에서 loadUserByUsername 메소드가 실행한다고 했는데

정작 이 코드에는 CustomUserDetailsService에 대한 구현체도 없고 UsernamePasswordAuthenticationToken 내부에도 UserDetailsService나 UserDetails에 대한 내용이 없는데

어떻게 UsernamePasswordAuthenticationToken 에서 UserDetailsService를 가져온다는 걸까?

 

A) 답은 전에 만든 TokenProvider에 있다

 

TokenProvider 안 getAuthentication메서드에서 jwt에서 인증정보를 빼 권한정보로 바꾸고  User객체 정보를 principal로 저장해 UsernamePasswordAuthenticationToken에 저장했다

public Authentication getAuthentication(String token) {
    Claims claims = Jwts
            .parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .getBody();

    Collection<? extends GrantedAuthority> authorities =
            Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());

    User principal = new User(claims.getSubject(), "", authorities);

    return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}

 

여기서 중요한건 User객체인데 User객체는 UserDetails의 구현체 이므로 DB에 사용자 정보를 저장하거나 찾을 수 있고\

User객체를 UsernamePasswordAuthenticationToken안에 저장했으니 

UsernamePasswordAuthenticationToken(UserDetailsService.loadUserByUsername(String username),null(보통은널),권한)와 같은 객체가 생성이 가능해지고 

결과적으로 authenticationToken을 이용해서 CustomUserDetailsService에서 loadUserByUsername 메소드 실행 가능

 

 

로그인 API 테스트

 

로그인 DTO에 이름과 패스워드를 전달해야 하므로 body에 raw

 

이때 json으로 리턴해야 한다

 

 select
        user0_.user_id as user_id1_1_0_,
        authority2_.authority_name as authorit1_0_1_,
        user0_.activated as activate2_1_0_,
        user0_.nickname as nickname3_1_0_,
        user0_.password as password4_1_0_,
        user0_.username as username5_1_0_,
        authoritie1_.user_id as user_id1_2_0__,
        authoritie1_.authority_name as authorit2_2_0__ 
    from
        user user0_ 
    left outer join
        user_authority authoritie1_ 
            on user0_.user_id=authoritie1_.user_id 
    left outer join
        authority authority2_ 
            on authoritie1_.authority_name=authority2_.authority_name 
    where
        user0_.username=?

 

정상결과값

 

  • Postman의 유용한 기능

 

위와 같이 Tests 탭에서 Response의 데이터를 전역변수에 저장해서 다른 Request에서도 사용할 수 있습니다.