해결된 질문
작성
·
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 버전의 최신 샘플 코드를 확인하실 수 있으니 참고 부탁드립니다.
Java : https://github.com/SilverNine/spring-boot-jwt-tutorial
Kotlin : https://github.com/SilverNine/spring-boot-jwt-tutorial-kotlin
0
안녕하세요, 인프런 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의 유효기간은 설정된 시간에 따라 달라집니다.
추가로 궁금한 점이 있으시다면 댓글로 알려주세요. 감사합니다.