Developer Note/국비과정 수업내용 정리&저장

24년 12월 16일

DH_PARK 2024. 12. 18. 02:33
@Slf4j
@Component
public class JwtTokenProvider {

    @Autowired
    private SignatureRepository signatureRepository;

    @Autowired
    private JWTTokenRepository jwtTokenRepository;

    @Autowired
    private HttpServletResponse response;

    @Autowired
    private UserRepository userRepository;

    //서명키 저장
    private Key key;
    public void setKey(Key key){
        this.key = key;
    }
    public JwtTokenProvider(){

    }
    @PostConstruct
    public void init(){

            List<Signature> list =  signatureRepository.findAll();
            if(list.isEmpty()){
                byte[] keyBytes = KeyGenerator.getKeygen();
                this.key = Keys.hmacShaKeyFor(keyBytes);
                Signature signature = new Signature();
                signature.setKeyBytes(keyBytes);
                signature.setCreateAt(LocalDate.now());
                signatureRepository.save(signature);
                System.out.println("JwtTokenProvider Constructor  최초 Key init: " + key);
            }else{
                Signature signature = list.get(0);
                byte[] keyBytes =signature.getKeyBytes();
                this.key = Keys.hmacShaKeyFor(keyBytes);
                System.out.println("JwtTokenProvider Constructor  DB Key init: " + key);
            }
    }

    // 유저 정보를 가지고 AccessToken, RefreshToken 을 생성하는 메서드
    public TokenInfo generateToken(Authentication authentication) {
        // 권한 가져오기
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();

        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
        UserDto userDto = principalDetails.getUserDto();

        // Access Token 생성
        Date accessTokenExpiresIn = new Date(now + JwtProperties.EXPIRATION_TIME); // 60초후 만료
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim("username",userDto.getUsername()) //정보저장
                .claim("auth", authorities)//정보저장
                .claim("principal", authentication.getPrincipal())//정보저장
                .claim("provider", userDto.getProvider())//정보저장
                .claim("providerId", userDto.getProviderId())//정보저장
                .claim("oauth2AccessToken", principalDetails.getAccessToken())//정보저장
                .claim("oauth2Attributes", principalDetails.getAttributes())//정보저장

//                .claim("credentials", authentication.getCredentials())//정보저장
//                .claim("details", authentication.getDetails())//정보저장

                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        // Refresh Token 생성
        Date refreshTokenExpiresIn = new Date(now + JwtProperties.EXPIRATION_TIME_REFRESH); // 60초후 만료
        String refreshToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim("username",userDto.getUsername()) //정보저장
                .setExpiration(refreshTokenExpiresIn)    //1일: 24 * 60 * 60 * 1000 = 86400000
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        //System.out.println("JwtTokenProvider.generateToken.accessToken : " + accessToken);
        //System.out.println("JwtTokenProvider.generateToken.refreshToken : " + refreshToken);

        return TokenInfo.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드
    public Authentication getAuthentication(String accessToken) {
        // 토큰 복호화
        Claims claims = parseClaims(accessToken);

        if (claims.get("auth") == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }
        // 클레임에서 권한 정보 가져오기
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get("auth").toString().split(","))
                        .map(auth -> new SimpleGrantedAuthority(auth))
                        .collect(Collectors.toList());

        String username = claims.getSubject(); //username

        LinkedHashMap principal_tmp = (LinkedHashMap)claims.get("principal");
        String provider  = (String)claims.get("provider");
        String providerId  = (String)claims.get("providerId");
        String auth = (String)claims.get("auth");
        String oauth2AccessToken = (String)claims.get("oauth2AccessToken");
        LinkedHashMap<String,Object> oauth2Attributes = (LinkedHashMap<String,Object>)claims.get("oauth2Attributes");
        //System.out.println("oauth2Attributes : " + oauth2Attributes);

//        String credentials = (String)claims.get("credentials");
//        LinkedHashMap details = (LinkedHashMap)claims.get("details");
//        System.out.println("principal_tmp : " + principal_tmp );
//        System.out.println("provider : " + provider );
//        System.out.println("credentials : " + credentials );
//        System.out.println("details : " + details );
        // UserDetails 객체를 만들어서 Authentication 리턴
        PrincipalDetails principalDetails = new PrincipalDetails();
        UserDto userDto = new UserDto();
        userDto.setUsername(username);
        userDto.setRole(auth);
        userDto.setProvider(provider);
        userDto.setProviderId(providerId);
        principalDetails.setUserDto(userDto);
        principalDetails.setAccessToken(oauth2AccessToken);
        principalDetails.setAttributes(oauth2Attributes);
//        System.out.println("JwtTokenProvider.getAuthentication UsernamePasswordAuthenticationToken : " + accessToken);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                new UsernamePasswordAuthenticationToken(principalDetails, claims.get("credentials"), authorities);
        return usernamePasswordAuthenticationToken;
    }

    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }

    // 토큰 정보를 검증하는 메서드
    public boolean validateToken(String token) throws IOException {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT Token", e);
        }
        catch (ExpiredJwtException e) {

            //log.info("Expired JWT Token", e);
            log.info("Expired JWT Token..");
            //RefreshToken 가져오기
            JWTToken tokenEntity = jwtTokenRepository.findByAccessToken(token);
            if(tokenEntity!=null){
                String refreshToken = tokenEntity.getRefreshToken();
                if(refreshToken!=null&&validateToken(refreshToken)){
                    // 토큰 만료 access x , refresh o
                    log.info("RefreshToken 유효함.. accessToken 다시발급");

                    //AccessToken 재발급---------------------------
                    String username = tokenEntity.getUsername();
                    Optional<User> userOptional =  userRepository.findById(username);
                    String accessToken=null;
                    if(userOptional.isPresent()){
                        User user = userOptional.get();
                        long now = (new Date()).getTime();
                        Date accessTokenExpiresIn = new Date(now + JwtProperties.EXPIRATION_TIME); // 60초후 만료
                        PrincipalDetails principalDetails = new PrincipalDetails();
                        UserDto userDto = new UserDto();
                        userDto.setUsername(user.getUsername());
                        userDto.setRole(user.getRole());
                        userDto.setProvider(user.getProvider());
                        userDto.setProviderId(user.getProviderId());
                        principalDetails.setUserDto(userDto);

                        accessToken = Jwts.builder()
                                .setSubject(user.getUsername())
                                .claim("username",user.getUsername()) //정보저장
                                .claim("auth", user.getRole())//정보저장
                                .claim("principal", principalDetails)//정보저장
                                .claim("provider", user.getProvider())//정보저장
                                .claim("providerId", user.getProviderId())//정보저장
                                .setExpiration(accessTokenExpiresIn)
                                .signWith(key, SignatureAlgorithm.HS256)
                                .compact();
                    }
                    //---------------------------
                    tokenEntity.setAccessToken(accessToken);
                    jwtTokenRepository.save(tokenEntity);
                    //COOKIE 재전달
                    Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME,tokenEntity.getAccessToken());
                    cookie.setMaxAge(JwtProperties.EXPIRATION_TIME);
                    cookie.setPath("/");
                    response.addCookie(cookie);
                    response.sendRedirect("/");
                    return true;
                }else{
                    // 토큰 만료 access x , refresh x
                    log.info("RefreshToken 만료!..");
                    ;
                }
            }else{
                // DB에 내용없음..
                log.info("DB에 JWT INFO  없음!..");
                ;
            }

        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims string is empty.", e);
        }
        return false;
    }
}

