인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

유요한님의 프로필 이미지

작성한 질문수

Spring Boot JWT Tutorial

JWT에 생성할 때 질문

해결된 질문

작성

·

2K

1

JwtProvider

package com.example.project1.config.jwt;

import com.example.project1.config.auth.PrincipalDetails;
import com.example.project1.domain.jwt.TokenDTO;
import com.example.project1.domain.member.UserType;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.security.Keys;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Component
public class JwtProvider {

    private static final String AUTHORITIES_KEY = "auth";

    @Value("${jwt.access.expiration}")
    private long accessTokenTime;

    @Value("${jwt.refresh.expiration}")
    private long refreshTokenTime;

    private Key key;


    public JwtProvider( @Value("${jwt.secret_key}") String secret_key) {
        byte[] secretByteKey = DatatypeConverter.parseBase64Binary(secret_key);
        this.key = Keys.hmacShaKeyFor(secretByteKey);
    }

//     유저 정보를 가지고 AccessToken, RefreshToken 을 생성하는 메소드
    public TokenDTO createToken(Authentication authentication, List<GrantedAuthority> authorities) {
        //  UsernamePasswordAuthenticationToken
        //  [Principal=zxzz45@naver.com, Credentials=[PROTECTED], Authenticated=false, Details=null, Granted Authorities=[]]
        // 여기서 Authenticated=false는 아직 정상임
        // 이 시점에서는 아직 실제로 인증이 이루어지지 않았기 때문에 Authenticated 속성은 false로 설정
        // 인증 과정은 AuthenticationManager와 AuthenticationProvider에서 이루어지며,
        // 인증이 성공하면 Authentication 객체의 isAuthenticated() 속성이 true로 변경됩니다.
        log.info("authentication in JwtProvider : " + authentication);

        // userType in JwtProvider : ROLE_USER
        log.info("userType in JwtProvider : " + authorities);

        // 권한 가져오기
        //  authentication 객체에서 권한 정보(GrantedAuthority)를 가져와 문자열 형태로 변환한 후,
        //  쉼표로 구분하여 조인한 결과를 authorities 변수에 저장합니다. 따라서 authorities는 권한 정보를 문자열 형태로 가지게 됩니다.
        // 권한 정보를 문자열로 변환하여 클레임에 추가하는 방식
//        String authorities = authentication.getAuthorities().stream()
//                .map(GrantedAuthority::getAuthority)
//                .collect(Collectors.joining(","));

        Map<String, Object> claims = new HashMap<>();
        claims.put(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));

        log.info("claims in JwtProvider : " + claims);
        log.info("authentication.getName() in JwtProvider : " + authentication.getName());

        long now = (new Date()).getTime();
        Date now2 = new Date();



        // AccessToken 생성
        Date accessTokenExpire = new Date(now + this.accessTokenTime);
        String accessToken = Jwts.builder()
                // 내용 sub : 유저의 이메일
                // 토큰 제목
                // JWT의 "sub" 클레임을 설정하는 메서드입니다.
                // "sub" 클레임은 일반적으로 사용자를 식별하는 용도로 사용되며,
                // 이메일과 같은 사용자의 고유한 식별자를 담고 있을 수 있습니다.
                .setSubject(authentication.getName())
                .setIssuedAt(now2)
                // 클레임 id : 유저 ID
//                .claim(AUTHORITIES_KEY, authorities)
                .setClaims(claims)
                // 내용 exp : 토큰 만료 시간, 시간은 NumericDate 형식(예: 1480849143370)으로 하며
                // 항상 현재 시간 이후로 설정합니다.
                .setExpiration(accessTokenExpire)
                // 서명 : 비밀값과 함께 해시값을 ES256 방식으로 암호화
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        Claims claims2 = Jwts.parser().setSigningKey(key).parseClaimsJws(accessToken).getBody();
        String subject = claims2.getSubject();
        log.debug("claims subject 확인 in JwtProvider : " + subject);



//         Claims claim = Jwts.parserBuilder()
//                .setSigningKey(key)
//                        .build()
//                                .parseClaimsJws(accessToken).getBody();


        // accessToken in JwtProvider : eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ6eHp6NDVAbmF2ZXIuY2
        // 9tIiwiaWF0IjoxNjg5OTk1MzM3LCJhdXRoIjoiUk9MRV9VU0VSIiwiZXhwIjoxNjkzNTk1MzM3fQ.2_2PR-A
        // X9N0jKDyA7LpK7xRRBZBYZ17_f8Jq2TY4ny8
        log.info("accessToken in JwtProvider : " + accessToken);

        // claim에서 auth 확인 in JwtProvider : ROLE_USER
        log.info("claim에서 accessToken에 담김 auth 확인 in JwtProvider : " + claims);

        // RefreshToken 생성
        Date refreshTokenExpire = new Date(now + this.refreshTokenTime);
        String refreshToken = Jwts.builder()
                .setSubject(authentication.getName())
                .setClaims(claims)
                .setIssuedAt(now2)
                .setExpiration(refreshTokenExpire)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();



