반응형

Redis 설치하기

 

Redis 설치 방법

Redis NoSQL DB의 한 종류이며 우리가 흔히 사용하는 MYSQL, Orcal DB, PostgreSQL 등 RDBMS와 다르게 NoSQL DB이다. 그렇다면 무슨 차이이고 어느 상황에 사용해야 할까? RDBMS와 NoSQL의 차이 RDBMS (관계형 DB) - 데

ngwdeveloper.tistory.com

 

 

 

Refresh Token의 목적

- Access Token의 유효기간을 짧고, 자주 재발급하도록 만들어 보안을 강화하면서도 사용자에게 잦은 로그아웃 경험을 주지 않도록 하는 목적으로 만들어졌다. Access Token은 리소스에 접근하기 위해서 사용되는 토큰이라면, Refresh Token은 기존에 클라이언트가 가지고 있던 Access Token이 만료되었을 때 Access Token을 새로 발급받기 위해 사용한다.

 

 

 

Refresh Token의 유효기간

- Refresh Token은 Access Token 대비 긴 유효기간을 갖는다. Refresh TOken을 사용하는 상황에서는 일반적으로 Access Token의 유효기간은 30분~1시간 이내, Refresh Token의 유효기간은 2주 정도로 설정한다고 한다. 서비스 성격에 따라 적절한 유효기간을 설정해야 한다.

 

프로젝트마다 JWT 설정방식이나 설정에 차이가 있을 수 있다.

*필요한 부분만 찾아서 사용.

 

 

 

1. JwtUtil.java

package com.gunwook.jpeople.security.jwt;

import com.gunwook.jpeople.redis.RefreshToken;
import com.gunwook.jpeople.user.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Key;

import java.util.Base64;
import java.util.Date;

@Component
public class JwtUtil {
    // Header KEY 값
    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String REFRESH_HEADER = "RefreshToken";

    // 사용자 권한 값의 KEY
    public static final String AUTHORIZATION_KEY = "auth";

    // Token 식별자
    public static final String BEARER_PREFIX = "Bearer ";

    // 토큰 만료시간
    private final long ACCESS_TOKEN_TIME = 60 * 60 * 1000L; // 1시간
    private final long REFRESH_TOKEN_TIME = 60 * 60 * 24 * 1000L; // 24시간

    @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 로그 설정
    public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // 토큰 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username) // 사용자 식별자값(ID)
                        .claim(AUTHORIZATION_KEY, role)
                        .setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME)) // 만료 시간
                        .setIssuedAt(date) // 발급일
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact();
    }

    // 리프레시 토큰 생성
    public RefreshToken createRefreshToken(String username, UserRoleEnum role){
        Date date = new Date();

        return new RefreshToken(username, BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username) // 식별자값 (사용자 ID)
                        .claim(AUTHORIZATION_KEY, role) // 사용자 권한
                        .setExpiration(new Date(date.getTime() + REFRESH_TOKEN_TIME)) // 만료 시간
                        .setIssuedAt(date) // 발급날짜
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact());
    }

    // JWT Cookie 에 엑세스 토큰 저장
    public void addJwtToCookie(String token, HttpServletResponse res) {
        token = URLEncoder.encode(token, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

        Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // 쿠키 생성
        cookie.setPath("/");
        cookie.setMaxAge(60*60); // 1시간
//        cookie.setSecure(true);
        res.addCookie(cookie);
    }

    // JWT Cookie 에 리프레시 토큰 저장
    public void addJwtToCookieRefreshToken(String refreshToken, HttpServletResponse res){
        refreshToken = URLEncoder.encode(refreshToken, StandardCharsets.UTF_8).replaceAll("\\+", "%20");

        Cookie cookie = new Cookie(REFRESH_HEADER, refreshToken);
        cookie.setPath("/");
        cookie.setMaxAge(60*60*24); // 24시간
        cookie.setHttpOnly(true);
//        cookie.setSecure(true);
        res.addCookie(cookie);
    }

    // JWT 토큰 substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException
                 | SignatureException
                e) {
            logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            logger.error("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }

    // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }


    // 사용자 권한 가져오기
    public UserRoleEnum getUserRole(Claims info) {
        String roleValue = info.get(AUTHORIZATION_KEY).toString();
        return roleValue.equals("USER") ? UserRoleEnum.USER : UserRoleEnum.ADMIN;
    }

    // HttpServletRequest 에서 Cookie Value : JWT 가져오기
    public String getTokenFromRequest(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if(cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                    try {
                        return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
                    } catch (UnsupportedEncodingException e) {
                        return null;
                    }
                }
            }
        }
        return null;
    }

    // HttpServletRequest 에서 Cookie Value : JWT Refresh Token 가져오기
    public String getRefreshTokenFromRequest(HttpServletRequest req){
        Cookie[] cookies = req.getCookies();
        if(cookies != null){
            for(Cookie cookie : cookies){
                if(cookie.getName().equals(REFRESH_HEADER)){
                    return URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8);
                }
            }
        }
        return null;
    }

    //로그아웃 쿠키 날짜를 0으로 만들어 만료시킴
    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return;
        }
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals(AUTHORIZATION_HEADER)
            | cookie.getName().equals(REFRESH_HEADER)){
                cookie.setValue(""); // Clear the value of the cookie
                cookie.setPath("/");
                cookie.setMaxAge(0);
                response.addCookie(cookie);
            }
        }
    }
}

 

 

 