토큰이 있는지 확인 → 유효성 검사 후 토큰 확인. → 토큰이 있다면 클라이언트에 쿠키형태로 토큰을 전달

없다면 토큰DB를 삭제

package com.example.demo.controller;

import com.example.demo.config.auth.jwt.JwtProperties;
import com.example.demo.config.auth.jwt.JwtTokenProvider;
import com.example.demo.config.auth.jwt.TokenInfo;
import com.example.demo.domain.dto.UserDto;
import com.example.demo.domain.entity.JWTToken;
import com.example.demo.domain.entity.User;
import com.example.demo.domain.repository.JWTTokenRepository;
import com.example.demo.domain.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@RestController
@Slf4j
public class UserRestController {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JWTTokenRepository jwtTokenRepository;

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @PostMapping(value ="/join",produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<String> join_post(@RequestBody UserDto userDto){
        log.info("POST /join..."+userDto);

        //dto->entity
        User user = User.builder()
                .username(userDto.getUsername())
                .password( passwordEncoder.encode(userDto.getPassword())  )
                .role("ROLE_USER")
                .build();

        // save entity to DB
        userRepository.save(user);

        return new ResponseEntity<String>("success", HttpStatus.OK); //join 반환 데이터를 rest 하게 변경 , 프론트에서 사용해야 하므로 json 타입으로 전달

    }

    
    @PostMapping(value = "/login",consumes = MediaType.APPLICATION_JSON_VALUE,produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity login(@RequestBody UserDto userDto) throws IOException {
        log.info("포스트 로그인 !"+userDto);
        Map<String,Object> response = new HashMap<>();

        //1 접속중인 동일 계정이 있는지 확인
        JWTToken token = jwtTokenRepository.findByUsername(userDto.getUsername());
        if(token!=null){
            //1-1 엑세스 토큰 만료 여부
            //1-2 리프레시 토큰 만료 여부   //토큰이 있다면
            if(jwtTokenProvider.validateTokenAsync(token.getAccessToken())){  //엑세스 토큰이 만료되면 리프레시 토큰 확인 , 있다면 엑세스 토큰 재갱신
                //access token 을 클라이언트에게 전달 //토큰이 있다면
                JWTToken reToken = jwtTokenRepository.findById(token.getId()).get();
                response.put(JwtProperties.COOKIE_NAME, token.getAccessToken()); //쿠키 이름과 토큰값을 전달
                response.put("state","success"); 
                response.put("message","기존 로그인 정보가 존재합니다");
                return new ResponseEntity(response,HttpStatus.OK);
            }else{  //access 와 refresh 토큰이 만료되었다면  ,, 둘다 만료되었다면 DB에 있는 토큰 데이터 모두 삭제시키기
                jwtTokenRepository.deleteById(token.getId());
//                response.put("state","failed");
//                response.put("message","엑세스 토큰이 만료됨. 새로 로그인 하세요");    //지우고 새로 로그인하는 작업가니까 return 필요 X
//                return response;
            }
        }

        //토큰이 null (DB 에 없는상태)
        try{
            //사용자 인증 처리 ID PW 일치 여부
            Authentication authentication = //authentication 으로 이 내용을 받음
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(userDto.getUsername(),userDto.getPassword()) //ID PW 주면 위에 어덴트매니저가 확인을함
            );
            System.out.println("인증확인 : "+ authentication);

            TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication);
            System.out.println("JWT TOKEN : " + tokenInfo);

            JWTToken tokenEntity = new JWTToken();
            tokenEntity.setAccessToken(tokenInfo.getAccessToken());
            tokenEntity.setRefreshToken(tokenInfo.getRefreshToken());
            tokenEntity.setUsername(userDto.getUsername());
            tokenEntity.setIssuedAt(LocalDateTime.now());
            jwtTokenRepository.save(tokenEntity);

            //
            response.put("state","success");
            response.put("message","인증성공");
            response.put(JwtProperties.COOKIE_NAME,tokenEntity.getAccessToken());

        }catch(AuthenticationException e){
            System.out.println("인증실패 : " + e.getMessage());
            response.put("state","failed");
            response.put("message","인증실패");
            return new ResponseEntity(response,HttpStatus.UNAUTHORIZED);
        }

        return new ResponseEntity(response,HttpStatus.OK);
    }
}

