https://dev-dx2d2y-log.tistory.com/213
[골든리포트!] 3) Spring Security Config로 스프링 OAuth 다루기
SecurityConfig 작성하기로그인 기능을 구현해야한다. 폼로그인은 사용하지 않을 것이고.. 네이버 OAuth 로그인만 사용할 것이다. OAuth 로그인을하려면 가장먼저 Spring Security 설정을 해주어야한다.packa
dev-dx2d2y-log.tistory.com
저번에 OAuth2에 대해서 다뤄보았다.
이번에는 인가방식에 대해서 다뤄보려한다. 인가는 세션과 JWT가 있는데, JWT방식을 써보려한다.
스프링 세션로그인
스프링에서 모든 OAuth 과정을 스프링에 일임하면 스프링에서 자동으로 세션에 로그인 정보를 저장한다.

저번 게시글에서 OAuth Success Handler에 대해서 다뤘는데, onAuthenticationSuccess 변수에 전해지는 Authentication 객체가 세션으로 저장된다. 그리고나서 인증이 필요할 때 세션을 뒤져보면 되는 것.
https://dev-dx2d2y-log.tistory.com/112
HttpSession을 통한 세션로그인 구현해보기
지금 해보고 싶은 프로젝트가 회원제로 운영되는데, 그러면 인증/인가를 개발해야한다. 저번에 홍대 맛집 아카이빙 프로젝트는 JWT토큰을 사용해 인증/인가를 개발했는데, 이번에는 세션을 통해
dev-dx2d2y-log.tistory.com
세션로그인에 대한 글
이 "세션생성"은 자동이기 때문에, 스프링에서 OAuth2 로그인을 끝내면 스프링이 자동으로 세션으로 Authentication 객체를 넣는다. 이게 디폴트 값이고, JWT토큰 인증 방식을 사용한다면 굳이 세션이 필요하지 않으므로 config 클래스에서 이를 처리할 수 있다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
이렇게.
스프링에서 세션에 로그인정보를 넣는 것은 SecurityContextRepository가 담당하는데, 만약 SessionCreationPolicy가 STATELESS라면 NullSecurityContextRepository가 세션에 로그인정보 넣는 것을 담당한다. 만약 이 클래스를 사용하면 정보만 받아서 세션에 저장하지 않고 가지고만 있는다.
SessionCreationPolicy는 NEVER 값도 가지고 있는데, STATELESS는 절대로 세션을 사용하지 않는다. 따라서 세션을 사용하는 모든 방식이 무시된다. 스프링이 세션을 생성하든, 다른 개발자나 내가 세션을 생성하든 모든 세션이 무시된다. 백엔드는 클라이언트의 상태(STATE)를 모른다.
하지만 모든 로직에서 세션을 사용하지 않을 수는 없다. 따라서 스프링에서 자동으로 인증정보를 세션으로 채워주는 기능만 꺼놓고 싶을 때가 있는데, 이 때 NEVER(을)를 사용한다. NEVER는 스프링에서 자동으로 세션에 값을 채우는 것을 막을 뿐, 개발자나 다른 개발자가 세션에 값을 추가하는 것은 자유롭게 사용할 수 있다.
공식문서에서는 NEVER를 사용해도 스프링이 채워넣은 세션을 사용할 때가 가끔 있다고하는데, 인증이 안된 상태에서 인증이 필요한 요청을 보냈을 때, 인증이 끝나고나서 다시 요청을 보내기위해 이전 HttpRequest를 저장하는 용도로 사용한다고한다. 물론 이 역시도 기능을 제어할 수 있다.
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
RequestCache를 사용하면 세션에 HttpRequest를 저장하고, NullRequestCache를 사용하면 저장하지 않는다. 만약 재요청을 보내는 과정에서 오류가 발생하면 RequestCacheAwareFilter가 동작해서 다시 요청을 보내는 식으로 동작한다.
만약 STATELESS 상태였다면 세션저장 자체가 불가능한 상황이기에 동작하지 않는다.
SessionCreationPolicy.NEVER & RequestCache = requestCache
암튼 이정도면 됐고, 따라서 이 공부에서는 좀 더 많은 것을 경험해보고자, SessionCreationPolicy는 NEVER, RequestCache는 requestCache를 사용해보고자한다.
package com.example.GoldenReport.Config;
import com.example.GoldenReport.Service.LogInAndSignUp.OAuth2SuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
@Configuration
@EnableWebSecurity
public class SecurityConfig{
OAuth2SuccessHandler oauth2SuccessHandler;
public SecurityConfig(OAuth2SuccessHandler oauth2SuccessHandler){
this.oauth2SuccessHandler = oauth2SuccessHandler;
}
@Bean
public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/css/**", "/js/**", "/images/**", "/favicon.ico/**").permitAll()
.requestMatchers(HttpMethod.GET, "/review/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.defaultSuccessUrl("/")
.successHandler((request, response, authentication) -> {
oauth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);
})
.failureUrl("/login?error=true")
)
.sessionManagement((session)-> session
.sessionCreationPolicy(SessionCreationPolicy.NEVER)
)
.requestCache((cache) -> cache
.requestCache(requestCache)
)
.logout(logout -> logout
.logoutSuccessUrl("/")
);
return http.build();
}
}
이렇게 config를 작성할 수 있다.
먼저 HttpSessionRequestCache의 setMatchingRequestParameterName이라는 값이 붙었는데, 이게 뭐냐면 만약 URL 쿼리 파라미터에 매개변수로 전한 값이 있다면 인증 후 사용자를 requestCache에 담긴, 원래 사용자가 보내려고 했던 요청으로 다시 보낸다는 뜻이다.
근데 이런 방식의 단점은 매번 쿼리 파라미터를 확인해야하기도하고, 모든 요청의 끝에 ?continue라는 쿼리파라미터가 필요해서 잘 쓰이지는 않는다. 그냥 null로 두고 매번 requestCache를 확인하는게 더 낫다고한다.
package com.example.GoldenReport.Config;
import com.example.GoldenReport.Service.LogInAndSignUp.OAuth2SuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
@Configuration
@EnableWebSecurity
public class SecurityConfig{
OAuth2SuccessHandler oauth2SuccessHandler;
public SecurityConfig(OAuth2SuccessHandler oauth2SuccessHandler){
this.oauth2SuccessHandler = oauth2SuccessHandler;
}
@Bean
public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/css/**", "/js/**", "/images/**", "/favicon.ico/**").permitAll()
.requestMatchers(HttpMethod.GET, "/review/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.defaultSuccessUrl("/")
.successHandler((request, response, authentication) -> {
oauth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);
})
.failureUrl("/login?error=true")
)
.sessionManagement((session)-> session
.sessionCreationPolicy(SessionCreationPolicy.NEVER)
)
.requestCache((cache) -> cache
.requestCache(requestCache)
)
.logout(logout -> logout
.logoutSuccessUrl("/")
);
return http.build();
}
}
package com.example.GoldenReport.Service.LogInAndSignUp;
import com.example.GoldenReport.Domain.Member;
import com.example.GoldenReport.Repository.MemberRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
MemberRepository memberRepository;
public OAuth2SuccessHandler(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
Map<String, Object> oAuth2UserResponse = oAuth2User.getAttribute("response");
String userId = oAuth2UserResponse.get("id").toString();
Optional<Member> member = memberRepository.findById(userId);
if (member.isPresent()) {
try {
System.out.println("There is Member with userId: " + userId);
//...로직 처리
Object returnURLObj = request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if (returnURLObj != null) {
String returnURL = (String) returnURLObj;
response.setStatus(HttpServletResponse.SC_OK);
response.sendRedirect(returnURL);
} else {
response.setStatus(HttpServletResponse.SC_FOUND);
response.sendRedirect("/");
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
System.out.println("There is No Member with userId: " + userId);
response.setStatus(HttpServletResponse.SC_FOUND);
response.sendRedirect("/signup");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
그래서 SuccessHandler에서 세션값을 확인한 후에 인증 후 어디로 보낼지 확인할 수 있다.
requestCache의 key값은 기본적으로 SPRING_SECURITY_SAVED_REQUEST다. 그래서 거기에 값이 있으면 글로보내고, 아니면 메인화면으로 보내고 이런 식으로 동작한다.
이렇게인줄 알았지만
package com.example.GoldenReport.Service.LogInAndSignUp;
import com.example.GoldenReport.Domain.Member;
import com.example.GoldenReport.Repository.MemberRepository;
import com.example.GoldenReport.Service.JWT.JWTFilter;
import com.example.GoldenReport.Service.JWT.JWTType;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
MemberRepository memberRepository;
JWTFilter jwtFilter;
RequestCache requestCache = new HttpSessionRequestCache();
public OAuth2SuccessHandler(MemberRepository memberRepository,
JWTFilter jwtFilter) {
this.memberRepository = memberRepository;
this.jwtFilter = jwtFilter;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
Map<String, Object> oAuth2UserResponse = oAuth2User.getAttribute("response");
String userId = oAuth2UserResponse.get("id").toString();
Optional<Member> member = memberRepository.findById(userId);
if (member.isPresent()) {
try {
String jwtToken = jwtFilter.generateAccessToken(userId);
Cookie cookie = new Cookie("Token", jwtToken);
cookie.setMaxAge((int) JWTType.ACCESS.getValidTime());
cookie.setPath("/");
response.addCookie(cookie);
System.out.println("Token is Ready" + jwtToken);
SavedRequest cache = requestCache.getRequest(request, response);
if (cache != null) {
String returnURL = cache.getRedirectUrl();
response.setStatus(HttpServletResponse.SC_OK);
response.sendRedirect(returnURL);
} else {
response.setStatus(HttpServletResponse.SC_FOUND);
response.sendRedirect("/");
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
System.out.println("There is No Member with userId: " + userId);
response.setStatus(HttpServletResponse.SC_FOUND);
response.sendRedirect("/signup");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
세션에는 value가 String 형태로 값이 저장되는게 아니라 DefaultSavedRequest라는 복잡한 객체의 형태로 저장되기 때문에, RequestCache 객체에서 직접 뽑아야한다.
그래서 RequestCache의 getRequest 메서드를 통해서 SavedRequest 객체를 뽑아내야한다. 거기서 getRedirectUrl 메서드를 통해 URL을 가져오는 식으로 매커니즘이 이루어진다.
JWT토큰 사용하기
이제 JWT토큰 방식을 사용할 차례
https://dev-dx2d2y-log.tistory.com/7
[GDG]홍대 맛집 아카이빙 프로젝트 백엔드 개발 #3 - 구글 OAuth
어제 개발하다가 구글로그인이 안돼서 보니까..OAuth2의 구글 로그인 API 유효기간이 1시간이더라.. 오늘은 API를 다시 발급받고 리프레시 토큰을 이용해보고자 한다.그거 성공하면 네이버 로그인 A
dev-dx2d2y-log.tistory.com
티스토리 초창기에도 한 번 다룬 적이 있었다. 이번에도 JJWT (Java JSON Web Token) 를 이용할 것이다.
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
의존성
Java에서 JJWT(Java JSON Web Token)를 이용한 JWT(JSON Web Token) 사용방법
Java에서 JJWT를 이용한 JSON Web Token 사용방법을 알아본다. 아래의 내용은 다음 링크를 참조하여 사용방법을 필요한 부분만 참조하여 작성을 하였다. github.com/jwtk/jjwt jwtk/jjwt Java JWT: JSON Web Token for Ja
samtao.tistory.com
이전에는 이 글을 참고했는데 JJWT가 세터를 Deprecated 시키고 빌더패턴으로 바꾼 모양이다.
package com.example.GoldenReport.Service.JWT;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
@Component
public class JWTProvider {
@Value("${jwt.secret-key}")
private String jwtSecretKey;
private SecretKey key = Keys.hmacShaKeyFor(jwtSecretKey.getBytes());
public String generateJWT(String userId, JWTType jwtType) {
Claims claims = Jwts.claims().build();
claims.put("userId", userId);
Date now = new Date();
return Jwts.builder()
.subject(userId)
.claims(claims)
.issuedAt(now)
.expiration(new Date(now.getTime() + jwtType.getValidTime()))
.signWith(SignatureAlgorithm.HS256, key)
.compact();
}
}
그래서 별건없고 그냥 세터를 빌더패턴으로만 바꾸면서 진행하면 된다.
Claims에는 저장할 내용이 들어간다. 주로 인증에 대한 사용자 정보가 들어가는데, 나는 userId만 넣으면되므로 빈 Claims 객체를 사용한다. userId는 JWT토큰의 subject 속성으로 들어간다.
package com.example.GoldenReport.Service.JWT;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum JWTType {
ACCESS (3600),
REFRESH (3600 * 24 * 3);
private final int validTime;
}
JWT 토큰의 유효시간은 Enum값으로 사용한다. ACCESS라면 3600초가 사용되고, REFRESH라면 3600 * 24 * 3 (3일)으로 유효시간을 설정한다. generateJWT 메서드에서는 저 enum 값을 통해서 유효시간을 설정한다.
Deprecated 찾기
package com.example.GoldenReport.Service.JWT;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.Optional;
@Component
public class JWTProvider {
@Value("${jwt.secret-key}")
private String jwtSecretKey;
private SecretKey key = Keys.hmacShaKeyFor(jwtSecretKey.getBytes());
public String generateJWT(String userId, JWTType jwtType) {
Claims claims = Jwts.claims().build();
claims.put("userId", userId);
Date now = new Date();
return Jwts.builder()
.subject(userId)
.claims(claims)
.issuedAt(now)
.expiration(new Date(now.getTime() + jwtType.getValidTime()))
.signWith(key)
.compact();
}
public Optional<String> decodeJWT(String jwt) {
String subject = Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt).getPayload().getSubject();
return Optional.ofNullable(subject);
}
}
이전보다 약간 코드가 변했다. 몇몇 명령어가 Deprecated 되었기 때문이다.
generateJWT 메서드는 위에서 설명했고, decodeJWT는 JWT토큰의 페이로드를 반환한다. 그중에서도 나는 페이로드가 비어있고, 실제로는 subject 속성만 필요하니 그것만 반환하기로한다.
subject가 비어있을 수 있으므로 Optional로 반환한다.

예전에 JWT를 만졌을 때보다 코드가 좀 더 간결해지고, 문법도 달라졌다. 위 사진의 문법들은 거의 Deprecated 됐다고봐도 무방하다.
또한 이전의 parsClaimsJws() 메서드나, 여기의 parsSignedClaims() 메서드나 모두 JWT토큰의 만료여부를 조사한다. 만약 만료가된다면 ExpiredJwtException이 터질 것이다.
이렇게 바뀐 이유는, 좀 더 간결하고 명확한 개발을 위한다고한다.
Jwts.parserBuilder().setSigningKey(String secret).build().parseClaimsJws(String token).getBody().getSubject();
//jjwt 0.10.0 이전 코드 (현재 Deprecated)
Jwts.parser().verifyWith(SecretKey key).build().parseSignedClaims(String jwt).getPayload().getSubject();
//현재 코드
우선은 가장 큰 차이는 이정도의 차이가 있는데, setSigningKey 메서드는 매개변수로 String을 받기 때문에, 실수로 secret 값으로 이상한 값이 들어가도 문제를 잡아낼 수 없었지만, 이를 verfyWith() 메서드로 교체하면서 반드시 SecretKey 를 매개변수로 받도록 고정했다. 따라서 실수가 일어날 가능성이 적은 편이다.
또한 parseClaimsJws()메서드와 parseSignedClaims() 메서드가 있는데, 순수한 JWT방식은 signature가 없었다. 즉, 그냥 헤더와 페이로드로만 이루어져있었다.
여기에 시그니쳐를 추가해서, 헤더와 페이로드를 secret으로 해시연산을 진행한 것과 시그니쳐를 비교해 데이터의 변조를 검사하는 방식이 JWS (JSON Web Signature), JSON 데이터까지 암호화시키면 JWE (JSON WEB Encryption)이 되는 것이다. 순수한 JWT는 보안에 취약해서 잘 안쓰이고, JWS와 JWE 두 방식은 사용하는 메서드가 달랐다.
그래서 보안을 가져오는 메서드를 JWS를 parseClaimsJws(), JWE를 parseClaimsJwe() 메서드로 구분해서 사용하는 것이었는데, 말이 좀 어렵고 JWT에 더해서 JWS와 JWE의 개념도 알아야하니 알기쉽게 parseSignedClaims() (서명된 claims 가져오기, JWS), parseEncryptedClaims() (암호화된 claims 가져오기, JWE)로 바꾼 것이다.
또한 기존 parseClaimsJws()는 setSigningKey(). 그러니까 시그니쳐와 페이로드를 검증해보지 않고도 페이로드 값을 불러올 수 있었다. parseSignedClaims()에서는 이 가능성을 원천차단하고자 반드시 veryfyWith() 메서드를 반드시 거쳐야, 그러니까 시그니쳐와 페이로드를 검증해봐야 반드시 페이로드를 읽을 수 있도록 로직을 변경했다.
package com.example.GoldenReport.Service.JWT;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class JWTFilter {
JWTProvider jwtProvider;
public JWTFilter(JWTProvider jwtProvider) {
this.jwtProvider = jwtProvider;
}
public String generateAccessToken(String userId) {
return jwtProvider.generateJWT(userId, JWTType.ACCESS);
}
public String generateRefreshToken(String userId) {
return jwtProvider.generateJWT(userId, JWTType.REFRESH);
}
public Optional<String> getPlainString(String token) {
return jwtProvider.decodeJWT(token);
}
}
필터
/**
* 필터는 헤더에서 토큰을 가져오고 이를 검증하기 위해 Provider에 데이터를 넘기는 역할을 한다.
* 서비스계층에서는 filter에 JWT토큰을 넘기고 이의 메시지를 받아서 ResponseEntity를 반환함.
*
* createToken / createRefreshToken -> 새로운 토큰을 만드는 역할
* getTokenFromHeader -> 프론트엔드에서 헤더에 토큰을 보내는데 헤더에서 토큰을 얻는 코드
* checkValidity -> 유효성 검사. 검사 후 자동으로 getUserRoles로 연결됨
* getUserRoles -> 유저의 권한 확인
* checkValidityInTrueFalse -> 유효성 검사 후 true / false를 반환함
* getUserRolesInJSON -> 유저의 권한 확인. 사실 이거 필요없는데 왜 만들었지
*
* @author dx2d2y
* @param secret JWT 토큰 발급 / 유효성 인증에 필요한 시크릿 키
* @param identifire 유저 식별자 (이메일이나 고유id)
* @param token JWT 토큰 (액세스 또는 리프레시)
*/
package com.hongchelin.service.JWT;
import com.hongchelin.dto.user.MemberDTO;
import com.hongchelin.dto.user.ResponseDTO;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
@Component
public class JWTFilter {
private final JWTProvider jwtProvider;
public JWTFilter(JWTProvider jwtProvider) {
this.jwtProvider = jwtProvider;
}
public String createToken(String secret, String identifier) {
return jwtProvider.createToken(secret, identifier);
}
public String createRefreshToken(String secret, String identifier) {
return jwtProvider.createRefreshToken(secret, identifier);
}
public ResponseDTO getTokenFromHeader(String secret, HttpServletRequest request) {
try {
String token = request.getHeader("AccessToken");
return checkValidityAndReturnUserRoles(secret, token);
} catch (Exception e) {
return ResponseDTO.builder()
.status(400)
.message("권한이 없습니다.")
.validity(false)
.build();
}
}
public ResponseDTO checkValidityAndReturnUserRoles(String secret, String token) {
String validityMessage = jwtProvider.validateToken(secret, token);
if (validityMessage.equals("성공")) {
Claims claims = jwtProvider.getSubject(secret, token);
String identifier = (String) claims.get("identifier");
MemberDTO member = MemberDTO.builder()
.identifier(identifier)
.build();
return ResponseDTO.builder()
.status(200)
.validity(true)
.MemberInfo(member)
.accessToken(token)
.build();
} else {
return ResponseDTO.builder()
.status(400)
.message(validityMessage)
.validity(false)
.build();
}
}
public ResponseDTO getUserInfo(String secret, String token) {
Claims claims = jwtProvider.getSubject(secret, token);
String identifier = (String) claims.get("identifier");
MemberDTO member = MemberDTO.builder()
.identifier(identifier)
.build();
ResponseDTO responseDTO = ResponseDTO.builder()
.status(200)
.MemberInfo(member)
.build();
return responseDTO;
}
}
예전에 JWT할 때는 뭔가 로직들이 잔뜩 꼬여있었는데, 이번에 개발하면서는 최대한 간결하게 짜보는게 목표다.
package com.example.GoldenReport.Service.LogInAndSignUp;
import com.example.GoldenReport.Domain.Member;
import com.example.GoldenReport.Repository.MemberRepository;
import com.example.GoldenReport.Service.JWT.JWTFilter;
import com.example.GoldenReport.Service.JWT.JWTType;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
MemberRepository memberRepository;
JWTFilter jwtFilter;
public OAuth2SuccessHandler(MemberRepository memberRepository,
JWTFilter jwtFilter) {
this.memberRepository = memberRepository;
this.jwtFilter = jwtFilter;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
Map<String, Object> oAuth2UserResponse = oAuth2User.getAttribute("response");
String userId = oAuth2UserResponse.get("id").toString();
Optional<Member> member = memberRepository.findById(userId);
if (member.isPresent()) {
try {
String jwtToken = jwtFilter.generateAccessToken(userId);
Cookie cookie = new Cookie("Token", jwtToken);
cookie.setMaxAge((int) JWTType.ACCESS.getValidTime());
cookie.setPath("/");
response.addCookie(cookie);
Object returnURLObj = request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if (returnURLObj != null) {
String returnURL = (String) returnURLObj;
response.setStatus(HttpServletResponse.SC_OK);
response.sendRedirect(returnURL);
} else {
response.setStatus(HttpServletResponse.SC_FOUND);
response.sendRedirect("/");
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
System.out.println("There is No Member with userId: " + userId);
response.setStatus(HttpServletResponse.SC_FOUND);
response.sendRedirect("/signup");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
암튼 JWT토큰 로직이 완성되었으므로 JWT토큰을 발급한다.
이전에 JWT토큰을 다룰 때에는 반환값에 반환하여 프론트엔드가 로컬 스토리지에 저장하도록했는데, 지금은 SuccessHandler에서 HttpStatus를 302로 반환하므로 응답값이 없기에 그냥 쿠키에 달아두는 것으로 사용했다.
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
쿠키는 보안에 다소 취약한 편이므로 application.properties에서 http-only와 secure 속성값을 true로 바꿨다. http-only 속성은 자바스크립트가 접근할 수 없는 쿠키를 나타내고, secure 속성은 HTTPS 환경에서만 쿠키를 사용할 수 있다는 설정이다.
다만 로컬호스트는 HTTP 연결을 사용하므로 테스트환경에서는 secure 속성을 제외시켜야할 듯하다.
우선은 이렇게 JWT토큰을 발급하고 쿠키에 저장하는 과정까지 마무리했다.
나중에 한 번 정리가 필요하겠다만 우선은 여기서 끝

이렇게 토큰도 잘 형성되는 것을 알 수 있다.
다음에할거
- 로그인 로직 정리
- 유효성검사
- 회원가입 로직 개발
- 회원기능연결
'개인 프로젝트 > [2026] 골든리포트!' 카테고리의 다른 글
| [골든리포트!] 6) 필터체인에서 JWT필터 인증 처리하기 (0) | 2026.02.13 |
|---|---|
| [골든리포트!] 5) JWT 토큰 후속기능 처리하기 (0) | 2026.02.12 |
| [골든리포트!] 3) Spring Security Config로 스프링 OAuth 다루기 (0) | 2026.02.10 |
| [골든리포트!] 2) TMDB API 가져와서 리뷰 작성기능 넣기 (0) | 2026.02.05 |
| [골든리포트!] 1. 영영 볼 수 없는 리뷰 작성 설계하기 (0) | 2026.01.24 |