82일차 내용 정리

채공부·2025년 9월 17일

JWT

Spring10_JWT 프로젝트 생성

  • pom.xml 에 dependency 추가
<!-- Json Web Token 관련 라이브러리 -->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-api</artifactId>
	<version>0.11.5</version>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-impl</artifactId>
	<version>0.11.5</version>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-jackson</artifactId>
	<version>0.11.5</version>
	<scope>runtime</scope>
</dependency>

패키지 생성

  1. spring10.config
  2. spring10.controller
  3. spring10.dto
  4. spring10.filter
  5. spring10.service
  6. spring10.util
  • UserDto 클래스 생성
    • @AllArgsConstructor : 모든 필드값을 전달받는 생성자
      ➜ Builder 를 사용하려면 필요
@Builder
@AllArgsConstructor
@NoArgsConstructor // 빈 생성자
@Data // setter & getter
public class UserDto {
	private long num;
	private String userName;
	private String password;
	private String email;
	private String role;
	private String profileImage;
	private String createdAt;
	private String updatedAt;
	// <input type="file" name="profileImage"> 을 처리하기 위한 필드
	private MultipartFile profileFile;
}
  • service 패키지 안에 CustomUserDetailsService 클래스 생성
    • @Service : bean 으로 만들기 위한 어노테이션
@Service
public class CustomUserDetailsService implements UserDetailsService{

	// userName 을 전달하면 해당 user 의 자세한 정보를 리턴하는 메소드
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// 원래는 DB 에서 dao 를 이용해 username 에 해당하는 사용자정보(UserDto) 를 얻어와야 한다.
		// 지금은 sample 데이터를 만들어서 사용하기
		
		// 실제 DB 에는 ROLE_XXX 형식으로 저장이 되어 있어야 한다 
		String role = "";
		if(username.equals("kimgura")) {
			role = "ROLE_USER";
		}else if(username.equals("batman")) {
			role = "ROLE_STAFF";
		}else if(username.equals("superman")){
			role = "ROLE_ADMIN";
		}
		
		// 비밀번호를 암호화 하기 위한 객체 
		BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
		// DB 에는 암호화된 비밀번호가 저장되어 있다고 가정하자 (암호화 하기 이전의 비밀번호가 1234 라고 가정)
		String encodedPwd = encoder.encode("1234");
		
		// Sample UserDto 객체 만들기(원래는 DB 에서 읽어온 데이터)
		UserDto dto=UserDto.builder()
					.userName(username)
					.password(encodedPwd)
					.email("aaa@naver.com")
					.role(role)
					.build();
		/*
		 * 	Spring Security 에서 role 과 authority 는 약간의 차이가 있지만 편의상 같다라고 생각하자
		 * 
		 *  ROLE_USER 를 예를 들면 
		 *  
		 *  해당 USER 의 role 은 "USER" 
		 *  해당 USER 의 authority 는 "ROLE_USER" 임을 명심하자...  
		 *  
		 */
		// 권한 목록을 List 에 담아서  (지금은 1개 이지만)
		List<GrantedAuthority> authList = new ArrayList<>();
		// Role 을 포함한 authoryty 객체를 생성해서 authList 에 추가 
		authList.add(new SimpleGrantedAuthority(dto.getRole()));
		
		// UserDetails 객체를 생성해서 
		UserDetails ud = new User(dto.getUserName(), dto.getPassword(), authList);
		// 리턴해준다
		return ud;
	}	
}
  • JwtUtil 클래스 생성
@Component
public class JwtUtil {
	/*
	 * custom.properties 파일을 만들고 해당 파일에
	 * jwt.secret 라는 키값으로 토큰을 발급할 때 서명할 key 를 설정했는데 그 문자열을 읽어와야 한다
	 * */
    @Value("${jwt.secret}")
    private String secretKey;

    // jwt.expiration 라는 키값으로 토큰의 유효기간을 설정했는데 해당 숫자를 읽어와야 한다
    @Value("${jwt.expiration}")
    private long expiration;

