JWT 토큰 서버 구축하기 (인가/인증) (3) - AccessToken 발급

2024. 3. 25. 00:48· BackEnd
목차
  1. 1. successfulAuthentication 구현 전 참고해야할 내용
  2. 2.  토큰 발급 코드 작성
  3. 2-1. JwtDto
  4. 2-2. JwtUtil에 발급 코드 작성
  5. 3. RedisConfig 와 RedisUtil
  6. 1) RedisConfig
  7. 2) RedisUtil
  8. 4. successfulAuthentication 구현
  9. 5. unsuccessfulAuthentication
  10. + 참고 ApiResponse, 
  11. 6. SecurityConfig에 Filter 추가하기
  12. 7. TEST
  13. 1) Success Case
  14. 2) Error Case
반응형
 

JWT 토큰 서버 구축하기 (인가/인증) (2) - 로그인 과정 처리

해당 POST 보고 오시길 바랍니다. Spring Boot에서 JWT 프로젝트 세팅하기 / JWT 테스트 gradle 기준 1. build.gradle에 Dependecy 추가 implementation 'io.jsonwebtoken:jjwt-api:0.11.1' implementation 'io.jsonwebtoken:jjwt-impl:0.11.1'

jinhos-devlog.tistory.com

이어서...

attemptAuthentication 인증이 완료되면 -> successfulAuthentication (인증 후 후처리)
attemptAuthentication 인증이 실패하면 -> unsuccessfulAuthentication

 

1. successfulAuthentication 구현 전 참고해야할 내용

4) JWT 토큰을 만들어서 응답해주면 된다.

마지막 4 단계인 토큰 발급 후 응답!

 

attemptAuthentication 함수에서 인증이 성공한 뒤에

이 함수가 종료되고, 이어서 다른 내부 메소드인 successfulAuthentication 함수가 실행된다!

 

인증에 성공한 사용자의 정보를 기반으로 JWT 토큰을 생성하고, 이를 응답으로 반환할 것이다.

우리는 이 과정을 successfulAuthentication 메소드에서 처리할 것이다!


우리는 접근성/보안성의 이유로 JWT 형식의 토큰을 두 가지 발급한다고 했다.

https://jinhos-devlog.tistory.com/entry/JWT%EB%9E%80
5. Refresh Token 사용하기 참고

 

액세스 토큰 (Access Token): 사용자가 인증되었음을 증명하는 토큰.
주로 클라이언트가 서버로 요청을 보낼 때 사용된다.
이 토큰은 사용자의 인증 정보 및 권한을 포함하고 있으며, 일반적으로 짧은 유효 기간을 가진다.
리프레시 토큰 (Refresh Token): 엑세스 토큰보다 오랜 유효 기간을 가지며,
액세스 토큰의 만료 시간이 지난 경우에 사용되어 새로운 액세스 토큰을 발급받기 위해 사용된다.
주로 보다 보안적으로 중요한 정보를 담고 있으며, 보통은 암호화되어 Redis에 저장된다.

 

successfulAuthentication 을 완성하기 전에 Jwt에 관한 Util을 만들어주자.
(생성 및 관리에 관한 메소드들)

2.  토큰 발급 코드 작성

2-1. JwtDto


      
public record JwtDto(
String accessToken,
String refreshToken
) {
}

