ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot JWT Tutorial (3)
    Spring/SpringSecurity 2022. 4. 12. 23:33

    ===

    https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt/dashboard

     

    [무료] Spring Boot JWT Tutorial - 인프런 | 강의

    Spring Boot, Spring Security, JWT를 이용한 튜토리얼을 통해 인증과 인가에 대한 기초 지식을 쉽고 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...

    www.inflearn.com

     

    ===

    외부와의 통신에 사용할 DTO 클래스 생성

    Repository 관련 코드 생성

    로그인 API, 관련 로직 생성

     

    ===

    login시 사용할 LoginDto를 만듦

    import lombok.*;
    
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    
    @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;
    }

     

    ===

    Token정보를 Response할 때 사용할 TokenDto

    @Getter
    @Setter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class TokenDto {
    
        private String token;
    }

     

    ===

    회원가입 때 쓰는 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;
    
       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();
       }
    }

    처음엔

    authorityDtoSet이랑

    from이라는 메소드는 없음.

    나중에 추가하나봐

     

    ===

    Repository만들기

    User라는 Entity

    에 매핑되는 UserRepository 인터페이스를 만들것

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

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

    findOneWithAuthoritiesByUsername메소드는 username을 기준으로 User정보를 가져올 때 권한 정보도 같이 가져옴

     

    @EntityGraph 애너테이션은 쿼리가 수행이 될 때 Lazy조회가 아니고 Eager조회로 authorities(attributePaths = ""에 적혀 있는 저것)정보를 같이 가져옴

     

    ===

    Spring Security에서 제일 중요한 부분중 하나인 UserDetailsService를 커스텀하게 구현한 CustomUserDetailsService 클래스를 생성

    import me.silvernine.tutorial.entity.User;
    import me.silvernine.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")
    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 + " -> 활성화되어 있지 않습니다.");
          }
          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를 주입받음

     

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

    DB에서 가져온 정보를 기준으로 그 유저가 활성화 상태라면(!user.isActivated()가 false)

    그 유저의 권한정보들하고 = user.getAuthorities()

    username = user.getUsername()

    password = user.getPassword()

    를 가지고 User객체를 리턴해주게 된다

    userdetails.User 객체 리턴

     

    ===

    마지막으로 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를 주입받음.

    <POST> /api/authenticate

    아까 만들었던 LoginDto로 파라미터를 받고 걔네 두개 getter로

    authenticationToken객체를 생성

     

    그걸이용해서 authenticate 메소드가 실행될 때

    CustomUserDetailsService에서 우리가 만들었던 loadUserByUsername메소드가 실행이 됨(왜?? 몰라)

    이거

     

    ===

    authenticationManangerBuilder.getObject().authenticate() 메소드가 실행되면

    1. AuthenticationManager 의 구현체인 ProviderManager 의 authenticate() 메소드가 실행됩니다

    2. 해당 메소드에선 AuthenticaionProvider 인터페이스의 authenticate() 메소드를 실행하는데

    해당 인터페이스에서 데이터베이스에 있는 이용자의 정보를 가져오는  UserDetailsService 인터페이스를 사용합니다.

    3. 그래서 UserDetailsService 인터페이스의 loadUserByUsername() 메소드를 호출하게 됩니다.

    따라서 CustomUserDetailsService 구현체에 오버라이드된 loadUserByUsername() 메소드를 호출하게 되는 것입니다.

     

    스프링 시큐리티의 아키텍처를 참고하시면 도움될것같습니다

    https://lilly021.com/spring-security-architecture/

     

    ===

     

    loadUserByUsername이 실행이 되고 이 결과값을 갖고 Authentication객체를 생성

     

    바로 아랫줄에 보면 그 객체를 SecurityContext에 저장을 하고

     

    그 인증정보를 기준으로 해서 tokenProvider에서 만들었던 createToken메소드를 통해서 jwt토큰을 생성하게 됨

     

    jwt token을 리스폰스헤더에도 넣어주고,

     

    우리가 만들었던 TokenDto를 통해 리스폰스바디에도 넣어서 리턴하게됨

     

    =>둘다 되는걸 보여준거래

     

    ===

    우리가 만든 로그인 API를 포스트맨으로 테스트

     

    포스트맨의 유용한 기능 Test탭

    responseBody에 있는 내용을 parse해서 포스트맨의(pm) 전역변수(globals)에 설정(set)해서 다른 리퀘스트에서도 해당 변수에 있는 값을 끌어서 쓸 수 있게됨

Designed by Tistory.