    private Key getSigningKey() {
        return Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey));
    }

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    public Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey()) // token 발급시 서명했던 키값도 일치하는지 확인도 된다.
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(String username, Map<String, Object> extraClaims) {
        Map<String, Object> claims = new HashMap<>(extraClaims);
        return createToken(claims, username);
    }
    
    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims) // 추가 정보 
                .setSubject(subject) // 주요 정보(주로 userName)
                .setIssuer("your-issuer") // 추가된 issuer(발급한 서비스명)
                .setIssuedAt(new Date(System.currentTimeMillis())) // 발급 시간
                .setExpiration(new Date(System.currentTimeMillis() + expiration)) // 파기되는 시간
                .signWith(getSigningKey(), SignatureAlgorithm.HS256) // HS256 알고리즘으로 서명
                .compact();
    }
    // token 검증하는 메소드 
    public boolean validateToken(String token) {
    	// token 에 담긴 추가 정보를 얻어낼수 있다 (role, issuer등등)
        Claims claims = extractAllClaims(token);
        // 토큰 유효기간이 남아 있는지와 issuer 정보도 일치하는지 확인해서 
        boolean isValid=!isTokenExpired(token) && "your-issuer".equals(claims.getIssuer());
        // 유효성 여부를 리턴한다
        return isValid;
    }
}

➜ JwtUtil 클래스가 동작하기 위해서 pom.xml 에 아래의 dependency 를 추가 해야 한다

<!-- Json Web Token 관련 라이브러리 -->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-api</artifactId>
	<version>0.11.5</version>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-impl</artifactId>
	<version>0.11.5</version>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-jackson</artifactId>
	<version>0.11.5</version>
	<scope>runtime</scope>
</dependency>
  • resources 폴더에 custom.properties 파일 생성
# jwt token secret key
jwt.secret = mySecretKey000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
# Jwt token expiration mili second
jwt.expiration = 1800000
  • Spring10JwtApplication 에 @PropertySource 어노테이션 추가
    • resources 에 작성한 custom.properties 파일을 로딩하도록 한다
@PropertySource(value = "classpath:custom.properties")
  • JwtFilter 클래스 생성
@Component
public class JwtFilter extends OncePerRequestFilter{

@Autowired JwtUtil jwtUtil;
	
	@Autowired CustomUserDetailsService service;
	
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		
		String jwtToken="";
	
		// 요청의 Header 에 "Authorization" 이라는 키값으로 전달된 문자열이 있는지 읽어와 본다.
		String authHeader = request.getHeader("Authorization");
		if(authHeader != null && authHeader.startsWith("Bearer ")) {
			jwtToken = URLDecoder.decode(authHeader, StandardCharsets.UTF_8);
		}
		
		// 토큰이 있으면 토큰에서 userName 을 얻어낸다
		String userName = null;
		if(jwtToken.startsWith("Bearer ")) {
			// "Bearer " 를 제외한 뒤의 token 문자열을 얻어낸다.
			jwtToken = jwtToken.substring(7);
			// userName 을 token 으로 부터 얻어낸다.
			userName = jwtUtil.extractUsername(jwtToken);
		}
		