2-2. JwtUtil에 발급 코드 작성


      
@Slf4j
@Component
public class JwtUtil {
private final SecretKey secretKey;
private final Long accessExpMs;
private final Long refreshExpMs;
private final RedisUtil redisUtil;
public JwtUtil(
// 해당 @Value 값들은 yml에서 설정할 수 있다
@Value("${spring.jwt.secret}") String secret,
@Value("${spring.jwt.token.access-expiration-time}") Long access,
@Value("${spring.jwt.token.refresh-expiration-time}") Long refresh,
RedisUtil redis) {
secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),
Jwts.SIG.HS256.key().build().getAlgorithm());
accessExpMs = access;
refreshExpMs = refresh;
redisUtil = redis;
}
// JWT 토큰을 입력으로 받아 토큰의 페이로드에서 사용자 이름(Username)을 추출
public String getUsername(String token) throws SignatureException {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
// JWT 토큰을 입력으로 받아 토큰의 페이로드에서 사용자 이름(roll)을 추출
public String getRoles(String token) throws SignatureException{
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("role", String.class);
}
// JWT 토큰의 페이로드에서 만료 시간을 검색, 밀리초 단위의 Long 값으로 반환
public long getExpTime(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration()
.getTime();
}
// Token 발급
public String tokenProvider(PrincipalDetails principalDetails, Instant expiration) {
Instant issuedAt = Instant.now();
String authorities = principalDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.header()
.add("typ", "JWT")
.and()
.subject(principalDetails.getUsername())
.claim("role", authorities)
.issuedAt(Date.from(issuedAt))
.expiration(Date.from(expiration))
.signWith(secretKey)
.compact();
}
// principalDetails 객체에 대해 새로운 JWT 액세스 토큰을 생성
public String createJwtAccessToken(PrincipalDetails principalDetails) {
Instant expiration = Instant.now().plusMillis(accessExpMs);
return tokenProvider(principalDetails, expiration);
}
// principalDetails 객체에 대해 새로운 JWT 리프레시 토큰을 생성
public String createJwtRefreshToken(PrincipalDetails principalDetails) {
Instant expiration = Instant.now().plusMillis(refreshExpMs);
String refreshToken = tokenProvider(principalDetails, expiration);
// 레디스에 저장
redisUtil.save(
principalDetails.getUsername(),
refreshToken,
refreshExpMs,
TimeUnit.MILLISECONDS
);
return refreshToken;
}
}

 

secret, access-expiration-time, refresh-expiration-time 와 같은 값들은 보안적으로 중요하기 때문에
소스 코드에 직접 하드코딩하는 것은 보안상 권장되지 않는다.

application.yml 파일에 설정해주자.


      
spring:
redis:
host: localhost
port: 6379
jwt:
secret: testSecretKey20240316testSecretKey20240316testSecretKey20240316
token:
access-expiration-time: 3600000
refresh-expiration-time: 86400000

 

추가적으로 getExpTime(), reissueToken(), resolveAccessToken(), validateRefreshToken() 등은 이후에 다시 작성하겠다!
일단은 토큰 생성하는 코드만.

 

3. RedisConfig 와 RedisUtil

 

그리고 리프레시 토큰은 Redis에 저장한다고 하였다!

Redis를 사용하기 위해 Config와 Util을 만들어주자.

 

https://jinhos-devlog.tistory.com/entry/SpringBoot%EC%97%90%EC%84%9C-Redis-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0#4.%20RedisConfig%20%EC%9E%91%EC%84%B1-1

RedisConfig 작성 & 템플릿 사용하기 (RedisUtil) 참고

1) RedisConfig


      
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}

2) RedisUtil


      
@Component
public class RedisUtil {
private final RedisTemplate<String, Object> redisTemplate;
public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void save(String key, Object val, Long time, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, val, time, timeUnit);
}
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public boolean delete(String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
}

 

참고 : Redis는 <Key : Value> 형태로 저장된다!

직접 터미널을 통해 보니, 이메일 에 RefreshToken이 Value로 매핑 되어 있는 것을 알 수 있다!

 

4. successfulAuthentication 구현

자... 리프레시 토큰을 저장할 RedisConfig 설정과, RedisUtil,

그리고 JwtUtil_토큰 발급 메서드 까지 만들었으니!

 

