있을 유, 참 진

[JWT] 로그인 구현 1::로그인 기능 구현, JWT Provider 생성 본문

Spring/JWT

[JWT] 로그인 구현 1::로그인 기능 구현, JWT Provider 생성

U_ma 2023. 4. 11. 01:06
로그인 기능 구현
   - 사용법 설명
JWT 관련 값 설정
loadUserByUsername(final String username)
   - User createUser(String username, User user)
JWT Provider 생성

   - String createToken(Authentication authentication)
   - Authentication getAuthentication(String token)
   - boolean validateToken(String token)

로그인 기능 구현

💡 로그인에 관련된 Controller 구현, `api/auth/authenticate` 을 호출하면 로그인이 된다. 순서는 아래와 같다.
  1. 유저의 아이디와 비밀번호 값을 이용해 authenticationToken 생성
  2. authenticationManager에서 토큰을 검증 후 검증된 토큰을 반환
  3. SecurityContextHolder에 해당 authentication 값을 저장
  4. JwtTokenProvider를 통해서 JWT 토큰을 생성한다
  5. 생성된 토큰을 헤더에 넣어 반환, 이후 해당 토큰 값은 추가된 JwtFilter에서 검증된다.
@RequiredArgsConstructor
@RequestMapping("/api/auth")
@RestController
public class AuthApi {
  private final JwtTokenProvider jwtTokenProvider;
  private final UserService userService;
  private final AuthenticationManagerBuilder authenticationManager;

  @PostMapping("/authenticate")
  public ResponseEntity<TokenResponseDto> login(@Valid @RequestBody LoginRequestDto loginRequestDto) {
      //1. AuthenticationManager를 통해 인증을 시도하고 인증이 성공하면 Authentication 객체를 리턴받는다.
      UsernamePasswordAuthenticationToken authenticationToken =
              new UsernamePasswordAuthenticationToken(loginRequestDto.getUsername(), loginRequestDto.getPassword());
      // 해당 부분에 왔을 때 UserDetailsServiceImpl의 loadUserByUsername() 메소드가 실행된다.
      Authentication authentication = authenticationManager.getObject().authenticate(authenticationToken);

      //2. SecurityContextHolder에 위에서 생성한 Authentication 객체를 저장한다.
      SecurityContextHolder.getContext().setAuthentication(authentication);
      //3. JwtTokenProvider를 통해 JWT 토큰을 생성한다.
      String jwtToken = jwtTokenProvider.createToken(authentication);

      //4. 생성한 JWT 토큰을 Response Header에 담아서 리턴한다.
      HttpHeaders httpHeaders = new HttpHeaders();
      httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwtToken);

      return ResponseEntity.status(HttpStatus.OK).headers(httpHeaders).body(new TokenResponseDto(jwtToken));
  }
}

JWT 토큰과 관련된 JwtTokenProvider가 필요하고 authenticationManager 검증을 할 때 사용할 loadUserByUsername(final String username) 구현

JWT 관련 값 설정

💡 yml or properties 파일에 jwt 관련 값 세팅
jwt:
  header: Authorization
  # 특정값을 Base64로 인코딩한 값
  secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
  # 토큰 만료 시간
  token-validity-in-seconds: 1800000

loadUserByUsername(final String username)

💡 인터페이스인 UserDetailsService를 구현, 본인은 UserService에 해당 값을 구현받아서 loadUserByUsername(final String username)을 오버라이딩해 구현했다.
@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    /**
     * 로그인 시에 DB에서 유저정보와 권한정보를 가져와 UserDetails 타입으로 리턴한다.
     * @param username the username identifying the user whose data is required.
     * @return 유저정보
     * @throws UsernameNotFoundException
     */
    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username)
                .map(user -> createUser(username, user))
                .orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
    }
}

User createUser(String username, User user)

💡 유저의 등급 권한 값을 받아 넣은 후, UserDetails의 구현체인 User값에 담아서 반환해 준다.
private org.springframework.security.core.userdetails.User createUser(String username, User user) {
    if(Objects.equals(user.isActivated(), false)) throw new RuntimeException(username + " -> 활성화되지 않은 사용자입니다.");

    List<GrantedAuthority> authorities = user.getAuthorities().stream().map(auth ->
                    new SimpleGrantedAuthority(auth.getName()))
            .collect(Collectors.toList());

    return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}

