'2025/03/09'에 해당되는 글 2건

728x90

React 에서 axios 를 통해서 백엔드에서 키를 호출하는 방법에 대해 기록한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import React from "react";
import {useState, useEffect, useRef} from "react";
import {Link, useNavigate} from "react-router-dom";
import axios from "axios";
import { JSEncrypt } from "jsencrypt"// npm install jsencrypt
 
const Signup = () => {
    //const baseUrl = process.env.REACT_APP_API_BASE_URL || "http://localhost:8080";
    const baseUrl = "https://spb.abc.com:8080";
 
    const navigate = useNavigate();
    const initData = {
        userID: '',
        password: ''
    }
 
    const [publicKey, setPublicKey] = useState("");
    const [sessionId, setSessionId] = useState(""); // 세션 ID 저장
    const isFetched = useRef(false); // 실행 여부 체크
 
    useEffect(() => {
        if (isFetched.current) return
        isFetched.current = true
 
        console.log("🔹 공개키 요청 실행됨");
        // 백엔드에서 공개키 가져오기
        axios.get(baseUrl + "/api/public-key")
            .then(response => {
                console.log("🔹 공개키 응답:", response.data);
                setPublicKey(response.data.publicKey);
                setSessionId(response.data.sessionId); // 세션 ID 저장
            })
            .catch(error => console.error("공개키 가져오기 실패", error));
    }, []);
 
    const encrypt = new JSEncrypt(); // Start our encryptor.
    encrypt.setPublicKey(publicKey); // Assign our encryptor to utilize the public key.
 
    const [formData, setFormData] = useState(initData);
    const [users, setUsers] = useState([]);
    // 첫번째 원소 : 현재 상태, 두번재 원소 : 상태를 바꾸어 주는 함수
    const [error, setError] = useState(null);
 
    const onChangeInput = (e) => {
        setFormData({
            ...formData,
            [e.target.name]: e.target.value
        });
        // e.target.name 은 해당 input의 name을 가리킨다.
        //console.log(formData);
    }
 
    const submitForm = (e) => {
        e.preventDefault();
        
        if (!sessionId) {
            console.error("세션 ID가 없습니다. 공개키를 다시 요청하세요.");
            setError(true);
            return;
        }
 
        const params = {
            username: formData.userID,
            password: encrypt.encrypt(formData.password), // 비밀번호 암호화
            //password: formData.password, 
            sessionId: sessionId // 세션 ID 포함
        }
        console.log(params);
        axios.post(baseUrl + '/login', params)
            .then((res) => {
                console.log(res);
                if (res.status === 200) {
                    console.log(res.data);
                    localStorage.setItem("accessToken", res.data.token);
                    axios.defaults.headers.common["Authorization"= "Bearer " + res.data.token;
                    navigate('/');
                } else {
                    console.log(res.data.message);
                    setError(true);
                }
            })
            .catch(error => {
                console.log(error.response)
            });
    }
 
    return (
        <div className="container h-100 mt-5">
            <div className="row d-flex justify-content-center align-items-center h-100">
                <div className="col-12 col-md-9 col-lg-7 col-xl-6">
                    <div className="card">
                        <div className="card-body p-5">
                            <h2 className="text-uppercase text-center mb-5">로그인</h2>
                            <form onSubmit={submitForm}>
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="userID">userID</label>
                                    <input type="text" name="userID" onChange={onChangeInput} id="userID"
                                            className="form-control form-control-lg" value={formData.userID}
                                            required/>
                                </div>
 
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="password">Password</label>
                                    <input type="password" name="password" onChange={onChangeInput} id="password"
                                            className="form-control form-control-lg" value={formData.password}
                                            required/>
                                </div>
 
                                { error &&
                                <div className="alert alert-danger" role="alert">
                                    로그인 정보를 다시 한번 확인하세요!
                                </div>
                                }
 
                                <div className="d-flex justify-content-center">
                                    <button type="submit"
                                            className="btn btn-primary btn-block btn-lg gradient-custom-3 text-body">로그인
                                    </button>
                                </div>
 
                                <p className="text-center text-muted mt-5 mb-0">
                                    <Link to="/register" className="fw-bold text-body"><u>회원가입</u></Link>
                                </p>
 
                            </form>
 
                        </div>
                    </div>
                </div>
            </div>
        </div>
    )
}
 
export default Signup

 

 

백엔드 : Spring Boot 설정 파일

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package com.example.springjwt.config;
 
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {
        corsRegistry.addMapping("/**")
                .allowedOrigins(
                        "https://spb.abc.com",
                        "http://localhost:3000"
                ) // 정확한 출처 지정
                .allowedMethods("GET""POST""PUT""DELETE""OPTIONS"// 허용할 메서드 명확히 설정
                .allowCredentials(true// 인증 정보 포함 허용
                .allowedHeaders("Authorization""Content-Type""Cache-Control");
    }
}
 
/**
 * 이 설정을 유지하려면 SecurityConfig.java에서 .cors() 설정을 제거해야 한다.
 * (권장) WebMvcConfigurer를 삭제하고, SecurityConfig.java에서 CORS를 관리하는 것이 좋다.
 */
 
package com.example.springjwt.config;
 
import com.example.springjwt.jwt.CustomLogoutFilter;
import com.example.springjwt.jwt.JWTFilter;
import com.example.springjwt.jwt.JWTUtil;
import com.example.springjwt.jwt.LoginFilter;
import com.example.springjwt.repository.RefreshRepository;
import com.example.springjwt.service.RSAService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
 
import java.util.Arrays;
import java.util.Collections;
 
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    // AuthenticationManager 가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입
    private final AuthenticationConfiguration authenticationConfiguration;
    private final JWTUtil jwtUtil;
    private final RefreshRepository refreshRepository;
    private final RSAService rsaService;
 
    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, RefreshRepository refreshRepository, RSAService rsaService) {
 
        this.authenticationConfiguration = authenticationConfiguration;
        this.jwtUtil = jwtUtil;
        this.refreshRepository = refreshRepository;
        this.rsaService = rsaService;
    }
 
    //AuthenticationManager Bean 등록
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
 
        return configuration.getAuthenticationManager();
    }
 
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 
        http
                .cors((corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
 
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
 
                        CorsConfiguration configuration = new CorsConfiguration();
                        configuration.setAllowedOriginPatterns(Arrays.asList(
                                "https://spb.abc.com",
                                "http://localhost:3000"
                        )); // allowedOrigins("*")을 사용하지 않고, 구체적인 출처를 명시
                        configuration.setAllowCredentials(true);
 
                        // allowedMethods("*") 대신 GET, POST, PUT, DELETE, OPTIONS를 명확하게 설정
                        // configuration.setAllowedMethods(Collections.singletonList("*"));
                        configuration.setAllowedMethods(Arrays.asList("GET""POST""PUT""DELETE""OPTIONS"));
 
                        // allowedHeaders("*") 대신 Authorization, Content-Type, Cache-Control을 지정
                        // configuration.setAllowedHeaders(Collections.singletonList("*"));
                        configuration.setAllowedHeaders(Arrays.asList("Authorization""Content-Type""Cache-Control"));
 
                        configuration.setExposedHeaders(Collections.singletonList("Authorization"));
 
                        configuration.setMaxAge(3600L);
 
                        return configuration;
                    }
                })));
 
        //csrf disable
        http
                .csrf((auth) -> auth.disable());
 
        //From 로그인 방식 disable
        http
                .formLogin((auth) -> auth.disable());
 
        //http basic 인증 방식 disable
        http
                .httpBasic((auth) -> auth.disable());
 
 
        //경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/api/join""/login""/""/api/public-key""/api/pub-key").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .requestMatchers("/reissue").permitAll()
                        .anyRequest().authenticated());
 
        http
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
 
        http
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository, rsaService), UsernamePasswordAuthenticationFilter.class);
 
        http
                .addFilterBefore(new CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter.class);
        
        //세션 설정
        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
 
        http
                .csrf((auth) -> auth.disable());
 
        return http.build();
    }
}
 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.example.springjwt.controller;
 
