본문 바로가기
[SpringBoot]/[Spring 강의]

[Spring 강의 작성] JWT 토큰 인증방식 적용하기 - 3

by Hevton 2023. 1. 8.
반응형

 

dto 패키지 아래에, 세 개의 dto를 만들어놓는다.

 

LoginDto.java

package io.spring.hevton.Team.dto;

import lombok.*;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginDto {

    private String username;

    private String password;
}

LoginDto는 JWT 토큰 생성의 기반이 될 것이다.

 

 

TokenDto.java

package io.spring.hevton.Team.dto;


import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TokenDto {

    String token;
}

TokenDto는 토큰 Response를 받을 때 사용한다.

 

 

UserDto.java

package io.spring.hevton.Team.dto;


import io.spring.hevton.Team.entity.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Set;
import java.util.stream.Collectors;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {

    private String username;

    private String password;

    private String nickname;

    private Set<AuthorityDto> authorityDtoSet;

    public static UserDto from(User user) {
        if(user == null) return null;

        return UserDto.builder()
                .username(user.getUsername())
                .nickname(user.getNickname())
                .authorityDtoSet(user.getAuthorities().stream()
                        .map(authority -> AuthorityDto.builder().authorityName(authority.getAuthorityName()).build())
                        .collect(Collectors.toSet()))
                .build();
    }

}

UserDto는 from을 자주 쓸 것이다.

 

 

 

repository 패키지 아래에 UserRepository를 추가한다.

 

UserRepository.java

package io.spring.hevton.Team.repository;


import io.spring.hevton.Team.entity.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

    @EntityGraph(attributePaths = "authorities") // 해당 쿼리가 수행될 때, Lazy가 아니고 Eager 조회로 authorites 정보를 가져옴
    Optional<User> findOneWithAuthoritiesByUsername(String username); // username을 기준으로 User 정보를 가져오는데, 권한 정보도 같이 가져오는 메서드.
}

findOneWithAuthoritiesByUsername은

Username 기반으로 하나의 User를 가져오는데(Username은 unique key이다), 그 때 권한정보도 같이 가져오는 방식이다.

User 와 Authority는 many to many 관계였다. 따라서 하나의 유저에 다양한 권한이 들어가 있을 수 있다.

 

 

service 패키지 아래에

CustomUserDetailsService를 추가한다.

CustomUserDetailsService.java

package io.spring.hevton.Team.service;


import io.spring.hevton.Team.entity.User;
import io.spring.hevton.Team.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")
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

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

    @Override
    @Transactional // 두 개 이상의 쿼리가 실행될 때, atomic 실행을 보장하기 위한.
    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 + " -> 활성화되어 있지 않습니다.");
        }
        
        // 유저의 권한을 모두 모아서, List 형태로 만듦
        List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
                .collect(Collectors.toList());
                
        // 유저의 정보와 권한데이터를 통해 UserDetails 생성
        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                user.getPassword(),
                grantedAuthorities);
    }
}

 

 

마지막으로 controller 패키지 아래에

AuthController를 추가해준다.

 

AuthController.ajva

package io.spring.hevton.Team.controller;


import io.spring.hevton.Team.dto.LoginDto;
import io.spring.hevton.Team.dto.TokenDto;
import io.spring.hevton.Team.jwt.JwtFilter;
import io.spring.hevton.Team.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.validation.annotation.Validated;
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;


@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(@Validated @RequestBody LoginDto loginDto) {

        // LoginDto의 username과 password를 파라미터로 받아서 AuthenticationToken 객체 생성
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());

        // CustomUserDetailsServce에서 만든 loadUserByUserName이 실행된다.
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        String jwt = tokenProvider.createToken(authentication); // 인증정보를 기반으로 JWT 토큰 생성

        // 토큰을 Response Header, Body 모두에 넣어준다.
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);

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

localhost:8080/authenticate 엔드포인트로

LoginDto인 json 데이터를 쏴주면 JWT 토큰을 반환받는다.

 

 

이를 테스트해보자.

 

JWT 토큰이 잘 발급되는 것을 확인할 수 있다.

 

 

근데 여기 이 데이터들을 넣어놨던걸 기억하는가

이를 기반으로 동작한 것이다.
따라서 id pw가 admin 이거나id pw가 user 로 넘겨주면 token이 발급되지만

그 이외의 경우에는 동작하지 않는다.

 

저기 DB에 저장된 패스워드들이 BCryptPasswordEncoder 방식으로 username이 해싱된 상태다.

우리가 SecurityConfig에서 BCryptPasswordEncoder 방식으로 비밀번호를 해싱하기로 했고,

DB에 id pw가 인증되지 않은 데이터에 대해서는 JWT 토큰 발급이 되지 않는 것이다.

 

반응형