1. JwtUtil

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
    private final RefreshTokenRepository refreshTokenRepository;
    private final UserDetailsServiceImpl userDetailsService;
    private static final String BEARER_PREFIX = "Bearer ";
    private static final long ACCESS_TIME = 60 * 60 * 1000L; // ACCESS_TIME 1시간
    private static final long REFRESH_TIME = 7 * 24 * 60 * 60 * 1000L;  // REFRESH_TIME 7일
    public static final String ACCESS_TOKEN = "Access_Token";
    public static final String REFRESH_TOKEN = "Refresh_Token";
    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

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

    // header 토큰을 가져오는 기능
    public String getHeaderToken(HttpServletRequest request, String type) {
        return type.equals("Access") ? request.getHeader(ACCESS_TOKEN) :request.getHeader(REFRESH_TOKEN);
    }

    // 토큰 생성
    public TokenDto createAllToken(String email) {
        return new TokenDto(createToken(email, "Access"), createToken(email, "Refresh"));
    }

    public String createToken(String email, String type) {
        //현재 시각
        Date date = new Date();
        //지속 시간
        long time = type.equals("Access") ? ACCESS_TIME : REFRESH_TIME;

        return Jwts.builder()
                .setSubject(email)
                .setExpiration(new Date(date.getTime() + time))
                .setIssuedAt(date)
                .signWith(key, signatureAlgorithm)
                .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            throw new CustomException(LOGIN_WRONG_SIGNATURE_JWT_TOKEN);
        } catch (ExpiredJwtException e) {
            throw new CustomException(LOGIN_EXPIRED_JWT_TOKEN);
        } catch (UnsupportedJwtException e) {
            throw new CustomException(LOGIN_NOT_SUPPORTED_JWT_TOKEN);
        } catch (IllegalArgumentException e) {
            throw new CustomException(LOGIN_WRONG_FORM_JWT_TOKEN);
        } catch (SignatureException e) {
            throw new CustomException(SIGNATURE_EXCEPTION);
        }
    }

    // refreshToken 토큰 검증
    public Boolean refreshTokenValidation(String token) {

        // 1차 토큰 검증
        if(!validateToken(token)) return false;

        // DB에 저장한 토큰 비교
        Optional<RefreshToken> refreshToken = Optional.ofNullable(refreshTokenRepository.findByEmail(getUserInfoFromToken(token)));

        return refreshToken.isPresent() && token.equals(refreshToken.get().getRefreshToken());
    }

    // 토큰에서 loginId 가져오는 기능
    public String getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject();
    }

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

 

JwtUtil = 토큰에 관련된 암호화, 복호화, 검증 로직이 이루어지는 곳

1. init()

  • @PostConstruct 어노테이션은 특정 클래스의 메소드에 붙여서 해당 클래스의 객체 내 모든 의존성(Bean) 들이 초기화 된 직후에 딱 한 번만 실행되도록 해준다. 만약 객체에 의존성이 하나도 없더라도 실행된다.
  • WAS가 뜰 때 서비스가 생성되며 초기화 메소드가 실행된다.
  • 깃헙엔는 올라가지 않는 application-local.properties 파일에에 저장되어 있는 Jwt Secret Key를 복호화(디코딩 = 암호화의 반댓말)한다.

2. getHeaderToken()

  • 헤더부분에 타입에 따라 Access Token 과 Refresh Token을 가져오는 메소드
  • JwtAuthFilter 클래스에 사용된다.

3. createToken() / createAllTolen()

  • 유저 정보(email)를 넘겨받아서 Access Token 과 Refresh Token 을 생성하는 메소드
  • 타입에따라 각각 유효 시간을 저장해주고, Token Dto에 담아준다.
  • KakaoService, MemberService 에서 로그인한 후  response Header에 JWT 토큰 추가할 때 사용된다.

4. validateToken/refreshTokenValidation

  • 토큰 정보를 검증한다.
  • JwtAuthFilter의 doFilterInternal()에서 토큰 만료 여부를 확인할 때 사용된다.

