JSESSIONID가 요청 시 자동으로 포함되지 않는 문제와 해결 방법
프론트엔드 (:5500)에서 백엔드 (:8080)로 로그인 요청을 보낼 때 JSESSIONID를 받아 저장했지만, 이후 요청에서 쿠키가 자동으로 포함되지 않는 문제 발생.
방안 1. 구조 변경
- 프론트엔드를 백엔드 서버에서 서빙하여 동일한 도메인/포트를 사용하도록 변경.
방안 2. 쿠키 설정 변경 (토큰)
데이터베이스 테이블 생성
1
2
3
4
5
6
7
| use ureca;
CREATE TABLE `ureca`.`login` (
`email` VARCHAR(50) NOT NULL primary key,
`token` VARCHAR(256) NOT NULL unique,
`logintime` TIMESTAMP NOT NULL DEFAULT current_timestamp
);
|
OpenCrypt.java (암호화 유틸리티)
- SHA-256을 이용해 이메일을 해싱하여 토큰을 생성.
- AES-256 암호화를 지원.
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
| import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class OpenCrypt {
public static byte[] getSHA256(String source, String salt) {
byte byteData[]=null;
try{
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(source.getBytes());
md.update(salt.getBytes());
byteData= md.digest();
System.out.println("원문: "+source+ " SHA-256: "+
byteData.length+","+byteArrayToHex(byteData));
}catch(NoSuchAlgorithmException e){
e.printStackTrace();
}
return byteData;
}
public static byte[] generateKey(String algorithm,int keySize) throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
keyGenerator.init(keySize);
SecretKey key = keyGenerator.generateKey();
return key.getEncoded();
}
public static String aesEncrypt(String msg, byte[] key) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
String iv = "AAAAAAAAAAAAAAAA";
cipher.init(Cipher.ENCRYPT_MODE,
skeySpec,
new IvParameterSpec(iv.getBytes()));
byte[] encrypted = cipher.doFinal(msg.getBytes());
return byteArrayToHex(encrypted);
}
public static String aesDecrypt(String msg,byte[] key ) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
String iv = "AAAAAAAAAAAAAAAA";
cipher.init(Cipher.DECRYPT_MODE,
skeySpec,
new IvParameterSpec(iv.getBytes()));
byte[] encrypted = hexToByteArray(msg);
byte[] original = cipher.doFinal(encrypted);
return new String(original);
}
public static byte[] hexToByteArray(String hex) {
if (hex == null || hex.length() == 0) {
return null;
}
byte[] ba = new byte[hex.length() / 2];
for (int i = 0; i < ba.length; i++) {
ba[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
}
return ba;
}
// byte[] to hex
public static String byteArrayToHex(byte[] ba) {
if (ba == null || ba.length == 0) {
return null;
}
StringBuffer sb = new StringBuffer(ba.length * 2);
String hexNumber;
for (int x = 0; x < ba.length; x++) {
hexNumber = "0" + Integer.toHexString(0xff & ba[x]);
sb.append(hexNumber.substring(hexNumber.length() - 2));
}
return sb.toString();
}
}
|
MemberService.java (토큰 로그인 로직)
- 사용자 인증 후 이메일을 기반으로 토큰 생성
- 토큰을 login 테이블에 저장
- 토큰을 응답하여 클라이언트가 저장 후 사용하도록 처리
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
| package com.shop.cafe.service;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.shop.cafe.dao.LoginDao;
import com.shop.cafe.dao.MemberDao;
import com.shop.cafe.dto.Login;
import com.shop.cafe.dto.Member;
import com.shop.cafe.util.OpenCrypt;
@Service
public class MemberService {
@Autowired
MemberDao memberDao;
@Autowired
LoginDao loginDao;
public Login tokenLogin(Member m) throws Exception {
m=memberDao.login(m);
if(m!=null) {
String nickname=m.getNickname();
if(nickname!=null && !nickname.trim().equals("")) {
//member table에서 email과 pwd가 확인된 상황 즉 login ok
String email=m.getEmail();
//1. salt를 생성한다
String salt=UUID.randomUUID().toString();
System.out.println("salt:"+salt);
//2. email을 hashing 한다
byte[] originalHash=OpenCrypt.getSHA256(email, salt);
//3. db에 저장하기 좋은 포맷으로 인코딩한다
String myToken=OpenCrypt.byteArrayToHex(originalHash);
System.out.println("myToken : "+myToken);
//4. login table에 token 저장
Login loginInfo=new Login(email, myToken, nickname, null);
loginDao.insertToken(loginInfo);
return loginInfo;
}
}
return null;
}
public Member login(Member m) throws Exception {
return memberDao.login(m);
}
public void insertMember(Member m) throws Exception{
memberDao.insertMember(m);
}
public void updateMember(Member m) throws Exception{
memberDao.updateMember(m);
}
public void deleteMember(String email) throws Exception{
memberDao.deleteMember(email);
}
public void logout(String authorization) throws Exception {
loginDao.deleteToken(authorization);
}
}
|
Login.java (DTO)
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
| package com.shop.cafe.dto;
import java.util.Date;
public class Login {
private String email, token, nickname;
private Date loginTime;
public Login(String email, String token, String nickname, Date loginTime) {
super();
this.email = email;
this.token = token;
this.nickname = nickname;
this.loginTime = loginTime;
}
public Login() {
super();
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public Date getLoginTime() {
return loginTime;
}
public void setLoginTime(Date loginTime) {
this.loginTime = loginTime;
}
@Override
public String toString() {
return "Login [email=" + email + ", token=" + token + ", nickname=" + nickname + ", loginTime=" + loginTime
+ "]";
}
}
|
LoginDao.java (토큰 저장/삭제)
- 토큰을 DB에 저장하고 삭제하는 기능을 제공.
1
2
3
4
5
6
7
8
9
10
| package com.shop.cafe.dao;
import org.apache.ibatis.annotations.Mapper;
import com.shop.cafe.dto.Login;
@Mapper
public interface LoginDao {
public void insertToken(Login login) throws Exception;
public void deleteToken(String token) throws Exception;
}
|
login.xml (MyBatis 설정)**
insertToken
: 로그인 시 토큰을 저장.deleteToken
: 로그아웃 시 토큰 삭제.
1
2
3
4
5
6
7
8
9
| <mapper namespace="com.shop.cafe.dao.LoginDao">
<insert id="insertToken" parameterType="Login">
insert into login(email, token) values(#{email},#{token})
</insert>
<delete id="deleteToken" parameterType="String">
delete from login where token=#{token}
</delete>
</mapper>
|
MemberController.java (API 엔드포인트)
- 로그인 시 토큰을 생성하여 응답
- 이후 요청은
Authorization
헤더에 토큰을 담아 인증
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
| @CrossOrigin("http://127.0.0.1:5500/")
@RestController
public class MemberController {
@PostMapping("tokenLogin")
public Map<String, String> tokenLogin(@RequestBody Member m) {
Map<String, String> responseMap = new HashMap<>();
try {
Login loginInfo = memberService.tokenLogin(m);
if (loginInfo != null) {
responseMap.put("nickname", loginInfo.getNickname());
responseMap.put("Authorization", loginInfo.getToken());
} else {
responseMap.put("msg", "다시 로그인 해주세요");
}
} catch (Exception e) {
responseMap.put("msg", "다시 로그인 해주세요");
}
return responseMap;
}
@PostMapping("logout")
public void logout(@RequestHeader String authorization) {
try {
memberService.logout(authorization);
} catch (Exception e) {
e.printStackTrace();
}
}
}
|
프론트엔드 axios 설정 변경
백엔드가 쿠키를 유지하도록 허용했다면, 프론트엔드에서도 요청 시 withCredentials: true 옵션을 추가해야 함.
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
| document.getElementById("loginBtn").addEventListener("click", async () => {
const email = document.getElementById("loginEmail").value;
const pwd = document.getElementById("loginPwd").value;
const data = { email, pwd };
const response=await axios.post("http://localhost:8080/tokenLogin" , data);
document.getElementById("loginSpan").innerHTML=`${response.data.nickname}
<button class="btn btn-danger btn-sm" id="logoutBtn">Logout</button>`;
const token = response.data.Authorization;
sessionStorage.setItem('Authorization', token);
sessionStorage.setItem('nickname', response.data.nickname);
axios.defaults.headers.common['Authorization'] = token; // Authorization 헤더 설정
const modal = bootstrap.Modal.getInstance(document.getElementById("loginModal"));
loginModal.setAttribute("aria-hidden", "true");
modal.hide();
});
const Authorization = sessionStorage.getItem("Authorization");
const nickname = sessionStorage.getItem("nickname");
if (Authorization && nickname) {
axios.defaults.headers.common['Authorization'] = Authorization; // Authorization 헤더 설정
document.getElementById("loginSpan").innerHTML = `${nickname}
<button class="btn btn-danger btn-sm" id="logoutBtn">Logout</button>`;
}
document.getElementById("loginSpan").addEventListener("click", async (event)=>{
if(event.target.id=='logoutBtn'){
await axios.post("http://localhost:8080/logout");
sessionStorage.removeItem("nickname");
sessionStorage.removeItem("Authorization");
axios.defaults.headers.common['Authorization'] = ''; // Authorization 헤더에서 삭제
window.location.reload();
}
});
|
결과
- JSESSIONID 대신 토큰을 사용하여 인증을 관리
- 클라이언트는 응답받은
Authorization
토큰을 이후 요청의 헤더에 포함하여 인증을 수행. - DB에 저장된 토큰을 활용해 사용자의 로그인 상태를 유지할 수 있음.
Request Header
와 Request Body
의 차이
- 요청에 대한 메타데이터(부가 정보)를 포함
- 요청의 형식, 인증 정보, 캐싱 정책, 클라이언트 정보 등을 전달
- 주로 키-값 쌍 (Key-Value Pair) 형태로 표현됨
1
2
3
4
| POST /api/user HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Bearer token123
|
Content-Type
: 요청 본문의 데이터 형식을 지정 (예: application/json
)Authorization
: 인증 정보를 전달 (예: JWT 토큰)User-Agent
: 클라이언트(브라우저 또는 앱)의 정보
2️⃣ Request Body (요청 본문)
- 요청의 실제 데이터(payload) 를 포함
POST
, PUT
, PATCH
등의 요청에서 주로 사용GET
요청에는 보통 본문이 없음
1
2
3
4
| {
"username": "yeeun",
"password": "secure123"
}
|
- 요청의 본문에는 실제 데이터를 담아 서버로 전달함
Content-Type: application/json
헤더가 있어야 JSON 데이터를 보낼 수 있음
정리
구분 | Request Header | Request Body |
---|
역할 | 요청의 메타데이터 | 요청의 실제 데이터 |
포함 정보 | 인증 정보, 콘텐츠 타입, 쿠키 등 | 사용자 입력 데이터, 파일 등 |
형식 | 키-값 쌍 (Key-Value) | JSON, XML, FormData 등 |
Teamwork Challenge - 장바구니 담기
백엔드 (BACK)
데이터베이스 (MySQL) : cart 테이블 생성
1
2
3
4
5
| create table cart(
cartNo int primary key,
token varchar(256) not null,
prodcode int not null,
);
|
데이터 모델(DTO) : Cart.java
- member 테이블과 매핑되는 객체
- 회원 정보를 저장하는 필드 및 getter, setter 포함
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
| package com.shop.cafe.dto;
public class Cart {
private int cartNo,prodcode;
private String token;
public Cart(int cartNo, int prodcode, String token) {
super();
this.cartNo = cartNo;
this.prodcode = prodcode;
this.token = token;
}
public Cart() {
super();
// TODO Auto-generated constructor stub
}
public int getCartNo() {
return cartNo;
}
public void setCartNo(int cartNo) {
this.cartNo = cartNo;
}
public int getProdcode() {
return prodcode;
}
public void setProdcode(int prodcode) {
this.prodcode = prodcode;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public String toString() {
return "Cart [cartNo=" + cartNo + ", prodcode=" + prodcode + ", token=" + token + "]";
}
}
|
데이터 접근 계층(DAO) : CartDao.java
🔹 데이터베이스와 연결하여 회원 정보를 저장하는 역할
1
2
3
4
5
6
7
8
9
10
11
12
13
| package com.shop.cafe.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.shop.cafe.dto.Cart;
import com.shop.cafe.dto.Product;
@Mapper
public interface CartDao {
public void addCart(Cart c);
}
|
서비스 계층 (Service) : CartService.java
🔹 DAO를 호출하여 회원 정보를 저장하는 역할
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| package com.shop.cafe.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.shop.cafe.dao.CartDao;
import com.shop.cafe.dto.Cart;
import com.shop.cafe.dto.Product;
@Service
public class CartService {
@Autowired
CartDao cartDao;
public void addCart(Cart c) throws Exception{
cartDao.addCart(c);
}
}
|
컨트롤러 (Controller) : CartController.java
🔹 REST API 엔드포인트를 제공하여 회원가입 요청을 처리
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
| package com.shop.cafe.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import com.shop.cafe.dto.Cart;
import com.shop.cafe.service.CartService;
@RestController
@CrossOrigin("http://127.0.0.1:5500/")
public class CartController {
@Autowired
CartService cartService;
@PostMapping("addCart")
public void addCart(@RequestBody Cart c, @RequestHeader String authorization) {
try {
if(authorization.equals(c.getToken())) {
cartService.addCart(c);
} else {
System.out.println("다름");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
|
프론트엔드 (FRONT)
Cart.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| let prodcode;
document.addEventListener("click", async (event) => {
if (event.target.classList.contains("cart-btn")) {
prodcode = event.target.dataset.productId;
try {
const reviewsResponse = await axios.post(
"http://localhost:8080/addCart",
{ prodcode: prodcode, token: sessionStorage.Authorization }
);
const cart = reviewsResponse.data;
} catch (error) {
console.error("에러");
}
}
});
|