		// userName 이 존재하고  Spring Security 에서 아직 인증을 받지 않은 상태라면 
		if(userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
			// 토큰이 유효한 토큰인지 체크한다음 
			boolean isValid = jwtUtil.validateToken(jwtToken);
			// 유효하다면 1회성 로그인(spring security 를 통과할 로그인) 을 시켜준다.
			if(isValid) {
				Claims claims = jwtUtil.extractAllClaims(jwtToken); // JWT에서 모든 정보 가져오기
				String role = claims.get("role", String.class);
				// userName 과 role 정보를 담은 UserDetails 객체를 만든다. 
				UserDetails ud = User.withUsername(userName)
							.password("") //비밀번호는 필요없지만 null 인 상태면 builder 에서 에러발생
							.authorities(role)
							.build();
				// 사용자가 제출한 사용자 이름과 비밀번호와 같은 인증 자격 증명을 저장
				UsernamePasswordAuthenticationToken authToken=
					new UsernamePasswordAuthenticationToken(ud, null, 
							ud.getAuthorities());
				authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
				// Security 컨텍스트 업데이트 (1회성 로그인)
				SecurityContextHolder.getContext().setAuthentication(authToken);
			}
		}
		// 다음 spring 필터 chain 진행하기
		filterChain.doFilter(request, response);	
	}
}
  • SecurityConfig 클래스 생성
    • @Configuration : 설정 클래스라고 알려준다
    • @EnableWebSecurity : Security 를 설정하기 위한 어노테이션
    • @EnableMethodSecurity(securedEnabled = true)
      : Controller 메소드에서 권한 체크 가능 하도록 한다
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {
	
	@Autowired
	private JwtFilter jwtFilter;
	
	/*
	 *  매개변수에 전달되는 HttpSecurity 객체를 이용해서 우리의 프로젝트 상황에 맞는 설정을 기반으로 
	 *  만들어진 SecurityFilterChain 객체를 리턴해주어야 한다
	 *  또한 SecurityFilterChain 객체도 스프링이 관리하는 Bean 이 되어야 한다  
	 */
	@Bean // 메소드에서 리턴되는 SecurityFilterChain 을 bean 으로 만들어준다
	public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
		String[] whiteList = {"/auth"};
		 
		httpSecurity
		.headers(header->
			// 동일한 origin 에서 iframe 을 사용할수 있도록 설정(default 값은 사용불가)
			header.frameOptions(option->option.sameOrigin()) // SmartEditor 에서 필요함
		)
		.csrf(csrf->csrf.disable())
		.authorizeHttpRequests(config ->
			config
				.requestMatchers(whiteList).permitAll()
				.requestMatchers("/admin/**").hasRole("ADMIN") 
				.requestMatchers("/staff/**").hasAnyRole("ADMIN", "STAFF")
				.requestMatchers(HttpMethod.POST, "/user").permitAll() // api 회원가입 요청은 받아들이도록
				.anyRequest().authenticated()
		)
		.sessionManagement(config ->
			// 세션을 사용하지 않도록 설정한다
			config.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
		)
		// JwtFilterSpring Security 필터보다 미리 수행되게 하기 
		.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
		
		// 설정 정보를 가지고 있는 HttpSecurity 객체의 build() 메소드를 호출해서 리턴되는 객체를 리턴해준다
		return httpSecurity.build();
	}
	
	//비밀번호를 암호화 해주는 객체를 bean 으로 만든다.
	@Bean
	PasswordEncoder passwordEncoder() { 
		//여기서 리턴해주는 객체도 bean 으로 된다.
		return new BCryptPasswordEncoder();
	}
	//인증 메니저 객체를 bean 으로 만든다. (Spring Security 가 자동 로그인 처리할때도 사용되는 객체)
	@Bean
	AuthenticationManager authenticationManager(HttpSecurity http,
			BCryptPasswordEncoder encoder, UserDetailsService service) throws Exception{
		// 적절한 설정을한 인증 메니저 객체를 리턴해주면 bean 이 되어서 Spring Security 가 사용한다 
		return http.getSharedObject(AuthenticationManagerBuilder.class)
				.userDetailsService(service)
				.passwordEncoder(encoder)
				.and()
				.build();
	}
}
  • UserController 클래스 생성
@RequestMapping("/v1")
@RestController
public class UserController {
	
	@Autowired JwtUtil jwtUtil;
	// SecurityConfig 클래스에서 Bean 이된 AuthenticationManager 객체 주입 받기
	@Autowired AuthenticationManager authManager;
	
	@PostMapping("/login")
	public ResponseEntity<String> login(@RequestBody UserDto dto){
		Authentication authentication=null;
		try {
			UsernamePasswordAuthenticationToken authToken=
					new UsernamePasswordAuthenticationToken(dto.getUserName(), dto.getPassword());
			// 인증 메니저 객체를 이용해서 인증을 진행한다.
			authentication=authManager.authenticate(authToken);
		}catch(Exception e) {
			// 예외가 발생하면 인증실패(아이디 혹은 비밀번호 틀림 등)
			e.printStackTrace();
			// 401 UNAUTHORIZED 에러를 응답하면서 문자열 한줄 보내기 
			return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 실패!");
		}
		
		// Authentication 객체에는 인증된 사용자 정보가 들어 있다 (userName, role 등)
		// 현재는 role 을 하나만 부여하기 때문에 0 번 방에 있는 데이터만 불러오면 된다
		GrantedAuthority authority=authentication.getAuthorities().stream().toList().get(0);
		// ROLE_XXX 형식
		String role=authority.getAuthority();
		// "role" 이라는 키값으로 Map 에 담기 
		Map<String, Object> claims=Map.of("role", role);

		// 예외가 발생하지 않고 여기까지 실행 된다면 인증을 통과 한 것이다 토큰을 발급해서 응답한다
		String token=jwtUtil.generateToken(dto.getUserName(), claims);
		// 발급받은 토큰 문자열을 ResponseEntity 에 담아서 리턴한다
		return ResponseEntity.ok("Bearer "+token);
	}
}
  • pom.xml 에 dependency 추가