        log.info("refreshToken in JwtProvider : " + refreshToken);
        log.info("claim에서 refreshToken에 담긴 auth 확인 in JwtProvider : " + claims);

        TokenDTO tokenDTO= TokenDTO.builder()
                .grantType("Bearer ")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .accessTokenTime(accessTokenExpire)
                .refreshTokenTime(refreshTokenExpire)
                // principalDeatails에서 getUserName 메소드가 반환한 것을 담아준다.
                // 이메일을 반환하도록 구성했으니 이메일이 반환됩니다.
                .userEmail(authentication.getName())
                .build();

        log.info("tokenDTO in JwtProvider : " + tokenDTO);

        return tokenDTO;
    }

    // 소셜 로그인 성공시 JWT 발급
    public TokenDTO createToken2(UserDetails userDetails) {
        long now = (new Date()).getTime();
        Date now2 = new Date();

        // userDetails.getAuthorities()는 사용자의 권한(authorities) 정보를 가져오는 메서드입니다.
        // claims.put("roles", userDetails.getAuthorities()) 코드는 사용자의 권한 정보를 클레임에 추가하는 것입니다.
        // 클레임에는 "roles"라는 키로 사용자의 권한 정보가 저장되며, 해당 권한 정보는 JWT의 페이로드 부분에 포함됩니다.
        Claims claims = Jwts.claims().setSubject(userDetails.getUsername());
        claims.put("auth", userDetails.getAuthorities());

        log.info("claims : " + claims);

        // access token
        Date accessTokenExpire = new Date(now + this.accessTokenTime);
        String accessToken = Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setClaims(claims)
                .setIssuedAt(now2)
                .setExpiration(accessTokenExpire)
                .signWith(key,SignatureAlgorithm.HS256)
                .compact();

        // RefreshToken 생성
        Date refreshTokenExpire = new Date(now + this.refreshTokenTime);
        String refreshToken = Jwts.builder()
                .setIssuedAt(now2)
                .setClaims(claims)
                .setExpiration(refreshTokenExpire)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        TokenDTO tokenDTO = TokenDTO.builder()
                .grantType("Bearer ")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .userEmail(userDetails.getUsername())
                .build();

        log.info("tokenDTO in JwtProvider : " + tokenDTO);
        return tokenDTO;
    }

    // accessToken 생성
    // 리프레시 토큰을 사용하여 새로운 액세스 토큰을 생성하는 로직을 구현
    public TokenDTO createAccessToken(String userEmail, List<GrantedAuthority> authorities) {
        Long now = (new Date()).getTime();
        Date now2 = new Date();
        Date accessTokenExpire = new Date(now + this.accessTokenTime);

        log.info("authorities : " + authorities);

        Map<String, Object> claims = new HashMap<>();
        claims.put(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));

        log.info("claims : " + claims);

        String accessToken = Jwts.builder()
                .setIssuedAt(now2)
                .setSubject(userEmail)
                .setExpiration(accessTokenExpire)
                .setClaims(claims)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        log.info("accessToken in JwtProvider : " + accessToken);

//        log.info("claim에서 accessToken에 담김 auth 확인 in JwtProvider : " + auth);

        TokenDTO tokenDTO = TokenDTO.builder()
                .grantType("Bearer ")
                .accessToken(accessToken)
                .userEmail(userEmail)
                .accessTokenTime(accessTokenExpire)
                .build();

        log.info("tokenDTO in JwtProvider : " + tokenDTO);
        return tokenDTO;
    }


    // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 코드
    // 토큰으로 클레임을 만들고 이를 이용해 유저 객체를 만들어서 최종적으로 authentication 객체를 리턴
    // 인증 정보 조회
    public Authentication getAuthentication(String token) {
        // 토큰 복호화 메소드
        Claims claims = parseClaims(token);
        log.info("claims in JwtProvider  : " + claims);

        if(claims.get("auth") == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }

        Object auth = claims.get("auth");
        // [ROLE_USER]
        log.info("auth in JwtProvider : " + auth);

        // 클레임 권한 정보 가져오기
         List<String> authorityStrings = (List<String>) claims.get(AUTHORITIES_KEY);
        // [ROLE_USER]
        log.info("authorityStrings in JwtProvider : " + authorityStrings);

        Collection<? extends GrantedAuthority> authorities =
                authorityStrings.stream()
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        // [ROLE_USER]
        log.info("authorities in JwtProvider : " + authorities);

        // UserDetails 객체를 만들어서 Authentication 리턴
//        User principal = new User(claims.getSubject(), "", authorities);
//        log.info("principal in JwtProvider : " + principal);
        log.info("claims.getSubject() in JwtProvider : " + claims.getSubject());

        return new UsernamePasswordAuthenticationToken(claims.getSubject(), token, authorities);
    }

    private Claims parseClaims(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.info("ExpiredJwtException : " + e.getMessage());
            log.info("ExpiredJwtException : " + e.getClaims());

            return e.getClaims();
        }
    }

    // 토큰의 유효성 검증을 수행
    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;
    }


}