import com.example.springjwt.dto.JoinDTO;
import com.example.springjwt.service.JoinService;
import com.example.springjwt.service.RSAService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
 
import java.util.Map;
import java.util.UUID;
 
@RestController
@RequestMapping("/api")
public class AuthController {
 
    private final RSAService rsaService;
    private final JoinService joinService;
 
    @Autowired
    public AuthController(RSAService rsaService, JoinService joinService) {
        this.rsaService = rsaService;
        this.joinService = joinService;
    }
 
    /**
     * 회원 가입용 공개키
     */
//    @GetMapping("/pub-key")
//    public Map<String, String> getPubKey() {
//        return Map.of("publicKey", rsaService.getPubKey());
//    }
 
    @GetMapping("/pub-key")
    public Map<StringString> getPubKey(HttpServletRequest request) {
        String clientIp = getClientIp(request);
        int clientPort = request.getRemotePort(); // ✅ 클라이언트 포트 가져오기
        System.out.println("📌 클라이언트 IP: " + clientIp + " | 포트: " + clientPort);
        return Map.of("publicKey", rsaService.getPubKey());
    }
 
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
 
 
    /**
     * 로그인 용 공개키
     */
    @GetMapping("/public-key")
    public Map<StringString> getPublicKey() {
        String sessionId = UUID.randomUUID().toString(); // 클라이언트마다 새로운 세션 ID 생성
        String publicKey = rsaService.generateNewPublicKey(sessionId);
        return Map.of("sessionId", sessionId, "publicKey", publicKey);
    }
 
 
    @PostMapping("/join")
    public Map<StringString> joinProcess(@RequestBody JoinDTO joinDTO) {
        joinService.joinProcess(joinDTO, rsaService.getPriKey());
        return Map.of("message""회원가입 성공");
    }
 
}
 
 

 

 