<!-- swagger ui 를 사용하기 위한 dependency (spring boot 3.0 이상 버전에서)-->
<dependency>
	<groupId>org.springdoc</groupId>
	<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
		<version>2.6.0</version>
</dependency>
  • application.properties 에 포트번호 추가
server.port=9000
  • SecurityConfig 에 화이트 리스트 추가
String[] whiteList = {"/auth", "/swagger-ui/**", "/v3/api-docs/**", "/v1/login"};

Swagger UI 로 가서 테스트 하기

{
  "userName": "kimgura",
  "password": "1234"
}

➜ Jwt 토큰의 Secret key 를 환경변수에 담아 읽어오는 구조로 사용한다

  • UserController 에 ping 메소드 생성
@GetMapping("/ping")
public String ping() {
	return "pong";
}
  • 토큰을 발급 받아 해당 토큰을 삽입해 Get 방식으로 Send
    • 토큰 없이 요청을 하면 403 이 뜬다
    • 해당 토큰은 30분 동안 유효하다

  • LoginModal.jsx 에 토큰 코드 수정
localStorage.token = res.data;

  • Home.jsx 에 버튼 코드 수정
<button onClick={()=>{
	axios.get("/api/v1/ping", {
		headers:{
			Authorization:localStorage.token
		}
    })
	.then(res => {
		alert(res.data)
	})
	.catch(err => {
		alert("토큰이 없어서 응답 불가")
	})
}}>요청 보내기</button>
  • Home.jsx 에 handleLogout 함수에 토큰 삭제 코트 수정
elete localStorage.token;
  • 로그인 없이 클릭 시

  • 로그인 성공 후 클릭 시


로그인

src 폴더를 Step05Redux 로 폴더명을 수정 후 src_spa 폴더를 복사 붙여넣기 해 src 로 폴더 생성

  • main.jsx 에 iniState & handlers & reducer & store 추가
// redux store 에서 관리될 state 의 초기값
const initState = {
  userInfo:null,
  loginModal:{
    title:null,
    show:false
  }
};

// reducer 함수에서 사용할 handler object
const handlers = {
  LOGIN_MODAL : (state, action) => ({
    ...state,
    loginModal:action.payload
  }), 
  USER_INFO : (state, action) => ({
    ...state,
    userInfo:action.payload
  })
};

// handler obejct 를 사용하는 새로운 reducer 함수
const reducer = (state = initState, action) => {
  // 전달된 action 을 수행할 handler 함수를 얻어낸다
  const handler = handlers[action.type];
  // handler 함수가 존재한다면 handler 함수가 리턴하는 state 를 리턴하고 아니면
  // 원래 state 를 리턴한다
  return handler ? handler(state, action) : state;
};

// 위에서 만든 reducer 함수를 사용하는 redux store (중앙 저장소) 만들기
const store = createStore(reducer);
  • main.jsx 에 RouterProviderProvider 로 감싸기
<Provider store={store}>
	<RouterProvider router={router}/>
</Provider>
  • src 폴더 안에 api 폴더 생성 후 index.js 생성
import axios from "axios";

// baseURL 값으로 "/api" 를 기본으로 가지고 있는 axios 객체를 만들어서  
const api = axios.create({
    baseURL:"/api"
});

// 요청 인터셉터 (ex : 토큰 자동 추가)
api.interceptors.request.use((config) => {
    // const token = localStorage.token; 과 동일한 동작
    const token = localStorage.getItem('token');
    // 만일 token 이 존재한다면
    if (token) {
        // 요청의 header 에 Authorization 이라는 키값으로 token 을 전달하도록 한다
        config.headers.Authorization = token;
    }
    return config;
});

// 리턴해준다
export default api;
import axios form 'axios';
axios.post("/api/v1/login", {
	headers : {Authorization : xxx}
})

			vs

import api from './api';
api.post("/v1/login")
  • src 폴더 안에 components 폴더 생성 후 LoginModal.jsx