Controller 코드.

DB에 토큰 테이블에 있는 토큰 정보 여부를 확인하여 리프레시 토큰 값이 있다면 엑세스코드를 재발급해주고 엑세스토큰과 리프레시 토큰값이 둘다 존재하지 않는다면 DB 에 있는 토큰 정보가 삭제되게끔 설정했다.


로컬 스토리지에 토큰 저장하기

import { useState,useEffect } from "react"
import axios from "axios"

const User = ()=>{

    useEffect(()=>{

        const token = localStorage.getItem('accessToken');
        try{
            axios.get
            ("<http://localhost:8090/user>",
            {headers:{"Authorization":`Bearer ${token}`,'Content-Type':'application/json'}})
            .then(resp=>{console.log(resp);})
            

        }catch(error){
            console.log(error);
        }

    },[])
    return (
        <h1>User Page</h1>

    )
}

export default User

위처럼 로컬스토리지에 엑세스토큰을 담아준다

 

그럼 이런식으로 로컬 스토리지에 엑세스 토큰값이 담기게 된다.


리액트의 useNavigate 란 ?

리액트 훅 중에 하나로 페이지 이동(라우팅)을 구현할 때 사용한다.

라우터 내에서만 작동하기 때문에 BrowserRouter 나 HashRouter 와 함께 사용해야 한다.

import React from "react";
import { useNavigate } from "react-router-dom";

function Home() {
  const navigate = useNavigate(); // useNavigate 훅 사용

  const goToAbout = () => {
    navigate("/about"); // "/about" 페이지로 이동
  };

  return (
    <div>
      <h1>Home Page</h1>
      <button onClick={goToAbout}>Go to About</button>
    </div>
  );
}

export default Home;

위 코드처럼 Home 함수를 사용하면 /about 경로로 이동하게 해준다.


오전에 했던거 정리

join에서 가입을 하면 위 username , password 에 상태값으로 저장

로그인을 하면 username , password 를 기반으로 엑세스 토큰을 발급


프로젝트 진행중 알게 된 지식

OncePerRequestFilter 란?

이름만 보자면 Http Request 의 한 번의 요청에 대해 한번만 실행하는 Filter이다.

Filter 와 Interceptor 의 차이

  • Filter :
    • 서블릿 API 의 일부. 요청 및 응답 객체를 조작하거나 필터링 하는데 사용됨
    • 일반적으로 서블릿이나 JSP 가 실행되기 전에 요청을 가로채거나 응답이 클라이언트로 전달되기 전에 가공하는 역할을 한다.
    • 순수 레거시 자바 환경에서 사용한다
  • Interceptor :
    • 인터셉트는 스프링이나 Hibernate 와 같은 특정 프레임워크에서 제공하는 기능으로 , 요청 전/후에 추가적인 처리를 수행한다.
    • 주로 Controller나 특정 메서드 호출 전/후에 동작한다.

주요 차이 : 서블릿 , 스프링 같은 사용하는 곳에서의 차이 ,

'Developer Note > 국비과정 수업내용 정리&저장' 카테고리의 다른 글

24년 12월 13일  (2) 2024.12.18
24년 12월 12일  (0) 2024.12.18
24년 12월 10일  (0) 2024.12.18
24년 12월 9일  (1) 2024.12.18
24년 12월 6일  (2) 2024.12.15