이 포스팅에서는 스프링 부트 3.2.2 버전을 사용하고, 스프링 시큐리티 6.2.1 버전을 사용합니다.
이번 포스팅부터는 JWT를 사용한 인증방식을 도입해보겠습니다.
JWT는 JSON Web Token의 약자로, 토큰 기반 인증을 지원하는 것입니다. 토큰 기반 인증은 말 그대로 토큰을 인증에 사용하는 것입니다.
토큰은 서버에서 클라이언트를 구분하기 위한 유일한 값입니다. 서버에서 코튼을 생성해서 클라이언트에게 제공하면, 클라이언트는 해당 토큰을 가지고 있다가 여러 요청과 토큰을 함께 보내면 서버에서 유효성 검사 후 토큰이 유효하다면 클라이언트의 요청을 성공적으로 처리해주게 됩니다.
토큰은 아래 두가지 경우에 사용하면 좋습니다.
(서명이란, 이 토큰을 누가 작성했는지 알아내는것)
header.payload.signature
세가지로 이루어져 있으며, 각각은 '.'으로 구분합니다. (헤더, 내용, 서명)
https://jwt.io/#debugger-io -> 이 사이트에서 직접 jwt 디버거를 통해 jwt를 생성해보실 수 있습니다.
토큰의 타입(type)과 해싱 알고리즘(alg)를 지정합니다.
헤더는 일반적으로 토큰의 유형과 서명에 대한 해싱 알고리즘(보통 HMAC SHA256 또는 RSA)를 지정하는 정보를 담고있습니다.
위 사진처럼 JWT에 HS512 해싱 알고리즘을 사용해서 암호화 및 복호화를 해보겠습니다.
참고로, 암호화 방식에서 SHA는 해쉬를 사용한 암호화 방식으로, 복호화가 불가능합니다. 하지만 HMAC은 시크릿 키를 포함하여 암호화하는 방식입니다. (HS 알고리즘 -> HMAC + SHA로 시크릿 키를 가짐)
토큰에 담을 정보입니다.
토큰에 담을 하나의 정보, 한 조각의 정보를 클레임(Claim)이라고 합니다. 클레임의 종류에는 3가지가 있습니다.
}
"sub": "accessToken", // 등록된 클레임 (제목)
"name": "Do Hyun", // 비공개 클레임
"email" : "dh1010a@naver.com" // 비공개 클레임
"iat": 1516239022 // 등록된 클레임 (발급된 시간)
}
토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드
서명은 위에서 만든 헤더와 페이로드의 값을 각각 BASE64로 인코딩하고, 인코딩 한 값을 비밀 키를 이용하여 헤더에서 정한 알고리즘으로 해싱을 한 후 다시 BASE64로 인코딩 하여 생성합니다.
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
서명은 HS256 방식의 경우 우리가 만든 헤더와 페이로드, 그리고 나만 아는 개인 키를 넣어서 HS256 암호화 알고리즘을 사용하여 암호화를 하여서 사용합니다.
이는 서버에서 클라이언트로부터 JWT를 받았을 때, JWT의 헤더와 페이로드를 서버에서 똑같이 HS256으로 암호화하여, 클라이언트가 보낸 JWT의 서명고 같으면 요청된 것으로 알고 검증합니다.
RSA의 경우, 시크릿 키를 넣지 않고, 서버의 개인 키로 잠군 후, 토큰을 전송합니다. 클라이언트는 해당 토큰을 받아 다시 서버에 전송시, 그냥 서버의 공개 키로 열어보기만 하면 됩니다.
토큰 기반 인증의 장점이었던 Stateless 서버는 한 가지 문제점이 있습니다. 바로 세션 기반 인증과 같이 서버 쪽에서 관리가 되고 있지 않기 때문에 올바른 사용자인지, 아니면 누군가 토큰을 탈취해 악의적인 의도로 접근해 온 사용자인지 알지 못한다는 것입니다.
때문에 액세스 토큰의 유효 기간을 아예 짧게 설정하는 것으로 나름 보안을 강화할 수 있지만 그러면 사용하는 입장에선 불편함을 느낄 수 밖에 없습다. 이런 문제점을 해결하기 위해 나타난 것이 Refresh Token
입니다.
리프레시 토큰은 액세스 토큰이 만료되었을 때 액세스 토큰을 새로 발급하기 위해 필요한 토큰입니다. 액세스 토큰과 다른 점은 리프레시 토큰은 액세스 토큰보다 유효 기간이 길고, 데이터베이스에 저장한다는 점입니다.
AccessToken
만 사용하게 되면 확실하게 Stateless하게 인증 정보를 처리할 수 있지만, RefreshToken
을 사용하게 되는 순간 결국 DB에 정보를 저장하게 되어 Stateless한가..? 라는 의문점이 들었습니다.
찾아보니 많은 분들이 공통적으로 하는 고민인거 같습니다. 하지만 아직 결론은 명확하지 않은 것 같고, 리프레쉬 토큰을 사용했을때의 장점은 'Session에 비해 RefreshToken이 만료된 경우에만 DB에 접근하므로 I/O가 줄어 성능이 향상' 이라고 하는것 같았습니다.
인증에서 사용자가 자격증명을 사용하여 성 공적으로 로그인하면 JSON 웹 토큰이 반환됩니다. 토큰은 자격증명을 주 목적으로 사용하므로 보안 문제에 주의가 필요합니다. 일반적으로 토큰을 필요이상으로 오래 보관하면 안됩니다.
또한 보안에 취약할 수 있기에 민감한 세션 데이터를 브라우저 저장소에 저장해서는 안됩니다.
클라이언트가 보호된 경로 또는 리소스에 접근하려고 할 때 마다, 클라이언트는 일반적으로 Bearer
스키마를 사용하여 Authorization
헤더에서 JWT를 보내야 합니다.
#헤더의 내용
Authorization: Bearer <token>
build.gradle
에 다음과 같은 의존성을 추가합니다.
//== jwt 추가 ==//
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.projectlombok:lombok'
implementation 'commons-codec:commons-codec:1.5'
implementation 'com.auth0:java-jwt:3.13.0'
이제 토큰을 사용하기 위해, Users 엔티티에 RefreshToken을 추가합니다.
/== jwt 토큰 추가 ==//
@Column(length = 1000)
private String refreshToken;
public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public void destroyRefreshToken() {
this.refreshToken = null;
}
@Entity
@Getter
@Builder
@AllArgsConstructor
@Table(name = "USERS")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true, length = 30)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String phoneNum;
private String imgUrl;
@Enumerated(EnumType.STRING)
@JsonIgnore
private PublicStatus publicStatus;
@JsonIgnore
@Enumerated(EnumType.STRING)
private ShareStatus shareStatus;
private LocalDate createdAt;
//== jwt 토큰 추가 ==//
@Column(length = 1000)
private String refreshToken;
public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public void destroyRefreshToken() {
this.refreshToken = null;
}
@JsonIgnore
@OneToMany(mappedBy = "user")
private List<SharedAlbum> sharedAlbums = new ArrayList<>();
//== 패스워드 암호화 ==//
public void encodePassword(PasswordEncoder passwordEncoder){
this.password = passwordEncoder.encode(password);
}
}
유저 레포지토리에 refreshToken
을 통해 유저를 조회하는 기능을 추가해주도록 하겠습니다.
public interface UsersRepository extends JpaRepository<Users, Long> {
Optional<Users> findByEmail(String email);
boolean existsByEmail(String email);
Optional<Users> findByPhoneNum(String phoneNum);
boolean existsByPhoneNum(String phoneNum);
Optional<Users> findByRefreshToken(String refreshToke);
}
JWT와 관련한 비즈니스 로직(AccessToken 생성, RefreshToken생성, RefreshToken 재발급, RefreshToken 삭제, AccessToken 삭제 등)을 작성해보겠습니다.
application.yml
에 다음을 추가합니다.
spring:
profiles:
include: jwt #jwt.yml 불러오기
이렇게 작성하면 application-jwt에 해당하는 .yml이나 .properties 파일을 읽어올 수 있습니다.
다음 application-jwt.yml
을 생성하여 아래와 같이 구성합니다. 이 파일에서는 JWT 관련 시크릿 키, 토큰의 만료 시간 등을 설정해주도록 하겠습니다.
jwt:
secret: 63fba97a41e0d004e10e8dbbcb9a547819280efb00a54c732aca36a8a58258e4fcc539ffc5159a7f0a7be78b86efe001c12ba6af6debeb0a89e8ce7e82e75455
access:
expiration: 80
header: Authorization
refresh:
expiration: 90
header: Authorization-refresh
accessToken은 80초, refreshToken은 90초로 설정해놨습니다. (향후 변경 예정)
저희는 base64로 인코딩된 암호 키, HS512를 사용할 것이기 때문에, 512비트(64바이트) 이상이 되어야 합니다. 시크릿 키는 아래와 같은 방법으로 생성해주었습니다.
64바이트의 랜덤한 키를 생성해줍니다.
이제, JwtService의 인터페이스 부터 작성하겠습니다.
public interface JwtService {
String createAccessToken(String email);
String createRefreshToken();
void updateRefreshToken(String email, String refreshToken);
void destroyRefreshToken(String email);
void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken);
void sendAccessToken(HttpServletResponse response, String accessToken);
Optional<String> extractAccessToken(HttpServletRequest request);
Optional<String> extractRefreshToken(HttpServletRequest request);
Optional<String> extractEmail(String accessToken);
void setAccessTokenHeader(HttpServletResponse response, String accessToken);
void setRefreshTokenHeader(HttpServletResponse response, String refreshToken);
boolean isTokenValid(String token);
}
구현체는 아래와 같습니다.
@Transactional
@Service
@RequiredArgsConstructor
@Setter(value = AccessLevel.PRIVATE)
@Slf4j
public class JwtServiceImpl implements JwtService {
//== jwt.yml에 설정된 값 가져오기 ==//
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access.expiration}")
private long accessTokenValidityInSeconds;
@Value("${jwt.refresh.expiration}")
private long refreshTokenValidityInSeconds;
@Value("${jwt.access.header}")
private String accessHeader;
@Value("${jwt.refresh.header}")
private String refreshHeader;
//== 1 ==//
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String USERNAME_CLAIM = "email";
private static final String BEARER = "Bearer ";
private final UsersRepository usersRepository;
private final ObjectMapper objectMapper;
//== 메서드 ==//
@Override
public String createAccessToken(String email) {
return JWT.create()
.withSubject(ACCESS_TOKEN_SUBJECT)
.withExpiresAt(new Date(System.currentTimeMillis() + accessTokenValidityInSeconds * 1000))
.withClaim(USERNAME_CLAIM, email)
.sign(Algorithm.HMAC512(secret));
}
@Override
public String createRefreshToken() {
return JWT.create()
.withSubject(REFRESH_TOKEN_SUBJECT)
.withExpiresAt(new Date(System.currentTimeMillis() + refreshTokenValidityInSeconds * 1000))
.sign(Algorithm.HMAC512(secret));
}
@Override
public void updateRefreshToken(String email, String refreshToken) {
usersRepository.findByEmail(email)
.ifPresentOrElse(
users -> users.updateRefreshToken(refreshToken),
() -> new Exception("회원 조회 실패")
);
}
@Override
public void destroyRefreshToken(String email) {
usersRepository.findByEmail(email)
.ifPresentOrElse(
users -> users.destroyRefreshToken(),
() -> new Exception("회원 조회 실패")
);
}
@Override
public void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken) {
response.setStatus(HttpServletResponse.SC_OK);
setAccessTokenHeader(response, accessToken);
setRefreshTokenHeader(response, refreshToken);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put(ACCESS_TOKEN_SUBJECT, accessToken);
tokenMap.put(REFRESH_TOKEN_SUBJECT, refreshToken);
}
@Override
public void sendAccessToken(HttpServletResponse response, String accessToken) {
response.setStatus(HttpServletResponse.SC_OK);
setAccessTokenHeader(response, accessToken);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put(ACCESS_TOKEN_SUBJECT, accessToken);
}
@Override
public Optional<String> extractAccessToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(accessHeader)).filter(
accessToken -> accessToken.startsWith(BEARER)
).map(accessToken -> accessToken.replace(BEARER, ""));
}
@Override
public Optional<String> extractRefreshToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(refreshHeader)).filter(
refreshToken -> refreshToken.startsWith(BEARER)
).map(refreshToken -> refreshToken.replace(BEARER, ""));
}
@Override
public Optional<String> extractEmail(String accessToken) {
try {
return Optional.ofNullable(
JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken).getClaim(USERNAME_CLAIM)
.asString());
} catch (Exception e) {
log.error(e.getMessage());
return Optional.empty();
}
}
@Override
public void setAccessTokenHeader(HttpServletResponse response, String accessToken) {
response.setHeader(accessHeader, accessToken);
}
@Override
public void setRefreshTokenHeader(HttpServletResponse response, String refreshToken) {
response.setHeader(refreshHeader, refreshToken);
}
@Override
public boolean isTokenValid(String token) {
try {
JWT.require(Algorithm.HMAC512(secret)).build().verify(token);
return true;
} catch (Exception e) {
log.error("유효하지 않은 Token입니다", e.getMessage());
return false;
}
}
}
아래 메서드들은 이름과 내용을 보시면 충분히 이해할 수 있을것 같습니다.
JWT.create() //JWT 토큰을 생성하는 빌더를 반환합니다.
.withSubject(ACCESS_TOKEN_SUBJECT)
//빌더를 통해 JWT의 Subject를 정합니다. AccessToken이므로 위에서 설정했던
//AccessToken의 subject를 합니다.
.withExpiresAt(new Date(System.currentTimeMillis() + accessTokenValidityInSeconds * 1000))
//만료시간을 설정하는 것입니다. 현재 시간 + 저희가 설정한 시간(밀리초) * 1000을 하면
//현재 accessTokenValidityInSeconds이 80이기 때문에
//현재시간에 80 * 1000 밀리초를 더한 '현재시간 + 80초'가 설정이 되고
//따라서 80초 이후에 이 토큰은 만료됩니다.
.withClaim(USERNAME_CLAIM, email)
//클레임으로는 email 하나만 사용합니다.
//추가적으로 식별자나, 이름 등의 정보를 더 추가가능합니다.
//추가하는 경우 .withClaim(클래임 이름, 클래임 값) 으로 설정합니다.
.sign(Algorithm.HMAC512(secret));
//HMAC512 알고리즘을 사용하여, 저희가 지정한 secret 키로 암호화 합니다.
RefreshToken의 경우에는 email을 넣지 않습니다. 리프레쉬 토큰은 엑세스 토큰 재발급의 용도로만 사용할 것이기 때문에 정보는 최대한 넣지 않고 DB에 보관하도록 하겠습니다.
JWT.require(Algorithm.HMAC512(secret))
//토큰의 서명의 유효성을 검사하는데 사용할 알고리즘이 있는
//JWT verifier builder를 반환합니다
.build()//반환된 빌더로 JWT verifier를 생성합니다
.verify(accessToken)//accessToken을 검증하고 유효하지 않다면 예외를 발생시킵니다.
.getClaim(USERNAME_CLAIM)//claim을 가져옵니다
.asString();
테스트 코드 및 전체 코드는 맨 아래에서 확인하실 수 있습니다.
로그인이 성공했을때의 동작을 관리하는 LoginSuccessJWTProvideHandler를 수정해 로그인 성공시 JWT 토큰을 발급할 수 있도록 하겠습니다.
@Slf4j
@RequiredArgsConstructor
public class LoginSuccessJWTProvideHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtService jwtService;
private final UsersRepository usersRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String email = extractEmail(authentication);
String accessToken = jwtService.createAccessToken(email);
String refreshToken = jwtService.createRefreshToken();
jwtService.sendAccessAndRefreshToken(response, accessToken, refreshToken);
usersRepository.findByEmail(email).ifPresent(
users -> users.updateRefreshToken(refreshToken)
);
log.info( "로그인에 성공합니다. email: {}" , email);
log.info( "AccessToken 을 발급합니다. AccessToken: {}" ,accessToken);
log.info( "RefreshToken 을 발급합니다. RefreshToken: {}" ,refreshToken);
response.getWriter().write("success");
}
private String extractEmail(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final ObjectMapper objectMapper;
private final UsersRepository usersRepository;
private final JwtService jwtService;
// 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http .csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/feed/**", "/albums/**", "/photo/**", "/user/signup", "/", "/login", "/album/init").permitAll()
.anyRequest().authenticated())
// .formLogin(formLogin -> formLogin
// .loginPage("/login")
// .defaultSuccessUrl("/home"))
.logout((logout) -> logout
.logoutSuccessUrl("/login")
.invalidateHttpSession(true))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http
.addFilterAfter(jsonUsernamePasswordLoginFilter(), LogoutFilter.class) // 추가 : 커스터마이징 된 필터를 SpringSecurityFilterChain에 등록
.addFilterBefore(jwtAuthenticationProcessingFilter(), JsonUsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 인증 관리자 관련 설정
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() throws Exception {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
@Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {//2 - AuthenticationManager 등록
DaoAuthenticationProvider provider = daoAuthenticationProvider();//DaoAuthenticationProvider 사용
return new ProviderManager(provider);
}
@Bean
public LoginSuccessJWTProvideHandler loginSuccessJWTProvideHandler(){
return new LoginSuccessJWTProvideHandler(jwtService, usersRepository);
}
@Bean
public LoginFailureHandler loginFailureHandler(){
return new LoginFailureHandler();
}
@Bean
public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordLoginFilter() throws Exception {
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordLoginFilter = new JsonUsernamePasswordAuthenticationFilter(objectMapper);
jsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
jsonUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessJWTProvideHandler());
jsonUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());
return jsonUsernamePasswordLoginFilter;
}
@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter(){
JwtAuthenticationProcessingFilter jsonUsernamePasswordLoginFilter = new JwtAuthenticationProcessingFilter(jwtService, usersRepository);
return jsonUsernamePasswordLoginFilter;
}
}
usersRepository의 종속성을 추가해줍니다. 추가로 MemberRepository와 JwtService를 필드에서 받아오도록 수정하겠습니다.
또한 .addFilterBefore(jwtAuthenticationProcessingFilter(), JsonUsernamePasswordAuthenticationFilter.class);
를 통해 새로 jwt 인증을 진행할 시큐리티 필터를 기존 jwtAuthenticationProcessingFilter()
앞에 추가하여 동작하도록 하였습니다.
필터의 요구사항은 다음과 같습니다.
JsonUsernamePasswordAuthenticationFilter
에게 로그인 처리를 위임합니다.그 외에 들어오는 모든 요청에 대해서 작동합니다.
RefreshToken을 포함하여 요청이 전송되는 경우는 다음 네 가지 상황이 있을 수 있습니다.
OncePerRequestFilter
을 상속받아 구현해보겠습니다.
OncePerRequestFilter
는 모든 서블릿 컨테이너에서 요청 디스패치당 단일 실행을 보장하는 것을 목표로 하는 필터 기본 클래스 입니다.
@RequiredArgsConstructor
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UsersRepository usersRepository;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();//5
private final String NO_CHECK_URL = "/login";//1
/**
* 1. 리프레시 토큰이 오는 경우 -> 유효하면 AccessToken 재발급후, 필터 진행 X, 바로 튕기기
*
* 2. 리프레시 토큰은 없고 AccessToken만 있는 경우 -> 유저정보 저장후 필터 계속 진행
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response);
return;//안해주면 아래로 내려가서 계속 필터를 진행하게됨
}
String refreshToken = jwtService
.extractRefreshToken(request)
.filter(jwtService::isTokenValid)
.orElse(null); //2
if(refreshToken != null){
checkRefreshTokenAndReIssueAccessToken(response, refreshToken);//3
return;
}
checkAccessTokenAndAuthentication(request, response, filterChain);//4
}
private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
jwtService.extractAccessToken(request).filter(jwtService::isTokenValid).ifPresent(
accessToken -> jwtService.extractEmail(accessToken).ifPresent(
email -> usersRepository.findByEmail(email).ifPresent(
users -> saveAuthentication(users)
)
)
);
filterChain.doFilter(request,response);
}
private void saveAuthentication(Users users) {
UserDetailsImpl userDetails = new UserDetailsImpl(users);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null,authoritiesMapper.mapAuthorities(userDetails.getAuthorities()));
SecurityContext context = SecurityContextHolder.createEmptyContext();//5
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
private void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
usersRepository.findByRefreshToken(refreshToken).ifPresent(
users -> jwtService.sendAccessToken(response, jwtService.createAccessToken(users.getEmail()))
);
}
}
"/login" 으로 들어오는 요청에 대해서는 작동하지 않습니다.
RefreshToken이 없거나 유효하지 않다면 null을 반환합니다.
refreshToken이 유효하다면 해당 refreshToken을 가진 유저정보를 찾아오고, 존재한다면 AccessToken을 재발급합니다.
이때 바로 return시키는데, 그 이유는 refreshToken만 보낸 경우에는 인증을 처리하지 않게 하기 위해서입니다.
refreshToken이 없다면 AccessToken을 검사하는 로직을 수행합니다.
request에서 AccessToken을 추출한 후, 있다면 해당 AccessToken에서 email을 추출합니다. email이 추출되었다면 해당 회원을 찾아와서 그 정보를 가지고 인증처리를 합니다. 이때 SecurityContextHolder
에 Authentication
객체를 만들어 반환하는데, NullAuthoritiesMapper
가 쓰입니다.
이는 스프링 시큐리티에서 제공해주는 것입니다.
전체 테스트 코드 및 제가 구현한 코드는 아래에서 확인하시면 됩니다.
다음 포스팅 부터는 본격적으로 API 기능 구현을 해보겠습니다.
- 참고하여 따라한 블로그 주소 : https://ttl-blog.tistory.com/272#JwtService%20%EC%9E%91%EC%84%B1-1
- 제가 작성한 코드의 깃허브 주소 : https://github.com/dh1010a/PickPick_BE
기타 참고