개요
이번에는 JWT 인증 인가 부분을 구현할 예정이다.
1. JWT 생성 / 검증 과정에 해당하는 유틸리티 클래스
/**
* JWT 생성 / 검증 과정에 해당하는 유틸리티 클래스
*/
@Component
@Slf4j
@Getter
public class JwtTokenizer {
private final byte[] accessSecret;
private final byte[] refreshSecret;
public static Long ACCESS_TOKEN_EXPIRE_COUNT = 30 * 60 * 1000L; // 30분
public static Long REFRESH_TOKEN_EXPIRE_COUNT = 7 * 24 * 60 * 60 * 1000L; // 7일
// .yml 파일에 secretKey, refreshKey 값이 있어야 한다.
public JwtTokenizer(@Value("${jwt.secretKey}") String accessSecret,
@Value("${jwt.refreshKey}") String refreshSecret){
this.accessSecret = accessSecret.getBytes(StandardCharsets.UTF_8);
this.refreshSecret = refreshSecret.getBytes(StandardCharsets.UTF_8);
}
/**
* AccessToken 생성
*/
public String createAccessToken(Long id, String username, String name, List<String> roles) {
return createToken(id, username, name, roles, ACCESS_TOKEN_EXPIRE_COUNT, accessSecret);
}
/**
* RefreshToken 생성
*/
public String createRefreshToken(Long id, String username, String name, List<String> roles) {
return createToken(id, username, name, roles, REFRESH_TOKEN_EXPIRE_COUNT, refreshSecret);
}
/**
* Jwts 빌더를 사용하여 token 생성
*/
private String createToken(Long id, String username, String name, List<String> roles, long expireCount, byte[] secret) {
Claims claims = Jwts.claims().setSubject(username); // 기본으로 가지고 있는 claim : subject
claims.put("userId", id);
claims.put("name", name);
claims.put("roles", roles);
Date now = new Date();
Date expiration = new Date(now.getTime() + expireCount);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS256, getSigningKey(secret))
.compact();
}
//----------------------------------------------------------------------------------추가-------------------------------//
/**
* JWT 토큰에서 사용자 ID 추출
* @param token JWT 토큰
* @return 사용자 ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token, accessSecret);
return claims.get("userId", Long.class);
}
/**
* JWT 토큰에서 사용자 이름 추출
* @param token JWT 토큰
* @return 사용자 이름
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token, accessSecret);
return claims.getSubject();
}
/**
* JWT 토큰에서 역할 정보 추출
* @param token JWT 토큰
* @return 역할 목록
*/
public List<String> getRolesFromToken(String token) {
Claims claims = parseToken(token, accessSecret);
return claims.get("roles", List.class);
}
//---------------------------------------------------------------------------------------------------------------------//
/**
* access token 파싱
* @param accessToken access token
* @return 파싱된 토큰
*/
public Claims parseAccessToken(String accessToken) {
return parseToken(accessToken, accessSecret);
}
/**
* refresh token 파싱
* @param refreshToken refresh token
* @return 파싱된 토큰
*/
public Claims parseRefreshToken(String refreshToken) {
return parseToken(refreshToken, refreshSecret);
}
/**
* token 파싱
* @param token access/refresh token
* @param secretKey access/refresh 비밀키
* @return 파싱된 토큰
*/
public Claims parseToken(String token, byte[] secretKey) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey(secretKey))
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* @param secretKey - byte형식
* @return Key 형식 시크릿 키
*/
public static Key getSigningKey(byte[] secretKey) {
return Keys.hmacShaKeyFor(secretKey);
}
/**
* 토큰이 만료되었는지 확인하는 메서드
*/
public boolean isTokenExpired(String token, byte[] secretKey) {
try {
Jwts.parserBuilder().setSigningKey(getSigningKey(secretKey)).build().parseClaimsJws(token).getBody();
return false;
} catch (io.jsonwebtoken.ExpiredJwtException e) {
return true;
}
}
/**
* 액세스 토큰이 만료되었는지 확인
*/
public boolean isAccessTokenExpired(String accessToken) {
return isTokenExpired(accessToken, accessSecret);
}
/**
* 리프레시 토큰이 만료되었는지 확인
*/
public boolean isRefreshTokenExpired(String refreshToken) {
return isTokenExpired(refreshToken, refreshSecret);
}
/**
* 리프레시 토큰으로 새로운 액세스 토큰을 발급
*/
public String refreshAccessToken(String refreshToken) {
Claims claims = parseRefreshToken(refreshToken);
Long userId = claims.get("userId", Long.class);
String username = claims.getSubject(); // 토큰의 주체
String name = claims.get("name", String.class);
List<String> roles = (List<String>) claims.get("roles");
return createAccessToken(userId, username, name, roles);
}
}
로그인할 때 필요한 username으로 sub 만들었고, userId, name, roles을 페이로드에 추가하였다.
2. JWT 인증 토큰 클래스
/**
* JWT 인증 토큰 클래스
*/
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private String token; // JWT 토큰
private Object principal; // 주체 (사용자)
private Object credentials; // 인증 정보 (자격 증명)
/**
* 인증된 토큰을 생성하는 생성자
* @param authorities 권한
* @param principal 주체 (사용자)
* @param credentials 인증 정보 (자격 증명)
*/
public JwtAuthenticationToken(Collection<? extends GrantedAuthority> authorities , Object principal, Object credentials) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(true); // 토큰을 인증된 상태로 설정
}
/**
* 인증되지 않은 토큰을 생성하는 생성자
* @param token JWT 토큰
*/
public JwtAuthenticationToken(String token){
super(null);
this.token = token;
this.setAuthenticated(false); // 토큰을 인증되지 않은 상태로 설정
}
/**
* 자격 증명을 반환
* @return 자격 증명
*/
@Override
public Object getCredentials() {
return this.credentials;
}
/**
* 주체 (사용자)를 반환
* @return 주체 (사용자)
*/
@Override
public Object getPrincipal() {
return this.principal;
}
}
3. 오류 코드
@Getter
public enum JwtExceptionCode {
UNKNOWN_ERROR("UNKNOWN_ERROR", "UNKNOWN_ERROR"),
NOT_FOUND_TOKEN("NOT_FOUND_TOKEN", "Cookie에 토큰 형식의 값 찾을 수 없음"),
INVALID_TOKEN("INVALID_TOKEN", "유효하지 않은 토큰"),
EXPIRED_TOKEN("EXPIRED_TOKEN", "기간이 만료된 토큰"),
UNSUPPORTED_TOKEN("UNSUPPORTED_TOKEN", "지원하지 않는 토큰"),
BLACKLISTED_TOKEN("BLACKLISTED_TOKEN", "로그아웃하여 블랙리스트에 올라간 토큰");
private String code;
private String message;
JwtExceptionCode(String code, String message) {
this.code = code;
this.message = message;
}
}
/**
* 시큐리티가 인증되지 않은 사용자가 보호된 리소스에 접근할 때 동작하는 클래스
*/
@Component
@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String exception = (String)request.getAttribute("exception");
if(isRestRequest(request)){
handleRestResponse(request, response, exception);
} else {
handlePageResponse(request, response, exception);
}
}
private void handlePageResponse(HttpServletRequest request, HttpServletResponse response, String exception) throws IOException {
if (exception != null) {
switch (exception) {
case "INVALID_TOKEN":
response.sendRedirect("/");
break;
case "EXPIRED_TOKEN":
response.sendRedirect("/");
break;
case "UNSUPPORTED_TOKEN":
response.sendRedirect("/");
break;
case "NOT_FOUND_TOKEN":
response.sendRedirect("/");
break;
default:
response.sendRedirect("/");
break;
}
return;
}
response.sendRedirect("/");
}
private boolean isRestRequest(HttpServletRequest request) {
String requestedWithHeader = request.getHeader("X-Requested-With");
return "XMLHttpRequest".equals(requestedWithHeader) || request.getRequestURI().startsWith("/api/");
}
private void handleRestResponse(HttpServletRequest request, HttpServletResponse response, String exception) throws IOException {
if (exception != null) {
log.error("Rest Request - Commence Get Exception : {}", exception);
if (exception.equals(JwtExceptionCode.INVALID_TOKEN.getCode())) {
log.error("entry point >> invalid token");
setResponse(response, JwtExceptionCode.INVALID_TOKEN);
} else if (exception.equals(JwtExceptionCode.EXPIRED_TOKEN.getCode())) {
log.error("entry point >> expired token");
setResponse(response, JwtExceptionCode.EXPIRED_TOKEN);
} else if (exception.equals(JwtExceptionCode.UNSUPPORTED_TOKEN.getCode())) {
log.error("entry point >> unsupported token");
setResponse(response, JwtExceptionCode.UNSUPPORTED_TOKEN);
} else if (exception.equals(JwtExceptionCode.NOT_FOUND_TOKEN.getCode())) {
log.error("entry point >> not found token");
setResponse(response, JwtExceptionCode.NOT_FOUND_TOKEN);
} else {
setResponse(response, JwtExceptionCode.UNKNOWN_ERROR);
}
} else {
setResponse(response, JwtExceptionCode.UNKNOWN_ERROR);
}
response.sendRedirect("/");
}
private void setResponse(HttpServletResponse response, JwtExceptionCode exceptionCode) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
HashMap<String, Object> errorInfo = new HashMap<>();
errorInfo.put("message", exceptionCode.getMessage());
errorInfo.put("code", exceptionCode.getCode());
Gson gson = new Gson();
String responseJson = gson.toJson(errorInfo);
response.getWriter().print(responseJson);
}
}
인증되지 않은 접근일 경우 CustomAuthenticationEntryPoint 를 작성하여 오류에 대해서 다뤘다.
4. 필터
/**
* 요청이 들어올 때마다 JWT 토큰을 검증하는 필터
* 토큰을 검증하고 유효한 사용자라면 그 사용자의 정보를 SecurityContextHolder 에 설정
*/
@Slf4j
public class JWTFilter extends OncePerRequestFilter {
private final JwtTokenizer jwtTokenizer;
private final JwtBlacklistService jwtBlacklistService;
private final RefreshTokenService refreshTokenService;
public JWTFilter(JwtTokenizer jwtTokenizer, JwtBlacklistService jwtBlacklistService, RefreshTokenService refreshTokenService) {
this.jwtTokenizer = jwtTokenizer;
this.jwtBlacklistService = jwtBlacklistService;
this.refreshTokenService = refreshTokenService;
}
/**
* 접근 허용 앤드 포인트 목록
*/
private static final List<String> PERMIT_ALL_PATHS = List.of(
"/signup", "/signin", "/", "/api/users/login", "/api/users/signup",
"/css/.*", "/js/.*", "/files/.*"
);
/**
* 필터 메서드
* 각 요청마다 JWT 토큰을 검증하고 인증을 설정
* @param request 요청 객체
* @param response 응답 객체
* @param filterChain 필터 체인
* @throws ServletException 서블릿 예외
* @throws IOException 입출력 예외
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestPath = request.getRequestURI(); // 요청 경로 확인
if (isPermitAllPath(requestPath)) {
filterChain.doFilter(request, response);
return;
}
String token = getToken(request); // 요청에서 토큰을 추출
if (!StringUtils.hasText(token)) { // 토큰을 사용하여 인증 설정
handleMissingToken(request, response);
} else {
handleTokenValidation(request, response, token);
}
filterChain.doFilter(request, response); // 다음 필터로 요청을 전달
// log.info("토큰 제대로?? : {}", token);
}
/**
* 요청 경로가 인증 없이 접근 가능한지 확인
*/
private boolean isPermitAllPath(String requestPath) {
return PERMIT_ALL_PATHS.stream().anyMatch(requestPath::matches);
}
/**
* Access Token이 없는 경우 처리하는 메서드
*/
private void handleMissingToken(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 쿠키에서 Refresh Token을 얻어옴
String refreshToken = getRefreshToken(request);
if (StringUtils.hasText(refreshToken)) {
try {
// Refresh Token이 DB에 존재하는지 확인
if (refreshTokenService.isRefreshTokenValid(refreshToken)) {
if (!jwtTokenizer.isRefreshTokenExpired(refreshToken)) {
String newAccessToken = jwtTokenizer.refreshAccessToken(refreshToken);
setAccessTokenCookie(response, newAccessToken);
getAuthentication(newAccessToken);
} else {
// Refresh Token이 만료된 경우
handleException(request, JwtExceptionCode.EXPIRED_TOKEN, "Refresh token expired");
}
} else {
// Refresh Token이 DB에 없는 경우
handleException(request, JwtExceptionCode.NOT_FOUND_TOKEN, "Refresh token not found in database");
}
} catch (ExpiredJwtException e) {
// Refresh Token이 만료된 경우
handleException(request, JwtExceptionCode.EXPIRED_TOKEN, "Expired refresh token", e);
}
} else {
handleException(request, JwtExceptionCode.NOT_FOUND_TOKEN, "Token not found in request");
}
}
/**
* Access Token이 있는 경우 처리하는 메서드
*/
private void handleTokenValidation(HttpServletRequest request, HttpServletResponse response, String token) throws ServletException, IOException {
try {
// 토큰이 블랙리스트에 있는지 확인
if (jwtBlacklistService.isTokenBlacklisted(token)) {
handleException(request, JwtExceptionCode.BLACKLISTED_TOKEN, "Token is blacklisted: " + token);
} else {
getAuthentication(token);
}
} catch (ExpiredJwtException e) {
handleExpiredAccessToken(request, response, token, e);
} catch (UnsupportedJwtException e) {
handleException(request, JwtExceptionCode.UNSUPPORTED_TOKEN, "Unsupported token: " + token, e);
} catch (MalformedJwtException e) {
handleException(request, JwtExceptionCode.INVALID_TOKEN, "Invalid token: " + token, e);
} catch (IllegalArgumentException e) {
handleException(request, JwtExceptionCode.NOT_FOUND_TOKEN, "Token not found: " + token, e);
} catch (Exception e) {
handleException(request, JwtExceptionCode.UNKNOWN_ERROR, "JWT filter internal error: " + token, e);
}
}
/**
* Access Token이 만료된 경우 처리하는 메서드
*/
private void handleExpiredAccessToken(HttpServletRequest request, HttpServletResponse response, String token, ExpiredJwtException e) throws ServletException, IOException {
log.warn("Access token expired: {}", token);
String refreshToken = getRefreshToken(request);
if (StringUtils.hasText(refreshToken) && !jwtTokenizer.isRefreshTokenExpired(refreshToken)) {
// Refresh Token이 유효한 경우 새로운 Access Token 발급
String newAccessToken = jwtTokenizer.refreshAccessToken(refreshToken);
setAccessTokenCookie(response, newAccessToken);
getAuthentication(newAccessToken);
} else {
handleException(request, JwtExceptionCode.EXPIRED_TOKEN, "Expired Token : " + token, e);
}
}
/**
* 요청에서 토큰 추출
*/
private String getToken(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) {
return authorization.substring(7);
}
Cookie[] cookies = request.getCookies(); // 쿠키에서 토큰을 찾음
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("accessToken".equals(cookie.getName())) {
return cookie.getValue(); // accessToken 쿠키에서 토큰 반환
}
}
}
return null; // 토큰을 찾지 못한 경우 null 반환
}
/**
* 쿠키에서 Refresh Token을 추출하는 메서드
*/
private String getRefreshToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("refreshToken".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
/**
* 새로운 Access Token을 쿠키에 설정하는 메서드
*/
private void setAccessTokenCookie(HttpServletResponse response, String newAccessToken) {
Cookie accessTokenCookie = new Cookie("accessToken", newAccessToken);
accessTokenCookie.setHttpOnly(true); // XSS 보호를 위해 HttpOnly 설정
accessTokenCookie.setPath("/");
accessTokenCookie.setMaxAge(Math.toIntExact(jwtTokenizer.ACCESS_TOKEN_EXPIRE_COUNT / 1000));
response.addCookie(accessTokenCookie);
}
/**
* 토큰을 사용하여 인증 설정
*/
private void getAuthentication(String token) {
Claims claims = jwtTokenizer.parseAccessToken(token); // 토큰에서 클레임을 파싱
String username = claims.getSubject(); // 이메일을 가져옴
Long userId = claims.get("userId", Long.class); // 사용자 ID를 가져옴
String name = claims.get("name", String.class); // 이름을 가져옴
List<GrantedAuthority> authorities = getGrantedAuthorities(claims); // 사용자 권한을 가져옴
CustomUserDetails userDetails = new CustomUserDetails(username, "", authorities);
Authentication authentication = new JwtAuthenticationToken(authorities, userDetails, null); // 인증 객체 생성
SecurityContextHolder.getContext().setAuthentication(authentication); // SecurityContextHolder에 인증 객체 설정
}
/**
* JWT Claims에서 권한 정보를 추출하는 메서드
*/
private List<GrantedAuthority> getGrantedAuthorities(Claims claims) {
List<String> roles = claims.get("roles", List.class);
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
/**
* 예외 처리 메서드
*/
private void handleException(HttpServletRequest request, JwtExceptionCode exceptionCode, String logMessage) {
handleException(request, exceptionCode, logMessage, null);
}
private void handleException(HttpServletRequest request, JwtExceptionCode exceptionCode, String logMessage, Exception e) {
request.setAttribute("exception", exceptionCode.getCode());
log.error(logMessage, e);
// throw new BadCredentialsException(logMessage, e);
log.error("로그인을 부탁드립니다.");
}
}
로그인이 들어올 때 기존 시큐리티 로그인 처리 필터인 UsernamePasswordAuthenticationFilter 이전에 실행되어서 토큰을 발급한 후 로그인 처리를 진행한다.
5. SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final JwtBlacklistService jwtBlacklistService;
private final RefreshTokenService refreshTokenService;
private final JwtTokenizer jwtTokenizer;
private final CustomOauth2UserService customOAuth2UserService;
private final CustomSuccessHandler customSuccessHandler;
// 모든 유저 허용 페이지
String[] allAllowPage = new String[] {
"/", // 메인페이지
"/signup", // 회원가입 페이지
"/signin", // 로그인 페이지
"/css/**", "/js/**", "/files/**", // css, js, 이미지 url
"/api/users/login", // 로그인 페이지
"/api/users/signup" // 회원가입 페이지
};
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.requestMatchers(allAllowPage).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JWTFilter(jwtTokenizer, jwtBlacklistService, refreshTokenService), UsernamePasswordAuthenticationFilter.class) // JWT 필터 사용
.formLogin(form -> form.disable()) // 로그인 폼 비활성화
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 관리 Stateless 설정(서버가 클라이언트 상태 저장x)
.csrf(csrf -> csrf.disable()) // cors 허용
.httpBasic(httpBasic -> httpBasic.disable()) // http 기본 인증(헤더) 비활성화
.cors(cors -> cors.configurationSource(configurationSource()))
.exceptionHandling(exception -> exception
.authenticationEntryPoint(customAuthenticationEntryPoint)
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/signin")
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
.failureUrl("/loginFailure")
.authorizationEndpoint(authorization -> authorization.baseUri("/oauth2/authorization"))
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource configurationSource(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*"); // 모든 도메인 허용
config.addAllowedOrigin("http://localhost:3000"); // 프론트의 주소
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowedMethods(List.of("GET", "POST", "DELETE"));
config.setExposedHeaders(Arrays.asList("Set-Cookie", "Authorization"));
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return source;
}
}
위 코드에서도 보듯이 UsernamePasswordAuthenticationFilter 이전에 우리가 커스텀한 jwt 필터를 추가해줬다. 그리고 exceptionhandling에 대해서도 위에서 정의한 cusstomAuthenticationEntryPoint 를 지정했다.
6. UserDetails
/**
* 사용자 인증 및 권한 관리를 위한 사용자 세부 정보 클래스
*/
public class CustomUserDetails implements UserDetails {
private final String username; // 사용자 아이디
private final String password; // 비밀번호
private final Collection<? extends GrantedAuthority> authorities; // 권환
// DB 에서 사용자 인증/인가
public CustomUserDetails(String username, String password, Set<Role> roles) {
this.username = username;
this.password = password;
this.authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
// jwt 인증 생성자
public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Spring Security는 이 클래스를 사용하여 사용자 인증 및 권한 부여를 처리한다. 그래서 jwt 로그인 진행 시 인증 설정을 하고 securityContextHolder에 저장한다.
7. APIController
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/api/users")
public class UserApiController {
private final JwtTokenizer jwtTokenizer;
private final UserService userService;
private final PasswordEncoder passwordEncoder;
private final RefreshTokenService refreshTokenService;
private final JwtBlacklistService jwtBlackListService;
/**
* 로그인 요청 시 jwt 토큰 발급
*/
@PostMapping("/login")
public ResponseEntity login(@RequestBody @Valid UserLoginDto userLoginDto,
BindingResult bindingResult,
HttpServletResponse response){
log.info("로그인 요청이 들어왔습니다.");
log.info("아이디 :: {}", userLoginDto.getUsername());
log.info("비밀번호 :: {}", userLoginDto.getPassword());
if(bindingResult.hasErrors()){ // 필드 에러 확인
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
Optional<User> user = userService.findByUsername(userLoginDto.getUsername());
// 비밀번호 일치여부 체크
if(!passwordEncoder.matches(userLoginDto.getPassword(), user.get().getPassword())) {
return new ResponseEntity("비밀번호가 올바르지 않습니다.",HttpStatus.UNAUTHORIZED);
}
List<String> roles = user.get().getRoles().stream().map(Role::getName).collect(Collectors.toList());
refreshTokenService.deleteRefreshToken(String.valueOf(user.get().getId()));
// 토큰 발급
String accessToken = jwtTokenizer.createAccessToken(user.get().getId(), user.get().getUsername(), user.get().getName(), roles);
String refreshToken = jwtTokenizer.createRefreshToken(user.get().getId(), user.get().getUsername(), user.get().getName(), roles);
// 리프레시 토큰 디비 저장
Date date = new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE_COUNT);
RefreshToken refreshTokenEntity = new RefreshToken();
refreshTokenEntity.setValue(refreshToken);
refreshTokenEntity.setUserId(user.get().getId());
refreshTokenEntity.setExpiration(date.toString());
refreshTokenService.addRefreshToken(refreshTokenEntity);
// 토큰 쿠키 저장
Cookie accessTokenCookie = new Cookie("accessToken",accessToken);
accessTokenCookie.setHttpOnly(true);
accessTokenCookie.setPath("/");
accessTokenCookie.setMaxAge(Math.toIntExact(jwtTokenizer.ACCESS_TOKEN_EXPIRE_COUNT/1000));
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setPath("/");
refreshTokenCookie.setMaxAge(Math.toIntExact(REFRESH_TOKEN_EXPIRE_COUNT/1000));
response.addCookie(accessTokenCookie);
response.addCookie(refreshTokenCookie);
// 응답 값
UserLoginResponseDto loginResponseDto = UserLoginResponseDto.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.userId(user.get().getId())
.username(user.get().getUsername())
.build();
return new ResponseEntity(loginResponseDto, HttpStatus.OK);
}
/**
* 로그아웃 요청
*/
@GetMapping("/logout")
public void logout(@CookieValue(name = "accessToken", required = false) String accessToken,
@CookieValue(name = "refreshToken", required = false) String refreshToken,
HttpServletResponse response) {
log.info("로그아웃 요청이 들어왔습니다.");
if (accessToken == null) {
try {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Access token not found in cookies.");
} catch (IOException e) {
e.printStackTrace();
}
return;
}
String jwt = accessToken;
Date expirationTime = Jwts.parser()
.setSigningKey(jwtTokenizer.getAccessSecret())
.parseClaimsJws(jwt)
.getBody()
.getExpiration();
log.info("accessToken 만료시간 :: {}" , expirationTime);
JwtBlacklist blacklist = new JwtBlacklist(jwt, expirationTime);
jwtBlackListService.save(blacklist);
SecurityContextHolder.clearContext();
Cookie accessCookie = new Cookie("accessToken", null);
accessCookie.setPath("/");
accessCookie.setMaxAge(0);
response.addCookie(accessCookie);
Cookie refresCcookie = new Cookie("refreshToken", null);
refresCcookie.setPath("/");
refresCcookie.setMaxAge(0);
response.addCookie(refresCcookie);
// tokens 데이터 삭제
refreshTokenService.deleteRefreshToken(refreshToken);
try {
response.sendRedirect("/");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 회원가입
*/
@PostMapping("/signup")
public ResponseEntity<String> signup(@RequestBody UserSignUpDto userSignUpDto) {
try {
userService.signUp(userSignUpDto);
return ResponseEntity.status(HttpStatus.CREATED).body("회원가입 성공");
} catch (RuntimeException e) {
log.error("회원가입 실패 :: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("회원가입 실패 :: " + e.getMessage());
}
}
/**
* 회원 탈퇴
*/
@DeleteMapping("/{userId}")
public ResponseEntity<String> deleteUser(@PathVariable("userId") Long userId,
@CookieValue(name = "accessToken", required = false) String accessToken,
@CookieValue(name = "refreshToken", required = false) String refreshToken,
HttpServletResponse response,
Authentication authentication) {
String username = authentication.getName();
try {
userService.deleteUser(username);
// 로그아웃 로직
if (accessToken != null) {
// JWT 토큰 추출
String jwt = accessToken;
// 토큰의 만료 시간 추출
Date expirationTime = Jwts.parser()
.setSigningKey(jwtTokenizer.getAccessSecret())
.parseClaimsJws(jwt)
.getBody()
.getExpiration();
// 블랙리스트에 토큰 저장
JwtBlacklist blacklist = new JwtBlacklist(jwt, expirationTime);
jwtBlackListService.save(blacklist);
}
// SecurityContext를 클리어하여 현재 세션을 무효화
SecurityContextHolder.clearContext();
// accessToken 쿠키 삭제
Cookie accessCookie = new Cookie("accessToken", null);
accessCookie.setPath("/");
accessCookie.setMaxAge(0);
response.addCookie(accessCookie);
// refreshToken 쿠키 삭제
Cookie refreshCookie = new Cookie("refreshToken", null);
refreshCookie.setPath("/");
refreshCookie.setMaxAge(0);
response.addCookie(refreshCookie);
// 로그아웃 전 db에 저장되어있는 refreshToken 삭제
if (refreshToken != null) {
refreshTokenService.deleteRefreshToken(refreshToken);
}
return ResponseEntity.ok("회원 탈퇴 성공 !!");
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("회원 탈퇴 실패 !!");
}
}
}
로그인, 로그아웃, 회원탈퇴 api요청이다. 유저가 로그인 요청을 받으면 엑세스, 리프레시 토큰이 생성되고 쿠키에 저장하는 로직이다.
다음은 OAuth2 로그인에 대해서 작성하겠다.