JwtAuthenticationFilter

package com.example.project1.config.jwt;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URI;

// 클라이언트 요청 시 JWT 인증을 하기 위해 설치하는 커스텀 필터로
// UsernamePasswordAuthenticationFiler 이전에 실행된다.
// 이전에 실행된다는 뜻은 JwtAuthenticationFilter 를 통과하면
// UsernamePasswordAuthenticationFilter 이후의 필터는 통과한 것으로 본다는 뜻이다.
// 쉽게 말해서, Username + Password 를 통한 인증을 Jwt 를 통해 수행한다는 것이다.

// JWT 방식은 세션과 다르게 Filter 하나를 추가해야 합니다.
// 이제 사용자가 로그인을 했을 때, Request 에 가지고 있는 Token 을 해석해주는 로직이 필요합니다.
// 이 역할을 해주는것이 JwtAuthenticationFilter입니다.
// 세부 비즈니스 로직들은 TokenProvider에 적어둡니다. 일종의 service 클래스라고 생각하면 편합니다.
// 1. 사용자의 Request Header에 토큰을 가져옵니다.
// 2. 해당 토큰의 유효성 검사를 실시하고 유효하면
// 3. Authentication 인증 객체를 만들고
// 4. ContextHolder에 저장해줍니다.
// 5. 해당 Filter 과정이 끝나면 이제 시큐리티에 다음 Filter로 이동하게 됩니다.

@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends GenericFilterBean {

    public static final String HEADER_AUTHORIZATION = "Authorization";
    private final JwtProvider jwtProvider;

    // doFilter는 토큰의 인증정보를 SecurityContext에 저장하는 역할 수행

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        // Request Header에서 JWT 토큰을 추출
        //  요청 헤더에서 JWT 토큰을 추출하는 역할
        String jwt = resolveToken(httpServletRequest);
        //  jwt :
        // eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ6eHp6NDVAbmF2ZXIuY29tIiwiaWF
        // 0IjoxNjg5OTQ0OTk0LCJhdXRoIjoiIiwiZXhwIjoxNjg5OTQ1MzU0fQ.qyR2bJMDmNb1iv
        // q6a4W55dGBmyFEzaENN1-F7qPlJKw
        log.info("jwt : " + jwt);
        String requestURI = httpServletRequest.getRequestURI();
        // requestURI/api/v1/users/1
        log.info("requestURI" + requestURI);

        if(StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)){
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장
            Authentication authentication = jwtProvider.getAuthentication(jwt);
            // UsernamePasswordAuthenticationToken
            // [Principal=null, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]
            log.info("authentication in JwtAuthenticationFilter : " + authentication);
            SecurityContextHolder.getContext().setAuthentication(authentication);

            log.info("Security Context에 인증 정보를 저장했습니다. 정보 : {}",authentication.getName());
        } else {
            log.debug("유효한 JWT 토큰이 없습니다. uri : {}", requestURI);
        }
        chain.doFilter(request, response);
    }


    // Request Header 에서 토큰 정보를 꺼내오기 위한 메소드
    // HEADER_AUTHORIZATION로 정의된 헤더 이름을 사용하여 토큰을 찾고,
    // 토큰이 "Bearer "로 시작하는 경우에만 실제 토큰 값을 반환
    private String resolveToken(HttpServletRequest httpServletRequest) {
        String bearerToken = httpServletRequest.getHeader(HEADER_AUTHORIZATION);

        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        } else {
            return null;
        }
    }
}

PrincipalDetailsService

package com.example.project1.config.auth;

import com.example.project1.entity.member.MemberEntity;
import com.example.project1.repository.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.Service;

// http://localhost:8080/login ← 이 때 동작을 함

@Service
@RequiredArgsConstructor
@Slf4j
public class PrincipalDetailsService implements UserDetailsService {

    private MemberRepository memberRepository;

    // 시큐리티 session = Authentication = UserDetails
    // 함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다.
    @Override
    public UserDetails loadUserByUsername(String userEmail) throws UsernameNotFoundException {
        MemberEntity member = memberRepository.findByUserEmail(userEmail);
        log.info("user in PrincipalDetailsService : " + member);
        return new PrincipalDetails(member);
    }


}

 

PrincipalDetails

package com.example.project1.config.auth;

import com.example.project1.entity.member.MemberEntity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

@Setter
@Getter
@ToString
@NoArgsConstructor
@Slf4j
public class PrincipalDetails implements UserDetails, OAuth2User {