successfulAuthentication에서 토큰을 발급해 날려주면 끝!!!


      
// 스프링 시큐리티에 UsernamePasswordAuthenticationFilter 라는게 있음
// /login 요청해서 username,password 전송하면 (POST)
// UsernamePasswordAuthenticationFilter 필터가 작동함
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
// /login 요청을 하면, 로그인 시도를 위해서 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// 중략
}
// JWT Token 생성해서 response에 담아주기
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
log.info("[*] Login Success! - Login with " + principalDetails.getUsername());
JwtDto jwtDto = new JwtDto(
jwtUtil.createJwtAccessToken(principalDetails),
jwtUtil.createJwtRefreshToken(principalDetails)
);
log.info("Access Token: " + jwtDto.accessToken());
log.info("Refresh Token: " + jwtDto.refreshToken());
HttpResponseUtil.setSuccessResponse(response, HttpStatus.CREATED, jwtDto);
}
}

5. unsuccessfulAuthentication

사용자의 로그인이 실패했을 때 호출되는 메서드이다!

successfulAuthentication와 같은 단에 놓아주자.

(successfulAuthentication 아래)

 

인증 실패 사유에 따라 적절한 오류 메시지를 설정하여, 쏴주자!


      
// 스프링 시큐리티에 UsernamePasswordAuthenticationFilter 라는게 있음
// /login 요청해서 username,password 전송하면 (POST)
// UsernamePasswordAuthenticationFilter 필터가 작동함
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
// /login 요청을 하면, 로그인 시도를 위해서 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
log.info("JwtAuthenticationFilter : 로그인 시도 중");
// 중략
}
// JWT Token 생성해서 response에 담아주기
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
log.info("[*] Login Success! - Login with " + principalDetails.getUser().getUsername());
JwtDto jwtDto = new JwtDto(
jwtUtil.createJwtAccessToken(principalDetails),
jwtUtil.createJwtRefreshToken(principalDetails)
);
// 테스트용 (실제 사용 시 삭제)
log.info("Access Token: " + jwtDto.getAccessToken());
log.info("Refresh Token: " + jwtDto.getRefreshToken());
HttpResponseUtil.setSuccessResponse(response, HttpStatus.CREATED, jwtDto);
}
}

 

+ 참고 ApiResponse, 

API 응답을 표현하는 데 사용되는 클래스이다.

이 클래스는 클라이언트에게 HTTP 응답을 반환할 때 사용되며, 주로 JSON 형식으로 응답을 전송하는 데 사용된다.

 

HttpResponseUtill, ApiResponse 등. 구글링 해보면 정보가 많이 나올 것이다~~~~

끝!

자, JwtAuthenticationFilter 구현은 이렇게 끝이 났다!!!

 

6. SecurityConfig에 Filter 추가하기


      
@Configuration // IoC 빈(bean)을 등록
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
private final JwtUtil jwtUtil;
private final RedisUtil redisUtil;
// 중략
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 중략
// JWT login
// Jwt Filter (with login)
JwtAuthenticationFilter loginFilter = new JwtAuthenticationFilter(
authenticationManager(authenticationConfiguration), jwtUtil);
loginFilter.setFilterProcessesUrl("/login");
http
.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
  • 우리는 이전에 formLogin(AbstractHttpConfigurer::disable); 으로 formlogin 방식을 disable했다!

 

  • loginFilter.setFilterProcessesUrl("/login");
  • 즉, 로그인 페이지의 URL은 "/login"이며, 로그인을 처리하는 URL도 "/login"이고,
    로그인 후에는 원래 요청한 페이지로 리다이렉트된다!
  • ( 참고로 /login이 default 값이라, 사실 따로 설정하지 않아도 된다! 명시적으로 작성한 것 뿐.)
  • (다만, 로그인 요청 처리를 "/loginuser" 등으로 post 하는 걸로 바꾸고 싶다.면 해당 메서드에서 url을 변경하면된다!)

 

7. TEST

포스트 맨으로 테스트를 해보자!!

1) Success Case