728x90

'Spring Boot > Security' 카테고리의 다른 글

Spring Boot SSL 인증서 적용  (0) 2025.03.09
Spring Boot Security 적용 예시 화면  (0) 2025.03.08
Spring Boot RSA 암호화/복호화 함수  (1) 2025.03.04
블로그 이미지

Link2Me

,
728x90

Windows 11 노트북에서 접속할 때는 인증서가 필요없이도 잘 접속이 되는데 도메인 URL을 통해 접속하는 리눅스 서버에서 443 포트로 접속하는 통신을 하려면 인증서가 필요하다.

 

# SSL 인증서 로컬 시스템에 등록 (Rocky 9)
PKCS12 (Public Key Cryptograhic Standards)의 줄임말로 패스워드로 보호된 형식으로써, 여러 인증서 및 키를 포함할 수 있다. Java뿐만 아니라 여러 플랫폼에서 사용 가능하다.
Letsencrypt 에서 얻는 키를 이용하여 생성하면 된다.

# PKC12생성하기 : Java KeyStore (keystore.p12)로 변환
SSL 인증서를 Spring Boot에 추가하여 HTTPS를 8080 포트에서 활성화하려면, 다음 단계를 따라 Java KeyStore (JKS) 또는 PKCS12 포맷으로 변환하고 설정해야 한다.


openssl pkcs12 -export -in cert1.pem -inkey privkey1.pem -out keystore.p12 -name tomcat -password pass:changeit

 


-export: PKCS12 파일을 생성
-in cert1.pem  → 공개 인증서 파일
-inkey privkey1.pem 개인 키 파일
-out keystore.p12 변환된 인증서 저장 위치
-name tomcat 별칭(alias) 설정
-password pass:changeit  비밀번호 설정 (Spring Boot 설정에서 동일하게 사용해야 함)

생성된 인증서를 위의 3번과 같이 Windows 11 환경의 IntelliJ IDEA 구동한 

spring boot 프로젝트의 resources 폴더에 넣고 build 해서 생성된 파일을 리눅스 시스템에 업로드하면 된다.

 

 

 

728x90
블로그 이미지

Link2Me

,