    // 일반 로그인 정보를 저장하기 위한 필드
    private MemberEntity member;
    // OAuth2 로그인 정보를 저장하기 위한 필드
    // attributes는 Spring Security에서 OAuth2 인증을 수행한 후에 사용자에 대한 추가 정보를 저장하는 데 사용되는 맵(Map)입니다.
    // OAuth2 인증은 사용자 인증 후에 액세스 토큰(Access Token)을 발급받게 되는데,
    // 이 토큰을 사용하여 OAuth2 서비스(provider)로부터 사용자의 프로필 정보를 요청할 수 있습니다.
    // 예를 들어, 소셜 로그인을 사용한 경우에는 attributes에는 사용자의 소셜 서비스(provider)에서 제공하는 프로필 정보가 담겨 있습니다.
    // 소셜 로그인 서비스(provider)마다 제공하는 프로필 정보가 다를 수 있습니다.
    // 일반적으로 attributes에는 사용자의 아이디(ID), 이름, 이메일 주소, 프로필 사진 URL 등의 정보가 포함됩니다.
    /*
    *   구글의 경우
    *   {
            "sub": "100882758450498962866", // 구글에서 발급하는 고유 사용자 ID
            "name": "John Doe", // 사용자 이름
            "given_name": "John", // 이름(이름 부분)
            "family_name": "Doe", // 성(성(성) 부분)
            "picture": "https://lh3.googleusercontent.com/a/AAcHTtdzQomNwZCruCcM0Eurcf8hAgBHcgwvbXEBQdw3olPkSg=s96-c", // 프로필 사진 URL
            "email": "johndoe@example.com", // 이메일 주소
            "email_verified": true, // 이메일 주소 인증 여부
            "locale": "en" // 지역 설정
        }
    * */
    private Map<String, Object> attributes;


    // 일반 로그인
    public PrincipalDetails(MemberEntity member) {
        this.member = member;
    }


    // OAuth2 로그인
    public PrincipalDetails(MemberEntity member, Map<String, Object> attributes) {
        this.member = member;
        this.attributes = attributes;
    }

    // 해당 유저의 권한을 리턴하는 곳
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new SimpleGrantedAuthority("ROLE_" + member.getUserType().toString()));
        log.info("collection : " + collection);
        return collection;
    }


    // 사용자 패스워드를 반환
    @Override
    public String getPassword() {
        return member.getUserPw();
    }

    // 사용자 이름 반환
    @Override
    public String getUsername() {
        return member.getUserEmail();
    }


    // 계정 만료 여부 반환
    @Override
    public boolean isAccountNonExpired() {
        // 만료되었는지 확인하는 로직
        // true = 만료되지 않음
        return true;
    }

    // 계정 잠금 여부 반환
    @Override
    public boolean isAccountNonLocked() {
        // true = 잠금되지 않음
        return true;
    }

    // 패스워드의 만료 여부 반환
    @Override
    public boolean isCredentialsNonExpired() {
        // 패스워드가 만료되었는지 확인하는 로직
        // true = 만료되지 않음
        return true;
    }

    // 계정 사용 가능 여부 반환
    @Override
    public boolean isEnabled() {
        // 계정이 사용 가능한지 확인하는 로직
        // true = 사용 가능
        return true;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    @Override
    // OAuth2 인증에서는 사용되지 않는 메서드이므로 null 반환
    public String getName() {
        return null;
    }
}