body에 json 형식으로, 회원가입된 계정을 담아서

http://localhost:8080/login url로 post를 쏴보자!

DB에 사용자가 있는 경우

  • 1 줄 : /login 요청해서 username,password 전송하면 (POST), JwtAuthenticationFilter이 돌아가고,
            attemptAuthentication를 통해 로그인이 시도된다.
  • 2 줄 : authenticationManager.authenticate(authenticationToken); 과정에서
            PrincipalDetailsService의 loadUserByUsername() 함수가 실행되고 정상이면 authentication이 return된다.
  • 3 줄 : PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
             log.info("로그인 완료 : " + principalDetails.getUser().getUsername());
  • 4 줄 : successfulAuthentication 함수가 실행되고, 
            log.info("[*] Login Success! - Login with " + principalDetails.getUser().getUsername()); 코드 실행
  • 5 줄 : 테스트 용으로 token 로그 찍은 부분.

2) Error Case

DB에 사용자가 없는 경우

 

  • 1, 2 줄 : authenticationManager.authenticate(authenticationToken); 과정에서 PrincipalDetailsService의 loadUserByUsername() 함수가 실행되고 정상이면 authentication이 return된다.
  • 3 줄 : 그러나 "로그인 완료 : " 로그가 뜨지 않은 것을 보아, 정상적으로 authentication이 return되지 않은 것을 알 수 있다!
  • 3 줄 : 그리고 unsuccessfulAuthentication가 실행되었고,
            사용자가 제공한 자격 증명(예: 사용자 이름 또는 암호)이 유효하지 않은 경우에 발생하는
            BadCredentialsException이 발생했다!

 

다음 포스팅에서 "Authorization(인가) 과정에 대해 알아보겠다!

반응형
저작자표시 (새창열림)
  1. 1. successfulAuthentication 구현 전 참고해야할 내용
  2. 2.  토큰 발급 코드 작성
  3. 2-1. JwtDto
  4. 2-2. JwtUtil에 발급 코드 작성
  5. 3. RedisConfig 와 RedisUtil
  6. 1) RedisConfig
  7. 2) RedisUtil
  8. 4. successfulAuthentication 구현
  9. 5. unsuccessfulAuthentication
  10. + 참고 ApiResponse, 
  11. 6. SecurityConfig에 Filter 추가하기
  12. 7. TEST
  13. 1) Success Case
  14. 2) Error Case
'BackEnd' 카테고리의 다른 글
  • JWT 토큰 서버 구축하기 (인가/인증) (5) - RefreshToken으로 토큰 재발급
  • JWT 토큰 서버 구축하기 (인가/인증) (4) - 인가 처리 필터
  • SpringBoot에서 Redis 사용하기
  • [Database] Redis란?
dog-pawwer
dog-pawwer
성장 중 🌱🌱
dog-pawwer
지노개발일기
dog-pawwer
전체
오늘
어제
  • 분류 전체보기 (116) N
    • FrontEnd (4)
      • Android (4)
    • BackEnd (20)
    • Cloud (15)
    • Trouble Shooting (2) N
    • Computer Science (53)
      • CS 개인 공부 (20)
      • 알고리즘 (코딩테스트) (1)
      • 프로그래밍언어론 (15)
      • 분산시스템 (5)
      • 정보처리기사 (개인공부용) (3)
    • 강의 (18)
      • 자바-스프링부트-서버개발 (8)
      • UMC (Study) (9)
      • 스프링 부트와 JPA (1)
    • 🚨ERROR (4)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • GitHub

공지사항

인기 글

태그

  • 오어스
  • oauth
  • 스프링부트
  • 카카오 로그인 구현
  • 카카오
  • kakao
  • 카카오 로그인
  • java
  • springboot
  • 9-0
  • RestAPI

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.1
dog-pawwer
JWT 토큰 서버 구축하기 (인가/인증) (3) - AccessToken 발급
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.