2. JwtAuthenticationFilter.java

package com.gunwook.jpeople.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gunwook.jpeople.redis.RefreshToken;
import com.gunwook.jpeople.redis.RefreshTokenRepository;
import com.gunwook.jpeople.security.jwt.JwtUtil;
import com.gunwook.jpeople.user.dto.LoginRequestDto;
import com.gunwook.jpeople.user.entity.UserRoleEnum;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;

import java.io.IOException;

@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;
    private final RefreshTokenRepository refreshTokenRepository;

    public JwtAuthenticationFilter(JwtUtil jwtUtil, RefreshTokenRepository refreshTokenRepository) {
        this.refreshTokenRepository = refreshTokenRepository;
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/api/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String AccessTokenValue = jwtUtil.getTokenFromRequest(request);
        String RefreshTokenValue = jwtUtil.getRefreshTokenFromRequest(request);

        if (StringUtils.hasText(AccessTokenValue) || StringUtils.hasText(RefreshTokenValue)) {
            jwtUtil.deleteCookie(request,response);
        }

        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
            //log.info(requestDto.getUsername() + requestDto.getPassword());
            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
        log.info("로그인 성공 및 JWT 생성");
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        // 엑세스 토큰 발급, 쿠키에 저장
        String accessToken = jwtUtil.createToken(username, role);
        jwtUtil.addJwtToCookie(accessToken, response);

        // 리프레시 토큰 발급, Redis에 저장
        RefreshToken refreshToken = jwtUtil.createRefreshToken(username, role);
        refreshTokenRepository.save(refreshToken);
        jwtUtil.addJwtToCookieRefreshToken(refreshToken.getRefreshToken(), response);

        log.info("로그인 성공");
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        log.info("로그인 실패");
        response.setStatus(401);
    }

}

 

 

 

 

3. JwtAuthorizationFilter.java

package com.gunwook.jpeople.security;

import com.gunwook.jpeople.redis.RedisService;
import com.gunwook.jpeople.security.jwt.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
@Slf4j(topic = "JWT 검증 및 인가")
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final RedisService redisService;
    private final UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, IOException {

        log.info("doFilterInternal");
        String AccessTokenValue = jwtUtil.getTokenFromRequest(request);
        String RefreshTokenValue = jwtUtil.getRefreshTokenFromRequest(request);

        // 로그인 페이지 요청 시 -> 토큰 모두 삭제
        if (request.getRequestURI().equals("/api/view/login")) {
            jwtUtil.deleteCookie(request, response);
        } else {
            // 엑세스 토큰 유효할 경우
            if (StringUtils.hasText(AccessTokenValue)) {
                AccessTokenValue = jwtUtil.substringToken(AccessTokenValue);
                if (jwtUtil.validateToken(AccessTokenValue)) {
                    Claims info = jwtUtil.getUserInfoFromToken(AccessTokenValue);
                    try {
                        setAuthentication(info.getSubject());
                    } catch (Exception e) {
                        log.error(e.getMessage());
                        return;
                    }
                }

            } else if(StringUtils.hasText(RefreshTokenValue)){
                // 엑세스 토큰은 유효하지 않으나, 리프레시 토큰이 존재할 경우
                log.error("엑세스 토큰이 없습니다. 하지마 리프레시 토큰은 존재합니다.");
                createNewAccessToken(request, response, RefreshTokenValue);
                return;
            } else{
                log.info("로그인이 안되어 있으면 403 에러가 발생할 수 있습니다.");
            }
        }

        filterChain.doFilter(request, response);
    }

    // 리프레시 토큰을 검증, 엑세스 토큰 재발급 메서드
    private void createNewAccessToken(HttpServletRequest request, HttpServletResponse response,
                                      String refreshTokenValue) throws IOException{
        refreshTokenValue = jwtUtil.substringToken(refreshTokenValue);
        redisService.generateAccessToken(request, response);
        response.sendRedirect(request.getRequestURI());
    }

    // 인증 처리
    public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

    // 인증 객체 생성
    private Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }

}

 

 

 