controller

   // 로그인
    @PostMapping("/api/v1/users/login")
    public ResponseEntity<?> login(@RequestBody MemberDTO memberDTO) throws Exception {
        log.info("member : " + memberDTO);
        try {
            log.info("-----------------");

            ResponseEntity<TokenDTO> login =
                    memberService.login(memberDTO.getUserEmail(), memberDTO.getUserPw());
            log.info("login : " + login);

            return ResponseEntity.ok().body(login);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    }

    // refresh로 access 토큰 재발급
    // @RequsetHeader"Authorization")은 Authorization 헤더에서 값을 추출합니다.
    // 일반적으로 리프레시 토큰은 Authorization 헤더의 값으로 전달되며,
    // Bearer <token> 형식을 따르는 경우가 일반적입니다. 여기서 <token> 부분이 실제 리프레시 토큰입니다
    // 로 추출하면 다음과 같이 문자열로 나온다.
    // Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String token) throws Exception {
        try {
            ResponseEntity<TokenDTO> accessToken = refreshTokenService.createAccessToken(token);
            return ResponseEntity.ok().body(accessToken);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

MemberService

package com.example.project1.service.member;

import com.example.project1.config.jwt.JwtAuthenticationFilter;
import com.example.project1.config.jwt.JwtProvider;
import com.example.project1.domain.jwt.TokenDTO;
import com.example.project1.domain.member.MemberDTO;
import com.example.project1.domain.member.UserType;
import com.example.project1.entity.jwt.TokenEntity;
import com.example.project1.entity.member.MemberEntity;
import com.example.project1.entity.member.embedded.AddressEntity;
import com.example.project1.repository.jwt.TokenRepository;
import com.example.project1.repository.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
@RequiredArgsConstructor
@Slf4j
public class MemberService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final JwtProvider jwtProvider;
    private final TokenRepository tokenRepository;

    // 회원가입
    public String signUp(MemberDTO memberDTO) throws Exception {

        try {
            MemberEntity byUserEmail = memberRepository.findByUserEmail(memberDTO.getUserEmail());

            if (byUserEmail != null) {
                return "이미 가입된 회원입니다.";
            } else {
                // 아이디가 없다면 DB에 넣어서 등록 해준다.
                MemberEntity member = MemberEntity.builder()
                        .userEmail(memberDTO.getUserEmail())
                        .userPw(passwordEncoder.encode(memberDTO.getUserPw()))
                        .userName(memberDTO.getUserName())
                        .nickName(memberDTO.getNickName())
                        .userType(memberDTO.getUserType())
                        .provider(memberDTO.getProvider())
                        .providerId(memberDTO.getProviderId())
                        .address(AddressEntity.builder()
                                .userAddr(memberDTO.getAddressDTO().getUserAddr())
                                .userAddrDetail(memberDTO.getAddressDTO().getUserAddrDetail())
                                .userAddrEtc(memberDTO.getAddressDTO().getUserAddrEtc())
                                .build())
                        .build();

                log.info("member : " + member);
                memberRepository.save(member);

//            MemberDTO memberDTO1 = MemberDTO.toMemberDTO(Optional.of(save));

                return "회원가입에 성공했습니다.";
            }

        } catch (Exception e) {
            log.error(e.getMessage());
            throw e; // 예외를 던져서 예외 처리를 컨트롤러로 전달
        }

    }

    // 아이디 조회
    public MemberDTO search(Long userId) {
        Optional<MemberEntity> searchId = memberRepository.findById(userId);
        MemberDTO memberDTO = MemberDTO.toMemberDTO(searchId);
        return memberDTO;
    }

    // 회원 삭제
    public String remove(Long userId) {
        MemberEntity member = memberRepository.deleteByUserId(userId);

        if(member == null) {
            return "회원 탈퇴 완료!";
        } else {
            return "회원 탈퇴 실패!";
        }
    }

    // 로그인
    public ResponseEntity<TokenDTO> login(String userEmail, String userPw) throws Exception {

        MemberEntity findUser = memberRepository.findByUserEmail(userEmail);
        log.info("findUser : " + findUser);


        if (findUser != null) {
            // 사용자가 입력한 패스워드를 암호화하여 사용자 정보와 비교
            if (passwordEncoder.matches(userPw, findUser.getUserPw())) {
                // UsernamePasswordAuthenticationToken은 Spring Security에서
                // 사용자의 이메일과 비밀번호를 이용하여 인증을 진행하기 위해 제공되는 클래스
                // 이후에는 생성된 authentication 객체를 AuthenticationManager를 이용하여 인증을 진행합니다.
                // AuthenticationManager는 인증을 담당하는 Spring Security의 중요한 인터페이스로, 실제로 사용자의 인증 과정을 처리합니다.
                // AuthenticationManager를 사용하여 사용자가 입력한 이메일과 비밀번호가 올바른지 검증하고,
                // 인증에 성공하면 해당 사용자에 대한 Authentication 객체를 반환합니다. 인증에 실패하면 예외를 발생시킵니다.
                // 인증은 토큰을 서버로 전달하고, 서버에서 해당 토큰을 검증하여 사용자를 인증하는 단계에서 이루어집니다.
                Authentication authentication = new UsernamePasswordAuthenticationToken(userEmail, userPw);

                //  UsernamePasswordAuthenticationToken
                //  [Principal=zxzz45@naver.com, Credentials=[PROTECTED], Authenticated=false, Details=null, Granted Authorities=[]]
                // 여기서 Authenticated=false는 아직 정상임
                // 이 시점에서는 아직 실제로 인증이 이루어지지 않았기 때문에 Authenticated 속성은 false로 설정
                // 인증 과정은 AuthenticationManager와 AuthenticationProvider에서 이루어지며,
                // 인증이 성공하면 Authentication 객체의 isAuthenticated() 속성이 true로 변경됩니다.
                log.info("authentication in MemberService : " + authentication);

                List<GrantedAuthority> authoritiesForUser = getAuthoritiesForUser(findUser);

//                TokenDTO token = jwtProvider.createToken(authentication, findUser.getUserType());
                TokenDTO token = jwtProvider.createToken(authentication, authoritiesForUser);

                log.info("tokenEmail in MemberService : "+ token.getUserEmail());

                TokenEntity checkEmail = tokenRepository.findByUserEmail(token.getUserEmail());
                log.info("checkEmail in MemberService : " + checkEmail);

                // 사용자에게 이미 토큰이 할당되어 있는지 확인합니다.
                if (checkEmail != null) {
                    log.info("이미 발급한 토큰이 있습니다.");
                    //  // 기존 토큰을 업데이트할 때 사용할 임시 객체로 TokenDTO token2를 생성합니다.
                    TokenDTO token2 = TokenDTO.builder()
                            .id(checkEmail.getId())
                            .grantType(token.getGrantType())
                            .accessToken(token.getAccessToken())
                            .refreshToken(token.getRefreshToken())
                            .userEmail(token.getUserEmail())
                            .nickName(findUser.getNickName())
                            .userId(findUser.getUserId())
                            .accessTokenTime(token.getAccessTokenTime())
                            .refreshTokenTime(token.getRefreshTokenTime())
                            .userType(findUser.getUserType())
                            .build();
                    // 기존 토큰을 업데이트할 때 사용할 임시 객체로 TokenEntity tokenEntity2를 생성합니다.
                    TokenEntity updateToken = TokenEntity.builder()
                            .id(token2.getId())
                            .grantType(token2.getGrantType())
                            .accessToken(token2.getAccessToken())
                            .refreshToken(token2.getRefreshToken())
                            .userEmail(token2.getUserEmail())
                            .nickName(token2.getNickName())
                            .userId(token2.getUserId())
                            .accessTokenTime(token2.getAccessTokenTime())
                            .refreshTokenTime(token2.getRefreshTokenTime())
                            .userType(token2.getUserType())
                            .build();

                    log.info("token in MemberService : " + updateToken);
                    tokenRepository.save(updateToken);
                } else {
                    log.info("발급한 토큰이 없습니다.");
                    token = TokenDTO.builder()
                            .grantType(token.getGrantType())
                            .accessToken(token.getAccessToken())
                            .refreshToken(token.getRefreshToken())
                            .userEmail(token.getUserEmail())
                            .nickName(findUser.getNickName())
                            .userId(findUser.getUserId())
                            .accessTokenTime(token.getAccessTokenTime())
                            .refreshTokenTime(token.getRefreshTokenTime())
                            .userType(findUser.getUserType())
                            .build();

                    // 새로운 토큰을 DB에 저장할 때 사용할 임시 객체로 TokenEntity tokenEntity를 생성합니다.
                    TokenEntity newToken = TokenEntity.builder()
                            .grantType(token.getGrantType())
                            .accessToken(token.getAccessToken())
                            .refreshToken(token.getRefreshToken())
                            .userEmail(token.getUserEmail())
                            .nickName(token.getNickName())
                            .userId(token.getUserId())
                            .accessTokenTime(token.getAccessTokenTime())
                            .refreshTokenTime(token.getRefreshTokenTime())
                            .userType(token.getUserType())
                            .build();


                    log.info("token in MemberService : " + newToken);
                    tokenRepository.save(newToken);
                }
                HttpHeaders headers = new HttpHeaders();
                // response header에 jwt token을 넣어줌
                headers.add(JwtAuthenticationFilter.HEADER_AUTHORIZATION, "Bearer " + token);

                return new ResponseEntity<>(token, headers, HttpStatus.OK);
            }
        } else {
            return null;
        }
        return null;
    }

    private List<GrantedAuthority> getAuthoritiesForUser(MemberEntity member) {
        // 예시: 데이터베이스에서 사용자의 권한 정보를 조회하는 로직을 구현
        // member 객체를 이용하여 데이터베이스에서 사용자의 권한 정보를 조회하는 예시로 대체합니다.
        UserType role = member.getUserType();  // 사용자의 권한 정보를 가져오는 로직 (예시)

        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_" +role.name()));
        log.info("role in MemberService : " + role.name());
        log.info("authorities in MemberService : " + authorities);
        return authorities;
    }


    // 회원정보 수정
    public MemberDTO update(MemberDTO memberDTO) {

        MemberEntity findUser = memberRepository.findByUserEmail(memberDTO.getUserEmail());

        if(findUser != null) {
            findUser = MemberEntity.builder()
                    .userPw(passwordEncoder.encode(memberDTO.getUserPw()))
                    .userType(memberDTO.getUserType())
                    .userName(memberDTO.getUserName())
                    .nickName(memberDTO.getNickName())
                    .address(AddressEntity.builder()
                            .userAddr(memberDTO.getAddressDTO().getUserAddr())
                            .userAddrDetail(memberDTO.getAddressDTO().getUserAddrDetail())
                            .userAddrEtc(memberDTO.getAddressDTO().getUserAddrEtc())
                            .build()).build();

            memberRepository.save(findUser);
            MemberDTO modifyUser = MemberDTO.toMemberDTO(Optional.of(findUser));
            return modifyUser;
        } else {
            MemberEntity member = MemberEntity.builder()
                    .userEmail(memberDTO.getUserEmail())
                    .userPw(passwordEncoder.encode(memberDTO.getUserPw()))
                    .userName(memberDTO.getUserName())
                    .nickName(memberDTO.getNickName())
                    .userType(memberDTO.getUserType())
                    .address(AddressEntity.builder()
                            .userAddr(memberDTO.getAddressDTO().getUserAddr())
                            .userAddrDetail(memberDTO.getAddressDTO().getUserAddrDetail())
                            .userAddrEtc(memberDTO.getAddressDTO().getUserAddrEtc())
                            .build())
                    .build();

            memberRepository.save(member);
            // 제대로 DTO 값이 엔티티에 넣어졌는지 확인하기 위해서
            // 엔티티에 넣어주고 다시 DTO 객체로 바꿔서 리턴을 해줬습니다.
            MemberDTO memberDto = MemberDTO.toMemberDTO(Optional.of(member));
            log.info("memberDto : " + memberDto);
            return memberDto;
        }
    }

    // 소셜 로그인 성공시 jwt 반환
    // OAuth2User에서 필요한 정보를 추출하여 UserDetails 객체를 생성하는 메서드
    public ResponseEntity<TokenDTO> createToken(OAuth2User oAuth2User) {
        String userEmail = oAuth2User.getAttribute("email");
        log.info("userEmail in MemberService : " + userEmail);

        MemberEntity findMember = memberRepository.findByUserEmail(userEmail);

        //  권한 정보 추출
        List<GrantedAuthority> authorities = getAuthoritiesForUser(findMember);

        // UserDetails 객체 생성 (사용자의 아이디 정보를 활용)
        // 첫 번째 인자 : username 사용자 아이디
        // 두 번째 인자 : 사용자의 비밀번호
        // 세 번째 인자 : 사용자의 권한 정보를 담은 컬렉션
        UserDetails userDetails = new User(userEmail, null, authorities);
        log.info("userDetails in MemberService : " + userDetails);
        TokenDTO token = jwtProvider.createToken2(userDetails);
        log.info("token in MemberService : " + token);

        return ResponseEntity.ok().body(token);
    }




}

RefreshTokenService

package com.example.project1.service.jwt;

import com.example.project1.config.jwt.JwtAuthenticationFilter;
import com.example.project1.config.jwt.JwtProvider;
import com.example.project1.domain.jwt.TokenDTO;
import com.example.project1.domain.member.UserType;
import com.example.project1.entity.jwt.TokenEntity;
import com.example.project1.entity.member.MemberEntity;
import com.example.project1.repository.jwt.TokenRepository;
import com.example.project1.repository.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class RefreshTokenService {

    private final TokenRepository tokenRepository;
    private final JwtProvider jwtProvider;
    private final MemberRepository memberRepository;

    public ResponseEntity<TokenDTO> createAccessToken(String refreshToken) {

        // refreshToken 유효성 검사하고 true면 넘어감
        if(jwtProvider.validateToken(refreshToken)) {
            TokenEntity findRefreshTokenEmail = tokenRepository.findByRefreshToken(refreshToken);
            // 아이디 추출
            String userEmail = findRefreshTokenEmail.getUserEmail();
            log.info("userEmail : " + userEmail);
            MemberEntity member = memberRepository.findByUserEmail(userEmail);
            log.info("member : " + member);
            // 사용자의 권한 정보를 가져옴
            List<GrantedAuthority> authoritiesForUser = getAuthoritiesForUser(member);

            TokenDTO accessToken = jwtProvider.createAccessToken(userEmail, authoritiesForUser);
            log.info("accessToken : " + accessToken);


            accessToken = TokenDTO.builder()
                    .grantType(accessToken.getGrantType())
                    .accessToken(accessToken.getAccessToken())
                    .userEmail(accessToken.getUserEmail())
                    .nickName(member.getNickName())
                    .userId(member.getUserId())
                    .accessTokenTime(accessToken.getAccessTokenTime())
                    .build();

            TokenEntity tokenEntity = TokenEntity.builder()
                    .grantType(accessToken.getGrantType())
                    .accessToken(accessToken.getAccessToken())
                    .userEmail(accessToken.getUserEmail())
                    .nickName(accessToken.getNickName())
                    .userId(accessToken.getUserId())
                    .accessTokenTime(accessToken.getAccessTokenTime())
                    .build();

            log.info("token : " + tokenEntity);
            tokenRepository.save(tokenEntity);

            HttpHeaders headers = new HttpHeaders();
            // response header에 jwt token을 넣어줌
            headers.add(JwtAuthenticationFilter.HEADER_AUTHORIZATION, "Bearer " + accessToken);

            return new ResponseEntity<>(accessToken, headers, HttpStatus.OK);
        } else {
            throw new IllegalArgumentException("Unexpected token");
                }
            }

    // 주어진 사용자에 대한 권한 정보를 가져오는 로직을 구현하는 메서드입니다.
    // 이 메서드는 데이터베이스나 다른 저장소에서 사용자의 권한 정보를 조회하고,
    // 해당 권한 정보를 List<GrantedAuthority> 형태로 반환합니다.
    private List<GrantedAuthority> getAuthoritiesForUser(MemberEntity member) {
        // 예시: 데이터베이스에서 사용자의 권한 정보를 조회하는 로직을 구현
        // member 객체를 이용하여 데이터베이스에서 사용자의 권한 정보를 조회하는 예시로 대체합니다.
        UserType role = member.getUserType();  // 사용자의 권한 정보를 가져오는 로직 (예시)

        log.info("role : " + role.name());
        List<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.name()));
        return authorities;
    }
}

