KeywordKatch user component 개발 - 로그인 기능

Spring Security를 사용해 토큰 방식의 인증, 인가를 구현할 수 있다.
스프링 시큐리티는 기본적으로 UsernamePasswordAuthenticationFilter를 통해 인증이 진행되는데 이 필터에서 인증이 실패하면 로그인 폼이 포함된 화면을 전달하는데 user component에는 화면이 없기 때문에 JWT 인증 필터를 구현하고 UsernamePasswordAuthenticationFilter 앞에 인증 필터를 배치해서 인증 주체를 변경해야 한다.

구현

스프링 시큐리티에게 인증 인가 절차를 부탁하려면 다음과 같은 객체들을 구현해줘야 한다

  • UserDetails - 사용자 정보를 담는 객체에 대한 인터페이스
  • UserDetailService - 스프링 시큐리티가 사용자 정보에 획득할 때 필요한 인터페이스. UserDetails 인스턴스를 반환한다
  • JwtTokenProvider - UserDetails 인스턴스에서 정보를 가져와 JWT를 생성한다

CustomUserDetails

의 예제에서는 @Entity 클래스가 바로 UserDetails를 구현했는데 JPA에게 필요한 클래스와 Spring Security를 위한 클래스를 분리하여 가독성을 높이기 위해 User 엔티티와 CustomUserDetails를 따로 만들었다.

@Getter
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

    private final User user;

    /**
     * 해당 회원의 접근 권한 반환
     * @return List<SimpleGrantedAuthority>
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.user.getRoles()
                .stream()
                .map(Role::toString)
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return this.user.getPassword();
    }

    /* Spring Security에서 username은 계정을 식별할 수 있는 고유한 값을 의미한다.
     * (이메일, PK 등 가능)
     * 회원의 이름만을 뜻하는게 아니다.*/
    @Override
    public String getUsername() {
        return user.getUserId().toString();
    }

    // 해당 기능 없음
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 해당 기능 없음
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 비밀번호 만료 여부 기능 없음
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 사용자 활성화 여부 기능 없음
    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserDetailServiceImpl

@Service
@RequiredArgsConstructor
public class UserDetailServiceImpl implements UserDetailsService {

    private final Logger LOGGER = LoggerFactory.getLogger(UserDetailServiceImpl.class);
    private final UserRepository userRepository;

    /*
     * 회원 정보를 불러와서 UserDetails로 반환한다.
     * username은 회원을 식별할수 있는 값이다.
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User desiredUser;
        Long userId = Long.valueOf(username);
        LOGGER.info("[loadUserByUsername] username: {} ", username);

        desiredUser = userRepository.findById(userId)
                .orElseThrow(()->new UsernameNotFoundException("no such user"));

        return new CustomUserDetails(desiredUser);
    }
}

JwtTokenProvider

token을 생성하는 부분에서 header를 생성하는 코드를 따로 작성하지 않았다.

다음과 같은 메서드가 정의되어 있다.

  • createToken - 특정 회원에 대한 토큰을 생성한다. 생성된 토큰의 claim은 다음과 같다.
      {
          // payload
          "aud" : {audience},
          "iat" : {issuedAt},
          "exp" : {now} + {tokenValidMillisecond},
          "user_id": {userId},
          "roles" : {roles}
      }
    
  • getAuthentication - 전달받은 토큰에 대한 Authentication 인스턴스를 반환한다. Authentication은 인증에 성공한 token을 표현한 객체이다. SecurityContextHolder에 저장된다.
  • extractUsername - 토큰에서 username을 추출한다.
  • resolveToken - 요청 서블릿 인스턴스에서 토큰을 추출한다.
  • validateToken - 토큰이 유효한지 확인한다.

references

Comments