4. WebSecurityConfig.java

package com.gunwook.jpeople.config;

import com.gunwook.jpeople.redis.RedisService;
import com.gunwook.jpeople.redis.RefreshTokenRepository;
import com.gunwook.jpeople.security.JwtAuthenticationFilter;
import com.gunwook.jpeople.security.JwtAuthorizationFilter;
import com.gunwook.jpeople.security.UserDetailsServiceImpl;
import com.gunwook.jpeople.security.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;
    private final RedisService redisService;
    private final RefreshTokenRepository refreshTokenRepository;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManger(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil, refreshTokenRepository);
        filter.setAuthenticationManager(authenticationManger(authenticationConfiguration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, redisService, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        // 토큰이 없어도 접근가능하도록
        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/").permitAll()
                        .requestMatchers("/view/**").permitAll() // viewLoad URI 요청 모두 허가
                        .requestMatchers("/api/**").permitAll() // 회원가입, 로그인으로 시작하는 요청 모두 접근 허가
                        .requestMatchers("/api/cards/**").permitAll() // 회원가입, 로그인으로 시작하는 요청 모두 접근 허가
                        .requestMatchers(HttpMethod.GET,"/api/user/*/callback").permitAll()
                        .requestMatchers(HttpMethod.POST,"/api/login").permitAll()
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );



        // 필터 관리
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

 

 

 

5. UserController.java

package com.gunwook.jpeople.user.controller;


import com.gunwook.jpeople.security.UserDetailsImpl;
import com.gunwook.jpeople.user.dto.SignUpRequestDto;
import com.gunwook.jpeople.user.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class UserController {

    private final UserService userService;

    /**
     * 회원가입
     * @param signUpRequestDto
     * @return 회원가입 성공/실패 여부
     */
    @PostMapping("/signup")
    ResponseEntity<String> signUp(@RequestBody @Valid SignUpRequestDto signUpRequestDto){
        String result = userService.signUp(signUpRequestDto);
        return ResponseEntity.ok(result);
    }

    /**
     * 로그아웃
     * @param request 요청 servlet
     * @param response 응답 servlet
     * @return 로그아웃 성공/실패 여부
     */
    @DeleteMapping("/logout")
    ResponseEntity<String> logout(HttpServletRequest request, HttpServletResponse response,
                                  @AuthenticationPrincipal UserDetailsImpl userDetails){
        String result = userService.logout(request, response, userDetails.getUser());
        return ResponseEntity.ok(result);
    }

    /**
     * 만료 전 access token 재발급 메서드
     * @param request 요청 Servlet
     * @param response 응답 Servlet
     * @return http status code 와 토큰 재발급 성공 여부
     */
    @GetMapping("/refresh/access-token")
    public ResponseEntity<String> generateRefreshToken(HttpServletRequest request, HttpServletResponse response){
        boolean result = userService.generateAccessToken(request, response);

        if(result){
            return ResponseEntity.ok("엑세스 토큰 생성 성공");
        } else{
            return ResponseEntity.badRequest().body("엑세스 토큰 생성 실패");
        }
    }
}

 

 

 

6. UserService.java

package com.gunwook.jpeople.user.service;


import com.gunwook.jpeople.redis.RefreshToken;
import com.gunwook.jpeople.redis.RefreshTokenRepository;
import com.gunwook.jpeople.security.jwt.JwtUtil;
import com.gunwook.jpeople.user.dto.SignUpRequestDto;
import com.gunwook.jpeople.user.entity.User;
import com.gunwook.jpeople.user.entity.UserRoleEnum;
import com.gunwook.jpeople.user.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    private final RefreshTokenRepository refreshTokenRepository;
    private final JwtUtil jwtUtil;

    @Value("${signup.admin.key}")
    private String ADMIN_TOKEN;

    public String signUp(SignUpRequestDto signUpRequestDto) {
        // 중복 체크
        if(userRepository.existsByUsername(signUpRequestDto.getUsername())){
            throw new IllegalArgumentException("이미 회원가입된 사용자 입니다.");
        }

        // 패스워드 암호화
        String password = passwordEncoder.encode(signUpRequestDto.getPassword());

        // 권한 체크
        UserRoleEnum role = UserRoleEnum.USER;
        if(StringUtils.hasText(signUpRequestDto.getAdminToken())) {
            if (!ADMIN_TOKEN.equals(signUpRequestDto.getAdminToken())) {
                throw new IllegalArgumentException("관리자 암호가 틀렸습니다.");
            }
            role = UserRoleEnum.ADMIN;
        }

        // 유저 생성
        User user = new User(signUpRequestDto, password, role);
        userRepository.save(user);

        return "회원가입 성공";

    }


    public String logout(HttpServletRequest request, HttpServletResponse response, User user) {
//        // 세션 종료
//        HttpSession session = request.getSession(false);
//        if(session != null){
//            session.invalidate();
//        }
//
//        // 쿠키 삭제
//        Cookie[] cookies = request.getCookies();
//        if(cookies != null){
//            for (Cookie cookie : cookies){
//                cookie.setMaxAge(0);
//                response.addCookie(cookie);
//            }
//        }
        Boolean result = refreshTokenRepository.delete(user.getUsername());
        if(!result){
            throw new IllegalArgumentException("리프레시 토큰을 삭제할 수 없습니다.");
        }
        jwtUtil.deleteCookie(request, response);
        return "로그아웃 성공";
    }

    // 엑세스 토큰 갱신 메서드
    public Boolean generateAccessToken(HttpServletRequest request, HttpServletResponse response){
        // 클라이언트 쿠키에서 refresh token 추출
        String InputRefreshToken = jwtUtil.getRefreshTokenFromRequest(request);
        String InputRefreshTokenValue = jwtUtil.substringToken(InputRefreshToken);

        // refresh token 이 없을 경우 예외 처리
        if(!StringUtils.hasText(InputRefreshToken)){
            log.error("리프레시 토큰이 존재하지 않습니다. 로그인 해주세요.");
            return false;
        }

        // refresh token 유효성 검사 불일치
        if(!jwtUtil.validateToken(InputRefreshTokenValue)){
            log.error("리프레시 토큰이 유효하지 않습니다.");
            jwtUtil.deleteCookie(request, response);
            return false;
        }

        // 유저 정보 추출
        Claims claims = jwtUtil.getUserInfoFromToken(InputRefreshTokenValue);
        String username = claims.getSubject();
        UserRoleEnum role = jwtUtil.getUserRole(claims);

        // Redis 의 refresh token 일치 여부 판단
        RefreshToken refreshToken = refreshTokenRepository.findByUsername(username).get();
        if(InputRefreshToken.equals(refreshToken.getRefreshToken())){
            createAccessToken(response, username, role);
            return true;
        }
        return false;
    }

    // 엑세스 토큰 발급
    private void createAccessToken(HttpServletResponse response, String username, UserRoleEnum role){
        // 엑세스 토큰 발급, 쿠키에 저장
        String accessToken = jwtUtil.createToken(username, role);
        jwtUtil.addJwtToCookie(accessToken, response);
    }




}

 

 

 