이런식으로 코드를 작성했습니다.

로그인 시 JWT는 제대로 생성해주는 것을 볼 수 있고 권한이 주어지고 auth에 ROLE_USER이런식으로 들어가는 것을 확인했습니다.

 

근데 .setSubject(authentication.getName())를 넣었는데 헤더에 토큰을 담아서 보내는 테스트를 할 때

 

  public Authentication getAuthentication(String token) {
        // 토큰 복호화 메소드
        Claims claims = parseClaims(token);
        log.info("claims in JwtProvider  : " + claims);

        if(claims.get("auth") == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }

        Object auth = claims.get("auth");
        // [ROLE_USER]
        log.info("auth in JwtProvider : " + auth);

        // 클레임 권한 정보 가져오기
         List<String> authorityStrings = (List<String>) claims.get(AUTHORITIES_KEY);
        // [ROLE_USER]
        log.info("authorityStrings in JwtProvider : " + authorityStrings);

        Collection<? extends GrantedAuthority> authorities =
                authorityStrings.stream()
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        // [ROLE_USER]
        log.info("authorities in JwtProvider : " + authorities);

        // UserDetails 객체를 만들어서 Authentication 리턴
//        User principal = new User(claims.getSubject(), "", authorities);
//        log.info("principal in JwtProvider : " + principal);
        log.info("claims.getSubject() in JwtProvider : " + claims.getSubject());

        return new UsernamePasswordAuthenticationToken(claims.getSubject(), token, authorities);
    }

    private Claims parseClaims(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.info("ExpiredJwtException : " + e.getMessage());
            log.info("ExpiredJwtException : " + e.getClaims());

            return e.getClaims();
        }
    }