5. getUserInfoFromToken()

  • 토큰에서 유저정보(email) 가져오는 메소드
  • JwtAuthFilter의 doFilterInternal()에서 토큰이 유효한 경우 토큰정보를 setAuthentication()메소드를 이용하여 Authentication에 저장해 줄 때 사용

6. createAuthentication()

  • 인증 객체를 생성하는 메소드
  • JwtAuthFilter의 setAuthentication() 에서 객체를 Authentication에 저장할 때 사용

 

2. JwtAuthFilter

@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
    public final JwtUtil jwtUtil;

    @Override //토큰 유효성 검사 후 Authentication 에 세팅
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String accessToken = jwtUtil.getHeaderToken(request, "Access");
        String refreshToken = jwtUtil.getHeaderToken(request, "Refresh");

        if (accessToken != null) {
            if (!jwtUtil.validateToken(accessToken)) {
                jwtExceptionHandler(response, "AccessToken Expired", HttpStatus.UNAUTHORIZED);
                return;
            }
            setAuthentication(jwtUtil.getUserInfoFromToken(accessToken));

        } else if (refreshToken != null) {
            if (!jwtUtil.refreshTokenValidation(refreshToken)) {
                jwtExceptionHandler(response, "RefreshToken Expired", HttpStatus.BAD_REQUEST);
                return;
            }
            // 3. 토큰이 유효하다면 토큰에서 정보를 가져와 Authentication 에 세팅
            setAuthentication(jwtUtil.getUserInfoFromToken(refreshToken));
        }
        // 4. 다음 필터로 넘어간다
        filterChain.doFilter(request, response);
    }

    public void setAuthentication(String email) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = jwtUtil.createAuthentication(email);
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    public void jwtExceptionHandler(HttpServletResponse response, String message, HttpStatus statusCode) {
        response.setStatus(statusCode.value());
        response.setContentType("application/json; charset=UTF-8");
        try {
            String json = new ObjectMapper().writeValueAsString(new GlobalResponseDto<>(StatusCode.BAD_REQUEST_TOKEN));
            response.getWriter().write(json);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

 

1. doFilterInternal()

  • 실제 필터링 로직을 수행하는 곳이다.
  • Request Header 에서 Access Token 과 Refresh Token을 꺼내온 후 토큰 유효 여부를 판단한다.
  • 토큰이 유효하면 Authentication에 넣어준다.
  • 가입/로그인/재발급을 제외한 모든 Request 요청은 이 필터를 거치기 때문에 토큰 정보가 없거나 유효하지 않으면 정상적으로 수행되지 않는다.

 

2. setAuthentication()

  • 토큰이 유효한 경우 토큰정보를 Authentication에 저장해 준다.

3. jwtExceptionHandler()

  • 토큰이 유효하지 않을 경우 핸들러를 통해 에러처리를 해준다.

 

3. Controller

@RestController
@RequiredArgsConstructor
@RequestMapping(produces = "application/json; charset=utf8")
public class MemberController {
    private final MemberService memberService;
    private final KakaoService kakaoService;

    // 로그인
    @PostMapping(value = "/auth/login")
    public ResponseEntity<?> login(@RequestBody SignupRequestDto signupRequestDto,
                                   HttpServletResponse response) {
        return ResponseUtil.response(memberService.login(signupRequestDto, response));
    }
    
    
    // 로그아웃
    @PostMapping("/auth/signout")
    public ResponseEntity<?> signout(@AuthenticationPrincipal UserDetailsImpl userDetails){
        return memberService.signout(userDetails.getMember().getEmail());
    }

    // 토큰 재발행
    @GetMapping("/auth/issue/token")
    public ResponseDto<String> issuedToken(@AuthenticationPrincipal UserDetailsImpl userDetails, HttpServletResponse response){
        return memberService.issuedToken(userDetails.getMember().getEmail(), response);
    }
}

 

4. Service

@RequiredArgsConstructor
@Service
@Slf4j
public class MemberService {
    private final PasswordEncoder passwordEncoder;
    private final RepositoryService repositoryService;
    private final JwtUtil jwtUtil;
    private final RefreshTokenRepository refreshTokenRepository;

    // 로그인
    @Transactional
    public MemberResponseDto login(SignupRequestDto signupRequestDto, HttpServletResponse response) {
        String email = signupRequestDto.getEmail();
        String password = signupRequestDto.getPassword();

        Member member = repositoryService.findMemberByEmail(email).orElseThrow(
                () -> new CustomException(StatusCode.LOGIN_MATCH_FAIL)
        );

        if (!passwordEncoder.matches(password, member.getPassword())) {
            throw new CustomException(StatusCode.BAD_PASSWORD);
        }

	// user email 값을 포함한 토큰 생성 후 tokenDto 에 저장
        TokenDto tokenDto = jwtUtil.createAllToken(signupRequestDto.getEmail());

        // user email 값에 해당하는 refreshToken 을 DB에서 가져옴
        Optional<RefreshToken> refreshToken = Optional.ofNullable(refreshTokenRepository.findByEmail(member.getEmail()));

        if (refreshToken.isPresent()) {
            refreshTokenRepository.saveRefreshToken(refreshToken.get().updateToken(tokenDto.getRefreshToken()));
        } else {
            RefreshToken newToken = new RefreshToken(tokenDto.getRefreshToken(), signupRequestDto.getEmail());
            refreshTokenRepository.saveRefreshToken(newToken);
        }

        setHeader(response, tokenDto);

        return new MemberResponseDto(member);
    }
    
    // 토큰 재발행
    public ResponseDto<String> issuedToken(String email, HttpServletResponse response){
        response.addHeader(JwtUtil.ACCESS_TOKEN, jwtUtil.createToken(email, "Access"));
        return ResponseDto.success("토큰재발행 성공");
    }
    
	// 로그인하면 Header에 토큰 담아주기
    private void setHeader(HttpServletResponse response, TokenDto tokenDto) {
        response.addHeader(JwtUtil.ACCESS_TOKEN, tokenDto.getAccessToken());
        response.addHeader(JwtUtil.REFRESH_TOKEN, tokenDto.getRefreshToken());
    }
    

    // 로그아웃
    public ResponseEntity<?> signout(String email) {
        // 해당 유저의 refreshtoken 이 없을 경우
        if(refreshTokenRepository.findByEmail(email) == null){
            throw new CustomException(StatusCode.INVALID_TOKEN);
        }
        // 자신의 refreshtoken 만 삭제 가능
        String memberIdrepo = refreshTokenRepository.findByEmail(email).getEmail();
        if(email.equals(memberIdrepo)){
            refreshTokenRepository.deleteRefreshToken(email);
            return ResponseUtil.response(StatusCode.SIGNOUT_OK);
        }else{
            return ResponseUtil.response(StatusCode.BAD_REFRESH_TOKEN);
        }
    }
}

 

 

※ 필요한 부분만 포스팅 했기 때문에 전체 코드는 아래 깃헙 링크를 참조해 주세요 ! ( 아직 수정중.... )

https://github.com/namoldak/Backend

 

GitHub - namoldak/Backend

Contribute to namoldak/Backend development by creating an account on GitHub.

github.com

 

 

1. 문제 상황 

  • 포스트맨으로 테스트를 했을 땐 잘 들어 가는데, 프론트에서 게시글 조회 하면 조회하는 이미지가 자꾸 엑박이 뜨고 아래와 같은 오류 메세지가 콘솔창에 떴다.  
  • Failed to load: resource: net::ERR_CERT_COMMON_NAME_INVALID
  • 확인해 보니 포스트맨에 나오는 이미지 url는 "https://namoldak.com.s3.ap-northeast-2.amazonaws.com/static/8c0ca99e-c76f-4ec5-90e1-8e6c4211" 라고 나오지만 AWS S3 버켓에서 해당 이미지 파일 주소를 확인했을 때는 "https://s3.ap-northeast-2.amazonaws.com/namoldak.com/static/8c0ca99e-c76f-4ec5-90e1-8e6c4211" 이렇게 다른 주소로 주소가 저장이 되어 있었다. 

2. 원인 

  • 버킷 이름에 .이 들어가면 Failed to load: resource: net::ERR_CERT_COMMON_NAME_INVALID 오류가 발생한다. 

 

3. 문제해결 

  • "namoldak.com" 인 버켓이름을 "namoldak"으로 바꿔 주었더니 바로 해결이 되었다 ! 

 


[ 참고 자료 ]

 

https://be-developer.tistory.com/85

 

[Spring Boot] aws s3 presignedUrl로 업로드 하는 법(One Time Token, OTT)

[Spring Boot] aws s3 presignedUrl로 업로드 하는 법 회사에서 이미지 업로드시 서버의 I/O 부하를 줄이기 위해 클라이언트에서 바로 aws의 S3에 업로드를 할 수 있도록 presignedUrl을 내리라는 미션을 받았다

be-developer.tistory.com

 

 

 

1. 어려웠던 부분 

  • 오늘 Redis에 저장하고 있는 Refresh Token 유효기간을 설정하는 코드를 작성했다. 구글링을 통해 레디스의 TTL(TimeToLive) 기능을 활용하면 유효시간이 지났을 때 자동삭제된다고 보았는데, 코드를 작성해도 DB에서 삭제 되지 않았다. 결국 RedisTemplate 오퍼레인 사용하는 방법으로 해결하긴 했는데, 다른조 분에게 물어보니 아마 CrudRepository를 이요해야 TTL기능을 사용할 수 있는 것 같다는 이야기를 들었다. 내일 이부분을 다시 수정해 봐야겠다.
  • 커뮤티니 게시판에 AWS S3를 활용하여 이미지 파일을 저장하고 불러오고 있는데, 포스트맨에 나오는 이미지 url과 실제 S3에 저장되는 url이 일치하지 않아 이미지르 불러오지 못하는 오류가 발생했다. 팀원들과 오랜 삽질 끝에 오늘도 역시 해결사 소영님이 해결해 주셨다. 이유는 우리 버켓이름이 namoldak.com 으로 설정되어 있었는데 버켓에 "."이 들어가서 제대로 인식하지 못했던 것이다 ! 

 

2. 느낀 점 : 

  • 문제를 척척 해결하는 팀원 분들을 보며, 난 정말 부족한게 많다는 생각을 했다. 부럽기도하고 또 한편으론, 난 코딩에 소질이 없는 것 같다는 생각도 들었다. 

 

3. 새로 알게 된 내용 :

  • Redis에서 만료 시간을 설정하는 방법은 RedisTemplate 오퍼레이션 사용시 만료 시간을 설정하는 법과 CrudRepository를 이용하여 저장할 객체에 timeToLive를 설정하는 방법 두 가지가 있는데, RedisTemplate만을 이용하면서 객체에 timeToLive를 적용해 제대로 적용되지 않는다. 제대로 적용하려면 CrudRepository를 사용해 timeToLive를 사용해야 한다.

 

4. 셀프칭찬 (오늘 잘한 일) 

  • 매일 하나씩 꼭 있는 트러블 슈팅을 통해 새로운 것들을 배우고 있다. 오늘도 트러블 슈팅을 통해 두가지나 알게 되었다 !

 

5. 내일 할 일 : 피드백 반영해서 코드 수정하기, 시그널링 서버에서 여러 session 충돌로 연결이 끊어지는 이슈 해결하기


[오늘 공부한 부분] 

 

[22] ubuntu 서버에 Redis 설치하기

현재 구현하고 있는 서비스에 Redis를 사용하고 있기 때문에 사용하는 ubuntu 서버에 Redis를 설치해 줘야만 애플리케이션이 제대로 동작한다. 설치 순서는 아래와 같다 1. ubuntu 서버에 아래 redis 설

leejincha.tistory.com

 

현재 구현하고 있는 서비스에 Redis를 사용하고 있기 때문에 사용하는 ubuntu 서버에 Redis를 설치해 줘야만 애플리케이션이 제대로 동작한다. 설치 순서는 아래와 같다

 

1. ubuntu 서버에 아래 redis 설치 명령어 입력 

sudo apt-get install redis-server

 

 

 

2. 아래 명령어로 redis.conf 파일을 열어서 bind 부분을 0.0.0.0 ::1 로 수정. (원래는 127.0.0.1::1 로 설정되어있음)

 sudo nano /etc/redis/redis.conf

 

 

 

 

3. control + x 누르면 아래와 같이 뜨는데, Y를 입력하고 엔터를 치면 설치 및 환경 설정 완료 ! 

 

1. 어려웠던 부분 

  • 우리팀은 계속해서 CD 과정에 오류를 겪고 있었다. 우리는 application-local.properties 파일에 깃헙이나 외부에 노출되면 안되는 정보들을 담아주고 있고, 이 파일을 깃헙에서 Secrets 파일로 설정해 놓았다. 

  • 그런데, CD를 하는 과정에서 계속 이 properties 파일이 포함되지 않고 빠지는 오류가 있었다. 이 부분을 우리팀 트러블슈팅 해결사 소영님이 해결해 주셨다. 아래는 우리 프로젝트의 gradle.yml파일 일부분이다.
  • 오류가 났던 경우엔 키 정보 있는 properties 부분이 #build 부분 아래에 위치하고 있었다. 즉, 빌드를 다 하고 나서야 암호화된 부분이 작동하기 때문에 빌드에 반영이 되지 않았던 것이다.
      # 키 정보 있는 properties 따로 관리
      - name: make application.properties
        if: true
        run: |
          cd ./src/main/resources
          touch ./application.properties
          echo "${{ secrets.PROPERTIES }}" > ./application.properties
        shell: bash

      # gradlew 권한 설정
      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
        shell: bash

      # build
      - name: Build with Gradle
        run: ./gradlew clean build -x test
        shell: bash
  • 아래는 소영님 블로그에서 발췌한 부분이다. 

📍gradle.yml 수정 (메인이되는 yml 파일)

yml 파일에서 jobs > steps 부분에서 아래 코드를 추가해야하는 데 반드시 build과정 전에! 이 코드가 있어야한다. 

깃헙 프로젝트 배포 과정에서 application-secret.properties를 포함시켜준 뒤 빌드를 하고 그 jar를 복사해 EC2에서 실행해야 하기 때문. 빌드 뒤에 들어가버리면 jar파일에서 빠져버려 정상 작동하지 않는다.

 

2. 느낀 점 : 

  • 드디어 CD 까지 성공하였다 ! 일주일 내내 고생하신 팀원분들에 너무 감사하다. 항해99 솔직히, 부트캠프에서 제공해주는 자료를 통해 배우는 것보다 팀원분들과 반 분들에게 배운게 더 많은 것 같다. 

 

3. 새로 알게 된 내용 :

  • gradle.yml 파일의 경우, 프로젝트마다 customized 되기 때문에 구글링을 통해 코드를 긁어오기보다 맡은 프로젝트의 성격에 맞게 새로  추가되는 부분이 많았다. 주의할 점은 이 파일의 코드는 위에서부터 아래로 실행 된다는 것이다. 따라서 순서를 유의해서 작성해야 한다.

 

4. 셀프칭찬 (오늘 잘한 일) 

  • 오늘도 부족한 나는 천천히 내 속도에 맞춰서 앞으로 가는 중!

 

5. 내일 할 일 : 리프레시 토큰 유효기간 설정하는 코드 추가하기


[오늘 공부한 부분]

 

[20] Spring CI/CD (2) CI 실전편

 

[20] Spring CI/CD (2) CI 실전편

지난 번에 개념을 공부했으니 오늘은 Github을 이용하여 CI를 구현하는 방법을 포스팅 : ) 미래의 나를 위한 포스팅이다. 1. Actions -> new workflow -> Java with Gradle [configure] 클릭 2. Edit new file 에 코드 추

leejincha.tistory.com

 

+ Recent posts