7. RedisCacheConfig.java

package com.gunwook.jpeople.redis;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public CacheManager testCacheManager(RedisConnectionFactory cf){
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofMinutes(5L)); // 3분

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf)
                .cacheDefaults(redisCacheConfiguration).build();
    }

}

 

 

 

8. RedisConfig.java

package com.gunwook.jpeople.redis;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Getter
@Configuration
@RequiredArgsConstructor
@EnableRedisRepositories
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    // 내장 / 외부 Redis 연결
    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        return new LettuceConnectionFactory(host, port);
    }

    /*
     * RedisConnection 에서 넘겨준 byte 값을 직렬화 RedisTemplate 은
     * Redis 데이터를 저장하고 조회하는 기능을 하는 클래스 REdis cli 를 사용해 Redis 데이터를 직접 조회할때,
     * Redis 데이터를 문자열로 반환하기 위한 설정
     */
    @Bean
    public RedisTemplate<String, String> redisTemplate(){
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }





}

 

 

9. RedisService.java

package com.gunwook.jpeople.redis;

import com.gunwook.jpeople.security.jwt.JwtUtil;
import com.gunwook.jpeople.user.entity.UserRoleEnum;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class RedisService {

    private final RefreshTokenRepository refreshTokenRepository;
    private final JwtUtil jwtUtil;

    /**
     * 엑세스 토큰 재발급 메서드
     * @param request 요청 Servlet
     * @param response 응답 Servlet
     */
    public Boolean generateAccessToken(HttpServletRequest request, HttpServletResponse response) {
        // 클라이언트 쿠키에서 refresh token 추출
        String InputRefreshToken = jwtUtil.getRefreshTokenFromRequest(request);
        String InputRefreshTokenValue = jwtUtil.substringToken(InputRefreshToken);

        // refresh token 유효성 검사 불일치
        if (!jwtUtil.validateToken(InputRefreshTokenValue)) {
            log.error("Refresh Token does not valid.");
            jwtUtil.deleteCookie(request, response);
            return false;
        }

        // 유저 정보 추출
        Claims claims = jwtUtil.getUserInfoFromToken(InputRefreshTokenValue);
        String username = claims.getSubject();
        UserRoleEnum role = jwtUtil.getUserRole(claims);

        // Redis 의 리프레시 토큰과 일치 여부 판단
        RefreshToken refreshToken = refreshTokenRepository.findByUsername(username).get();
        if (InputRefreshToken.equals(refreshToken.getRefreshToken())) {
            // 엑세스 토큰 생성
            createAccessToken(response, username, role);
            return true;
        }
        return false;
    }

    /**
     * 리프레시 토큰 삭제
     * @param request 요청 Servlet
     * @param response 응답 Servlet
     */
    public void deleteRefreshToken(HttpServletRequest request, HttpServletResponse response){
        // 클라이언트 쿠키에서 refresh token 추출
        String InputRefreshToken = jwtUtil.getRefreshTokenFromRequest(request);
        String InputRefreshTokenValue = jwtUtil.substringToken(InputRefreshToken);

        // 유저 정보 추출
        Claims claims = jwtUtil.getUserInfoFromToken(InputRefreshTokenValue);
        String username = claims.getSubject();

        refreshTokenRepository.delete(username);
        jwtUtil.deleteCookie(request,response);
        response.setStatus(HttpStatus.OK.value());
    }

    private void createAccessToken(HttpServletResponse response, String username, UserRoleEnum role) {
        // access token 발급 및 쿠키에 저장
        String accessToken = jwtUtil.createToken(username, role);
        jwtUtil.addJwtToCookie(accessToken, response);
    }

}

 

 

10. RefreshToken.java

package com.gunwook.jpeople.redis;

import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
@AllArgsConstructor
public class RefreshToken {
    @Id
    private String username; // email id 값 저장
    private String refreshToken;
}

 

 

11. RefreshTokenRepository.java

package com.gunwook.jpeople.redis;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Repository
@RequiredArgsConstructor
public class RefreshTokenRepository {
    private final RedisTemplate redisTemplate;

    // 리프레시 토큰 만료시간
    private final long REFRESH_TOKEN_TIME = 60 * 60 * 24; // 24시간

    public void save(final RefreshToken refreshToken){
        ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(refreshToken.getUsername(),refreshToken.getRefreshToken());
        redisTemplate.expire(refreshToken.getUsername(), REFRESH_TOKEN_TIME, TimeUnit.SECONDS);
    }

    public Boolean delete(String username) {
        return redisTemplate.delete(username);
    }

    public Optional<RefreshToken> findByUsername(final String username) {
        ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
        String keyValue = String.valueOf(valueOperations.get(username));

        if (Objects.isNull(keyValue)) {
            return Optional.empty();
        }

        return Optional.of(new RefreshToken(username, keyValue));
    }



}
반응형

+ Recent posts