auth는 제대로 나오는데 claims.getSubject() 이 부분이 null이 나옵니다. 즉 권한을 제대로 주어지는데 new UsernamePasswordAuthenticationToken(claims.getSubject(), token, authorities); 인증하는 이부분에서 인자가 (이메일, 토큰, 권한)이렇게 주어져야 하는데 (null, 토큰, 권한) 이렇게 갑니다. JwtAuthenticationFilter에서 Authentication authentication = jwtProvider.getAuthentication(jwt); authentication 담아주는 것을 로그로 찍어본 결과 // [Principal=null, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]] 이렇게 로그가 찍혔습니다. 로그를 보면 인증은 true니까 된거 같은데 log.info("Security Context에 인증 정보를 저장했습니다. 정보 : {}", authentication.getName()); 여기서도는 null도 아니고 앞에 문자열만 뜨고 안뜹니다. 근데 JwtProvider에서 log로 찍어보면 이메일이 제대로 나오고 있습니다....

답변 2

0

정은구님의 프로필 이미지
정은구
지식공유자

안녕하세요 🙂

Spring Boot 3.4.0 (SNAPSHOT) 버전에 맞춰 샘플 코드를 업데이트했습니다.

아래 링크에서 Java와 Kotlin 버전의 최신 샘플 코드를 확인하실 수 있으니 참고 부탁드립니다.