JWT Provider 생성

💡 JWT에 관련된 클래스. 토큰 생성, 토큰 → Authentication, 토큰 검증 등 JWT 토큰과 관련된 모든 것을 다 해준다.
//...생략
/**
 * JWT 토큰 생성, 추출 그리고 검증에 관한 클래스
 */
@Slf4j
@Component
public class JwtTokenProvider implements InitializingBean {
    private static final String AUTHORITIES_KEY = "auth";
    private final String secretKey;
    private final long tokenValidityInMilliseconds;
    private Key key;

    // 1. Bean 생성 후 주입 받은 후에
    public JwtTokenProvider(@Value("${jwt.secret}") String secretKey,
                            @Value("${jwt.token-validity-in-seconds}") Long tokenValidityInSeconds) {
        this.secretKey = secretKey;
        this.tokenValidityInMilliseconds = tokenValidityInSeconds;
    }
    // 2. secret 값을 Base64로 디코딩해 Key변수에 할당
    @Override
    public void afterPropertiesSet() throws Exception {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    /**
     * Authentication 객체의 권한 정보를 이용해 토큰 생성
     * @param authentication - Authentication 객체
     * @return - 토큰
     */
    public String createToken(Authentication authentication) {
        //권한 값을 받아 하나의 문자열로 합침
        String authorities = authentication.getAuthorities().stream()
                .map(auth -> auth.getAuthority())
                .collect(Collectors.joining(","));

        long now = new Date().getTime();
        Date validity = new Date(now + this.tokenValidityInMilliseconds);

        return Jwts.builder()
                .setSubject(authentication.getName()) // 페이로드 주제 정보
                .claim(AUTHORITIES_KEY, authorities)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(validity) 
                .compact();
    }

    /**
     * 토큰에서 인증 정보 조회 후 Authentication 객체 리턴
     * @param token
     * @return
     */
    //토큰 -> 클레임 추출 -> 유저 객체 제작 -> Authentication 객체 리턴
    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();

        List<? extends SimpleGrantedAuthority> 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);
    }

    /**
     * 필터에서 사용할 토큰 검증
     * @param token 필터 정보
     * @return 토큰이 유효 여부
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            log.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }
}

String createToken(Authentication authentication)

💡 Login에 성공했을 때 JWT 토큰을 만들어 헤더에 넣어 반환하는 부분에 사용할 메서드
public String createToken(Authentication authentication) {
    //권한 값을 받아 하나의 문자열로 합침
    String authorities = authentication.getAuthorities().stream()
            .map(auth -> auth.getAuthority())
            .collect(Collectors.joining(","));

    long now = new Date().getTime();
    Date validity = new Date(now + this.tokenValidityInMilliseconds);

    return Jwts.builder()
            .setSubject(authentication.getName()) // 페이로드 주제 정보
            .claim(AUTHORITIES_KEY, authorities)
            .signWith(key, SignatureAlgorithm.HS512)
            .setExpiration(validity) 
            .compact();
}

Authentication getAuthentication(String token)

💡 토큰값을 받아서 Authentication 객체로 반환해 주는 메서드, 앞으로 구현할 JWTFilter에서 사용한다.
public Authentication getAuthentication(String token) {
    Claims claims = Jwts.parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .getBody();

    List<? extends SimpleGrantedAuthority> 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);
}

boolean validateToken(String token)

💡 JWT 토큰을 검증하는 메서드, 앞으로 구현할 JWTFilter에서 사용한다.
/**
 * 필터에서 사용할 토큰 검증
 * @param token 필터 정보
 * @return 토큰이 유효 여부
 */
public boolean validateToken(String token) {
    try {
        Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        return true;
    } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
        log.info("잘못된 JWT 서명입니다.");
    } catch (ExpiredJwtException e) {
        log.info("만료된 JWT 토큰입니다.");
    } catch (UnsupportedJwtException e) {
        log.info("지원되지 않는 JWT 토큰입니다.");
    } catch (IllegalArgumentException e) {
        log.info("JWT 토큰이 잘못되었습니다.");
    }
    return false;
}
Comments