Skip to content

Commit

Permalink
feat: JWT 인증 필터 추가 및 인증 처리/토큰 재발급 로직 설정 (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyeong-hyeok committed Aug 2, 2023
1 parent 2c1fdb3 commit f76e04b
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.project.mapdagu.jwt.filter;

import com.project.mapdagu.domain.member.entity.Member;
import com.project.mapdagu.domain.member.repository.MemberRepository;
import com.project.mapdagu.error.ErrorCode;
import com.project.mapdagu.error.exception.custom.TokenException;
import com.project.mapdagu.jwt.service.JwtService;
import com.project.mapdagu.jwt.util.PasswordUtil;
import com.project.mapdagu.util.RedisUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

/**
* Jwt 인증 필터
* "/login" 이외의 URI 요청이 왔을 때 처리하는 필터
*/
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {

private static final String NO_CHECK_URL = "/login"; // "/login"으로 들어오는 요청은 Filter 작동 X

private final JwtService jwtService;
private final MemberRepository memberRepository;
private final RedisUtil redisUtil;

private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
if (request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response); // "/login" 요청이 들어오면, 다음 필터 호출
return;
}

// 사용자 요청 헤더에서 RefreshToken 추출-> RefreshToken이 없거나 유효하지 않다면 null
String refreshToken = jwtService.extractRefreshToken(request)
.filter(jwtService::isTokenValid)
.orElse(null);
String email = jwtService.extractEmail(refreshToken).orElseThrow(() -> new TokenException(ErrorCode.INVALID_TOKEN));

// 리프레시 토큰이 요청 헤더에 존재하고 유효하다면, AccessToken이 만료된 것 -> AccessToken 재발급
if (refreshToken != null && isRefreshTokenMatch(email, refreshToken)) {
String newAccessToken = jwtService.createAccessToken(email);
String newRefreshToken = jwtService.createRefreshToken(email);
jwtService.updateRefreshToken(email, newRefreshToken);
jwtService.sendAccessAndRefreshToken(response, newAccessToken, refreshToken);
return;
}

// AccessToken을 검사하고 인증 처리
// AccessToken이 없거나 유효하지 않다면, 인증 객체가 담기지 않은 상태로 다음 필터로 넘어가기 때문에 403 에러 발생
// AccessToken이 유효하다면, 인증 객체가 담긴 상태로 다음 필터로 넘어가기 때문에 인증 성공
else {
checkAccessTokenAndAuthentication(request, response, filterChain);
}
}

public boolean isRefreshTokenMatch(String email, String refreshToken) {
if (redisUtil.get(email).equals(refreshToken)) {
return true;
}
throw new TokenException(ErrorCode.INVALID_TOKEN);
}

/**
* [액세스 토큰 체크 & 인증 처리 메소드]
* 인증 허가 처리된 객체를 SecurityContextHolder에 담기
*/
public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
log.info("checkAccessTokenAndAuthentication() 호출");
jwtService.extractAccessToken(request)
.filter(jwtService::isTokenValid)
.ifPresent(accessToken -> jwtService.extractEmail(accessToken)
.ifPresent(email -> memberRepository.findByEmail(email)
.ifPresent(this::saveAuthentication)));

filterChain.doFilter(request, response);
}

/**
* [인증 허가 메소드]
* 파라미터의 유저 : 우리가 만든 회원 객체 / 빌더의 유저 : UserDetails의 User 객체
*/
public void saveAuthentication(Member member) {
String password = member.getPassword();
if (password == null) { // 소셜 로그인 유저의 비밀번호 임의로 설정 하여 소셜 로그인 유저도 인증 되도록 설정
password = PasswordUtil.generateRandomPassword();
}

UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder()
.username(member.getEmail())
.password(password)
.roles(member.getRole().name())
.build();

Authentication authentication =
new UsernamePasswordAuthenticationToken(userDetailsUser, null,
authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities()));

SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
24 changes: 17 additions & 7 deletions src/main/java/com/project/mapdagu/jwt/service/JwtService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.project.mapdagu.domain.member.repository.MemberRepository;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Getter;
Expand All @@ -25,10 +26,10 @@ public class JwtService {
private String secretKey;

@Value("${jwt.access.expiration}")
private Long accessTokenExpirationPeriod;
private Integer accessTokenExpirationPeriod;

@Value("${jwt.refresh.expiration}")
private Long refreshTokenExpirationPeriod;
private Integer refreshTokenExpirationPeriod;

@Value("${jwt.access.header}")
private String accessHeader;
Expand All @@ -52,19 +53,19 @@ public String createAccessToken(String email) {
return JWT.create()
.withSubject(ACCESS_TOKEN_SUBJECT)
.withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod)) // 토큰 만료 시간 설정
// claim -> email 사용
.withClaim(EMAIL_CLAIM, email)
.sign(Algorithm.HMAC512(secretKey));
}

/**
* RefreshToken 생성 (claim에 email 필요 x)
* RefreshToken 생성
*/
public String createRefreshToken() {
public String createRefreshToken(String email) {
Date now = new Date();
return JWT.create()
.withSubject(REFRESH_TOKEN_SUBJECT)
.withExpiresAt(new Date(now.getTime() + refreshTokenExpirationPeriod))
.withClaim(EMAIL_CLAIM, email)
.sign(Algorithm.HMAC512(secretKey));
}

Expand All @@ -77,6 +78,15 @@ public void sendAccessToken(HttpServletResponse response, String accessToken) {
log.info("재발급된 Access Token : {}", accessToken);
}

public void sendRefreshToken(HttpServletResponse response, String refreshToken) {
Cookie cookie = new Cookie(REFRESH_TOKEN_SUBJECT, refreshToken);
cookie.setMaxAge(refreshTokenExpirationPeriod);
cookie.setSecure(true);
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);
}

/**
* 로그인 시 AccessToken + RefreshToken 헤더에 실어서 보내기
*/
Expand Down Expand Up @@ -109,11 +119,11 @@ public Optional<String> extractAccessToken(HttpServletRequest request) {
* AccessToken에서 Email 추출
* AceessToken 검증 후 이메일 추출 (유효하지 않다면 빈 Optional 객체 반환)
*/
public Optional<String> extractEmail(String accessToken) {
public Optional<String> extractEmail(String token) {
try {
return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey))
.build()
.verify(accessToken)
.verify(token)
.getClaim(EMAIL_CLAIM)
.asString());
} catch (Exception e) {
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/project/mapdagu/jwt/util/PasswordUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.project.mapdagu.jwt.util;

import java.util.Random;

public class PasswordUtil {
public static String generateRandomPassword() {
int index = 0;
char[] charSet = new char[] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
}; //배열안의 문자 숫자는 원하는대로

StringBuffer password = new StringBuffer();
Random random = new Random();

for (int i = 0; i < 8 ; i++) {
double rd = random.nextDouble();
index = (int) (charSet.length * rd);

password.append(charSet[index]);
}
System.out.println(password);
return password.toString();
//StringBuffer를 String으로 변환해서 return 하려면 toString()을 사용하면 된다.
}
}

0 comments on commit f76e04b

Please sign in to comment.