Javahttps://github.com/SilverNine/spring-boot-jwt-tutorial

Kotlinhttps://github.com/SilverNine/spring-boot-jwt-tutorial-kotlin

0

인프런 AI 인턴님의 프로필 이미지

안녕하세요, 인프런 AI 인턴이에요. JWT 생성에 관한 질문입니다.

주어진 코드를 보면 JwtProvider 클래스가 있습니다. 이 클래스는 JWT를 생성하는 역할을 수행합니다.

주요 부분은 생성자에서 secret key를 받아서 key를 초기화하는 부분입니다.

secret key는 JWT를 암호화하고 복호화하기 위해 사용되는 키입니다. 이 키는 반드시 안전하게 보관되어야 합니다. 주어진 코드에서는 secret_key라는 속성을 받아와서 byte 배열로 변환하여 key를 생성하고 있습니다.

만약에 secret_key 속성을 어떻게 설정하고 있는지 확인하고 싶으시다면, 속성 파일 또는 환경 변수에서 jwt.secret_key 값을 찾아보시면 됩니다.

이 외에도 JWT의 유효기간을 설정하는 accessTokenTime과 refreshTokenTime도 주어진 코드에서 확인할 수 있습니다. 이 값들은 속성 파일이나 환경 변수에서 설정된 값을 받아오고 있을 것입니다.

JWT 생성을 위해서는 먼저 jwt.secret_key, jwt.access.expiration, jwt.refresh.expiration 값이 설정되어 있어야 합니다. 이 값들을 설정한 뒤에 JwtProvider 클래스의 createToken() 메소드를 호출하면 JWT를 생성할 수 있습니다.

이렇게 생성된 JWT는 클라이언트에게 전달되며, 클라이언트가 요청을 보낼 때마다 JWT가 함께 전송되어 인증을 수행하게 됩니다.

더 자세한 내용이 필요하시다면 JwtProvider 클래스의 내용을 살펴보시면 도움이 될 것입니다. 또한 JWT에 대해 더 알고 싶다면 JWT를 검색해보세요.

요약하자면, JwtProvider 클래스는 secret key를 사용하여 JWT를 생성하는 역할을 합니다. JWT의 유효기간은 설정된 시간에 따라 달라집니다.

추가로 궁금한 점이 있으시다면 댓글로 알려주세요. 감사합니다.