function LoginModal() {

    // store 로부터 loginModal 의 상태값을 읽어온다
    // {title:"Modal의 제목", show:Modal을 띄울지 여부(true or false)} 형식의 object
    const loginModal = useSelector(state => state.loginModal);

    // 입력한 userName 과 password 를 상태값으로 관리
    const [state, setState] =  useState();
    
    // 아이디 비밀번호를 입력했을 때 호출될 함수
    const handleChange = (e) => {
        // 입력한 내용을 상태값에 반영한다
        setState({
            ...state,
            [e.target.name]:e.target.value
        });
    }

    // 에러 메세지를 상태값으로 관리
    const [errorMsg, setErrorMsg] = useState(null);

    // 액션을 발행할 함수
    const dispatch = useDispatch();

    // route 이동을 위한 navigate 함수
    const navigate = useNavigate();

    // 로그인 버튼을 눌렀을 때 실행할 함수
    const handleLogin = async () => { // 이 함수는 즉시 리턴하는 함수가 아니라고 async 로 알려준다
        try{      
            // api 서버에 로그인 요청을 보내고
            const res = await api.post("/v1/login", state);
            // userName 과 password 가 유효한 정보이면 정상 응답이 되고
            console.log(res.data);
            localStorage.token = res.data;
            // redux store 에 로그인 정보를 저장한다
            const payload = {userName:state.userName, role:"ADMIN"};
            const action = {type:"USER_INFO", payload};
            dispatch(action);
            // 로그인 폼을 초기화 하고 숨긴다
            setErrorMsg(null);
            dispatch({
                type:"LOGIN_MODAL",
                payload:{
                    title:"",
                    show:false
                }
            });
            // 로그인 성공 후에 이동할 url
            const url = loginModal.url; // null 일 가능성 존재
            // 원래 가려던 목적지 정보가 있으면 해당 위치로 이동시킨다
            if(url){
                navigate(url);
            }
        }catch(err){
            // userName 과 password 가 틀리면 401 UNAUTHORIZED 에러 응답을 할 예정
            setErrorMsg(err.response.data);
        }
    }

    // Modal 창을 닫기 버튼을 누르면 react bootstrap Modal 에는 hide 이벤트가 발생
    // 해당 이벤트가 발생했을 때 redux store 의 상태값을 변경해서 모달창이 숨겨지도록 한다
    const handleHide = () => {
        dispatch({
            type:"LOGIN_MODAL",
            payload:{title:"", show:false}
        });
    };

    return <>
        {/* store 에서 관리되는 loginModal 의 show 값을 이용해서 띄울지 말지를 결정 */}
        <Modal onHide={handleHide} show={loginModal.show} size="lg" centered>
            <Modal.Header closeButton>
                <Modal.Title>{loginModal.title}</Modal.Title>
            </Modal.Header>
            <Modal.Body>
                <FloatingLabel controlId='userName' label="User Name" className='mb-3'>
                    <Form.Control onChange={handleChange} name="userName" type="text" />
                </FloatingLabel>
                <FloatingLabel controlId='password' label="Password" className='mb-3'>
                    <Form.Control onChange={handleChange} name="password" type="password" />
                </FloatingLabel>
                {errorMsg && <Alert variant="danger">{errorMsg}</Alert>}
            </Modal.Body>
            <Modal.Footer>
                <Button onClick={handleLogin}>로그인</Button>
            </Modal.Footer>
        </Modal>    
    </>
}
  • components 폴더 안에 BsNavBar.jsx 생성
    • `fixed = "top" :상단 고정되게 하려고
function BsNavBar() {
    return <>
        <Navbar fixed="top" expand="md" className="bg-warning">
            <Container>
                <Navbar.Brand as={NavLink} tp="/">Acorn</Navbar.Brand>
            </Container>
        </Navbar>
    </>
}
  • App.jsx 에 BsNavBar & div 요소에 style 추가
<BsNavBar/>

<div className="container" style={{marginTop:"60px"}}>
  • HomeController 클래스 생성
@RequestMapping("/v1")
@RestController
public class HomeController {
	
	@GetMapping("/notice")
	public List<String> notice(){
		return List.of("React 로 App 구현중 입니다", "테스트 시작", "테스트 완료");
	}
}
  • SecurityConfig 에 화이트 리스트 추가
String[] whiteList = {"/swagger-ui/**", "/v3/api-docs/**", "/v1/notice"};
  • Home.jsx 에 useState & useEffect 추가 및 공지사항 출력
    • map() 함수에 전달한 함수에는 2번째 매개변수에 item 의 인덱스 값도 전달된다
// 공지사항
const [noticeList, setNoticeList] = useState([]);

useEffect(()=>{
	api.get("/v1/notice")
	.then(res => setNoticeList(res.data))
	.catch(err => console.log(err));
}, []);

return (
	<>
		<h1>인덱스 페이지</h1>
		<h2>공지 사항</h2>
		<ul>
			{noticeList.map((item, index)=><li key={index}>{item}</li>)}
		</ul>
	</>
);

  • BsNavBar.jsx 에 Navbar & Nav & 버튼 코드 추가
<Navbar.Toggle aria-controls="collapse"/>
<Navbar.Collapse id="collapse">
	<Nav className="me-auto">
		<Nav.Link as={NavLink} to="/">HOME</Nav.Link>
		<Nav.Link as={NavLink} to="/posts">POST</Nav.Link>
		<Nav.Link as={NavLink} to="/gallery">Gallery</Nav.Link>
	</Nav>
	<Button size="sm" variant="success">로그인</Button>
	<Button size="sm" variant="primary" className="ms-1">회원가입</Button>
</Navbar.Collapse>
  • BsNavBar.jsx 에 useDispatch 함수 추가
    • store 의 상태값을 바꿀 함수
const dispatch = useDispatch();
  • BsNavBar.jsx 에 useSelector 함수 추가
    • redux store 로 부터 상태값 가져오기
const userInfo = useSelector(state => state.userInfo);
  • BsNavBar.jsx 에 useNavigate 함수 추가
    • route 이동을 위한 hook
const navigate = useNavigate();
  • BsNavBar.jsx 에 handleLogin 함수 추가
    • 로그인 버튼을 눌렀을 때 실행할 함수
 const handleLogin = (e) => {
	// LoginModal 을 띄울 action 을 발행한다
	const action = {type:"LOGIN_MODAL", payload:{
		title:"로그인 폼 입니다",
		show:true
	}};
	dispatch(action);
};
  • 로그인 버튼에 onClick 이벤트 요소 추가
<Button onClick={handleLogin} size="sm" variant="success">로그인</Button>
  • App.jsx 에 LoginModal 사용
import LoginModal from './components/LoginModal';

<LoginModal/>

로그아웃

  • BsNavBar.jsx 에 Nav & 로그아웃 버튼 추가
<Nav>
	<Nav.Link>{userInfo.userName}</Nav.Link>
	<span className="navbar-text">로그인중...</span>
</Nav>
<Button className="ms-1" size="sm" variant="outline-primary">로그아웃</Button>
  • 로그인 & 로그아웃 상태를 삼항 연산자로 관리
{ userInfo ?
    <>
        <Nav>
            <Nav.Link>{userInfo.userName}</Nav.Link>
            <span className="navbar-text">로그인중...</span>
        </Nav>
        <Button size="sm" variant="outline-primary">로그아웃</Button>
    </>
:
    <>
        <Button onClick={handleLogin} size="sm" className="ms-1" variant="success">로그인</Button>
        <Button size="sm" variant="primary" className="ms-1">회원가입</Button>
    </>
}
  • 로그아웃 버튼에 onClick 이벤트 요소 추가
<Button onClick={handleLogout} size="sm" variant="outline-primary">로그아웃</Button>
  • BsNavBar.jsx 에 handleLogout 함수 추가
    • 로그아웃 버튼을 눌렀을 때 실행할 함수
const handleLogout = (e) => {
	// 토큰 삭제
	delete localStorage.token;
	// redux store 에 userInfo 초기화
	dispatch({type:"USER_INFO", payload:null});
	// 인덱스로 이동
	navigate("/");
}

jwt-decode 설치

npm install jwt-decode
  • App.jsx useEffect 함수 추가
    • 컴포넌트가 활성화되는 시점에 토큰을 읽어와 본다
useEffect(()=>{
	const token = localStorage.token;
	if(token){
		// 앞의 7 자리 문자열 "Bearer" 를 제외한 위의 문자열을 디코딩한다
		const decoded = jwtDecode(token.substring(7));
		// 콘솔에 출력
		console.log("토큰 정보");
		console.log(decoded);       
	}
}, []);

  • useDispatch 함수 추가
const dispatch = useDispatch();
  • useEffect 함수 안에 api 코드 추가
api.get("/v1/ping")       
	.then(res => {
		// 여기가 실행되면 사용 가능한 토큰
		// 발행할 action
		const action = {type:"USER_INFO", payload:{
			userName : decoded.sub,
			role:decoded.role
		}};
		//액션 발행하기
		dispatch(action);
	})
	.catch(err => {
		// 여기가 실행되면 사용 불가능한 토큰
		delete localStorage.token;
});

➜ 새로고침을 해도 토큰이 남아있으면 로그인된 상태로 뜬다

profile
학원 공부 내용 정리

0개의 댓글