1. JwtUtil
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
private final RefreshTokenRepository refreshTokenRepository;
private final UserDetailsServiceImpl userDetailsService;
private static final String BEARER_PREFIX = "Bearer ";
private static final long ACCESS_TIME = 60 * 60 * 1000L; // ACCESS_TIME 1시간
private static final long REFRESH_TIME = 7 * 24 * 60 * 60 * 1000L; // REFRESH_TIME 7일
public static final String ACCESS_TOKEN = "Access_Token";
public static final String REFRESH_TOKEN = "Refresh_Token";
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// header 토큰을 가져오는 기능
public String getHeaderToken(HttpServletRequest request, String type) {
return type.equals("Access") ? request.getHeader(ACCESS_TOKEN) :request.getHeader(REFRESH_TOKEN);
}
// 토큰 생성
public TokenDto createAllToken(String email) {
return new TokenDto(createToken(email, "Access"), createToken(email, "Refresh"));
}
public String createToken(String email, String type) {
//현재 시각
Date date = new Date();
//지속 시간
long time = type.equals("Access") ? ACCESS_TIME : REFRESH_TIME;
return Jwts.builder()
.setSubject(email)
.setExpiration(new Date(date.getTime() + time))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
throw new CustomException(LOGIN_WRONG_SIGNATURE_JWT_TOKEN);
} catch (ExpiredJwtException e) {
throw new CustomException(LOGIN_EXPIRED_JWT_TOKEN);
} catch (UnsupportedJwtException e) {
throw new CustomException(LOGIN_NOT_SUPPORTED_JWT_TOKEN);
} catch (IllegalArgumentException e) {
throw new CustomException(LOGIN_WRONG_FORM_JWT_TOKEN);
} catch (SignatureException e) {
throw new CustomException(SIGNATURE_EXCEPTION);
}
}
// refreshToken 토큰 검증
public Boolean refreshTokenValidation(String token) {
// 1차 토큰 검증
if(!validateToken(token)) return false;
// DB에 저장한 토큰 비교
Optional<RefreshToken> refreshToken = Optional.ofNullable(refreshTokenRepository.findByEmail(getUserInfoFromToken(token)));
return refreshToken.isPresent() && token.equals(refreshToken.get().getRefreshToken());
}
// 토큰에서 loginId 가져오는 기능
public String getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject();
}
// 인증 객체 생성
public Authentication createAuthentication(String email) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
JwtUtil = 토큰에 관련된 암호화, 복호화, 검증 로직이 이루어지는 곳
1. init()
- @PostConstruct 어노테이션은 특정 클래스의 메소드에 붙여서 해당 클래스의 객체 내 모든 의존성(Bean) 들이 초기화 된 직후에 딱 한 번만 실행되도록 해준다. 만약 객체에 의존성이 하나도 없더라도 실행된다.
- WAS가 뜰 때 서비스가 생성되며 초기화 메소드가 실행된다.
- 깃헙엔는 올라가지 않는 application-local.properties 파일에에 저장되어 있는 Jwt Secret Key를 복호화(디코딩 = 암호화의 반댓말)한다.
2. getHeaderToken()
- 헤더부분에 타입에 따라 Access Token 과 Refresh Token을 가져오는 메소드
- JwtAuthFilter 클래스에 사용된다.
3. createToken() / createAllTolen()
- 유저 정보(email)를 넘겨받아서 Access Token 과 Refresh Token 을 생성하는 메소드
- 타입에따라 각각 유효 시간을 저장해주고, Token Dto에 담아준다.
- KakaoService, MemberService 에서 로그인한 후 response Header에 JWT 토큰 추가할 때 사용된다.
4. validateToken/refreshTokenValidation
- 토큰 정보를 검증한다.
- JwtAuthFilter의 doFilterInternal()에서 토큰 만료 여부를 확인할 때 사용된다.
5. getUserInfoFromToken()
- 토큰에서 유저정보(email) 가져오는 메소드
- JwtAuthFilter의 doFilterInternal()에서 토큰이 유효한 경우 토큰정보를 setAuthentication()메소드를 이용하여 Authentication에 저장해 줄 때 사용
6. createAuthentication()
- 인증 객체를 생성하는 메소드
- JwtAuthFilter의 setAuthentication() 에서 객체를 Authentication에 저장할 때 사용
2. JwtAuthFilter
@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
public final JwtUtil jwtUtil;
@Override //토큰 유효성 검사 후 Authentication 에 세팅
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String accessToken = jwtUtil.getHeaderToken(request, "Access");
String refreshToken = jwtUtil.getHeaderToken(request, "Refresh");
if (accessToken != null) {
if (!jwtUtil.validateToken(accessToken)) {
jwtExceptionHandler(response, "AccessToken Expired", HttpStatus.UNAUTHORIZED);
return;
}
setAuthentication(jwtUtil.getUserInfoFromToken(accessToken));
} else if (refreshToken != null) {
if (!jwtUtil.refreshTokenValidation(refreshToken)) {
jwtExceptionHandler(response, "RefreshToken Expired", HttpStatus.BAD_REQUEST);
return;
}
// 3. 토큰이 유효하다면 토큰에서 정보를 가져와 Authentication 에 세팅
setAuthentication(jwtUtil.getUserInfoFromToken(refreshToken));
}
// 4. 다음 필터로 넘어간다
filterChain.doFilter(request, response);
}
public void setAuthentication(String email) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = jwtUtil.createAuthentication(email);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
public void jwtExceptionHandler(HttpServletResponse response, String message, HttpStatus statusCode) {
response.setStatus(statusCode.value());
response.setContentType("application/json; charset=UTF-8");
try {
String json = new ObjectMapper().writeValueAsString(new GlobalResponseDto<>(StatusCode.BAD_REQUEST_TOKEN));
response.getWriter().write(json);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
1. doFilterInternal()
- 실제 필터링 로직을 수행하는 곳이다.
- Request Header 에서 Access Token 과 Refresh Token을 꺼내온 후 토큰 유효 여부를 판단한다.
- 토큰이 유효하면 Authentication에 넣어준다.
- 가입/로그인/재발급을 제외한 모든 Request 요청은 이 필터를 거치기 때문에 토큰 정보가 없거나 유효하지 않으면 정상적으로 수행되지 않는다.
2. setAuthentication()
- 토큰이 유효한 경우 토큰정보를 Authentication에 저장해 준다.
3. jwtExceptionHandler()
- 토큰이 유효하지 않을 경우 핸들러를 통해 에러처리를 해준다.
3. Controller
@RestController
@RequiredArgsConstructor
@RequestMapping(produces = "application/json; charset=utf8")
public class MemberController {
private final MemberService memberService;
private final KakaoService kakaoService;
// 로그인
@PostMapping(value = "/auth/login")
public ResponseEntity<?> login(@RequestBody SignupRequestDto signupRequestDto,
HttpServletResponse response) {
return ResponseUtil.response(memberService.login(signupRequestDto, response));
}
// 로그아웃
@PostMapping("/auth/signout")
public ResponseEntity<?> signout(@AuthenticationPrincipal UserDetailsImpl userDetails){
return memberService.signout(userDetails.getMember().getEmail());
}
// 토큰 재발행
@GetMapping("/auth/issue/token")
public ResponseDto<String> issuedToken(@AuthenticationPrincipal UserDetailsImpl userDetails, HttpServletResponse response){
return memberService.issuedToken(userDetails.getMember().getEmail(), response);
}
}
4. Service
@RequiredArgsConstructor
@Service
@Slf4j
public class MemberService {
private final PasswordEncoder passwordEncoder;
private final RepositoryService repositoryService;
private final JwtUtil jwtUtil;
private final RefreshTokenRepository refreshTokenRepository;
// 로그인
@Transactional
public MemberResponseDto login(SignupRequestDto signupRequestDto, HttpServletResponse response) {
String email = signupRequestDto.getEmail();
String password = signupRequestDto.getPassword();
Member member = repositoryService.findMemberByEmail(email).orElseThrow(
() -> new CustomException(StatusCode.LOGIN_MATCH_FAIL)
);
if (!passwordEncoder.matches(password, member.getPassword())) {
throw new CustomException(StatusCode.BAD_PASSWORD);
}
// user email 값을 포함한 토큰 생성 후 tokenDto 에 저장
TokenDto tokenDto = jwtUtil.createAllToken(signupRequestDto.getEmail());
// user email 값에 해당하는 refreshToken 을 DB에서 가져옴
Optional<RefreshToken> refreshToken = Optional.ofNullable(refreshTokenRepository.findByEmail(member.getEmail()));
if (refreshToken.isPresent()) {
refreshTokenRepository.saveRefreshToken(refreshToken.get().updateToken(tokenDto.getRefreshToken()));
} else {
RefreshToken newToken = new RefreshToken(tokenDto.getRefreshToken(), signupRequestDto.getEmail());
refreshTokenRepository.saveRefreshToken(newToken);
}
setHeader(response, tokenDto);
return new MemberResponseDto(member);
}
// 토큰 재발행
public ResponseDto<String> issuedToken(String email, HttpServletResponse response){
response.addHeader(JwtUtil.ACCESS_TOKEN, jwtUtil.createToken(email, "Access"));
return ResponseDto.success("토큰재발행 성공");
}
// 로그인하면 Header에 토큰 담아주기
private void setHeader(HttpServletResponse response, TokenDto tokenDto) {
response.addHeader(JwtUtil.ACCESS_TOKEN, tokenDto.getAccessToken());
response.addHeader(JwtUtil.REFRESH_TOKEN, tokenDto.getRefreshToken());
}
// 로그아웃
public ResponseEntity<?> signout(String email) {
// 해당 유저의 refreshtoken 이 없을 경우
if(refreshTokenRepository.findByEmail(email) == null){
throw new CustomException(StatusCode.INVALID_TOKEN);
}
// 자신의 refreshtoken 만 삭제 가능
String memberIdrepo = refreshTokenRepository.findByEmail(email).getEmail();
if(email.equals(memberIdrepo)){
refreshTokenRepository.deleteRefreshToken(email);
return ResponseUtil.response(StatusCode.SIGNOUT_OK);
}else{
return ResponseUtil.response(StatusCode.BAD_REFRESH_TOKEN);
}
}
}
※ 필요한 부분만 포스팅 했기 때문에 전체 코드는 아래 깃헙 링크를 참조해 주세요 ! ( 아직 수정중.... )
https://github.com/namoldak/Backend
'항해99 개발 일지 > [Final] 실전 프로젝트' 카테고리의 다른 글
[26] Refresh Token with Redis final! 최종 버전 :) (0) | 2023.02.04 |
---|---|
[24] Refresh Token 코드 리뷰 (2) (0) | 2023.01.28 |
[22] ubuntu 서버에 Redis 설치하기 (0) | 2023.01.26 |
[20] Spring CI/CD (2) CI 실전편 (0) | 2023.01.25 |
[19] OAuth 2.0 개념 정리 (0) | 2023.01.24 |