로그인, 로그아웃 jwt + mybatis + mysql + springboot

GoldenDusk·2023년 9월 29일
0
post-thumbnail

1. 로그인, 로그아웃 Spring boot로 작업하기 전 정리

일단, redius까지는 구성하지 않으려고 한다. 어쨋든 이게 다른 팀원들이랑 작업하는데.. redius까지 구성하기엔 좀 힘들어할 수도 있겠다라는 생각이 들었음

🍇 써야 할 기술들

  1. Spring boot 프레임워크 기반
  2. 데이터베이스 Mysql
  3. Mybatis
  4. jwt
  5. 가능하다면 스프링 시큐리티

1. Spring이랑 React 연동

🔗 관련 정리 글 : https://velog.io/@prettylee620/팀-프로젝트-공통-템플릿-만들기-프론트엔드와-백엔드-환경-설정인텔리제이에-리액트와-스프링부트-연결하기

  • 포트 번호랑 맞춰야 하며, 이게 연동하면서 알게 된 건데 8080에는 데이터베이스에 연동된 데이터가 보이는데 3000은 안보여서 추가로 WebConfig 설정 해주었다.

package com.in4mation.festibook.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 WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "POST", "PUT", "DELETE");
    }
}

2. Mysql의 경우

  • ERD 작업한 것을 기반으로 테이블 쿼리문 뽑아왔다.

3. Mybatis와 Mysql 연동

🔗 관련 정리 글 : https://velog.io/@prettylee620/팀-작업-회의록

  • application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/festibook?&serverTimezone=UTC&autoReconnect=true&allowMultiQueries=true&characterEncoding=UTF-8
spring.datasource.username=Festibook
spring.datasource.password=f112
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:/mappers/*.xml
mybatis.type-aliases-package=com.in4mation.festibook

🍇 MyBatis 대해 자세히 알아보자

MyBatis는 Java 객체와 SQL 데이터베이스 사이의 매핑을 담당하는 프레임워크

1. **MyBatis의 주요 구성 요소**

  1. DataSource: 데이터베이스 연결을 관리
  2. Mapper (XML): SQL 쿼리와 Java 메서드를 매핑
  3. SqlSessionFactory: SqlSession 객체를 생성
  4. SqlSession: 데이터베이스 연결과 SQL 쿼리 실행을 담당
  5. DAO (Data Access Object): 데이터 접근 로직을 포함

2. MyBatis의 동작 과정

  1. DataSource 설정: 데이터베이스 연결 정보가 설정
  2. Mapper 정의: XML 파일 또는 Annotation을 사용하여 Java 인터페이스 메서드와 SQL 쿼리가 매핑
  3. SqlSessionFactory 생성: DataSource와 Mapper 설정을 기반으로 SqlSessionFactory가 생성
  4. SqlSession 생성: SqlSessionFactory는 필요에 따라 SqlSession 객체를 생성되며, SqlSession은 실제로 SQL 쿼리를 실행
  5. DAO 구현 및 사용: DAO는 Mapper 인터페이스를 사용하여 정의된 SQL 쿼리를 실행하는 메서드를 포함되며, 애플리케이션 로직에서는 DAO 객체를 통해 데이터베이스 접근을 수행

3. 그렇다면 SqlSessionTemplate, DAO(Data Access Object), DTO(Data Transfer Object)은 뭘까?

  • SqlSessionTemplateMyBatis-Spring 통합 모듈의 일부로, SqlSession을 스프링의 트랜잭션 관리에 적합하게 만들어주며, SqlSessionTemplate는 thread-safe하므로 여러 DAO에서 공유할 수 있다.
  • DAO데이터베이스에 접근하는 로직을 캡슐화한 객체로 DAO를 통해 애플리케이션 코드와 데이터 접근 코드를 분리할 수 있어, 코드의 가독성과 유지 보수성이 향상된다. MyBatis에서 DAO는 Mapper 인터페이스의 구현체로서, Mapper에 정의된 SQL 쿼리를 실행하는 메서드를 포함된다.
@Repository
public class UserDao {
    private final SqlSessionTemplate sqlSession;

    @Autowired
    public UserDao(SqlSessionTemplate sqlSession) {
        this.sqlSession = sqlSession;
    }

    public User getUserById(Integer id) {
        return sqlSession.getMapper(UserMapper.class).getUserById(id);
    }
}
  • DTO(Data Transfer Object)데이터 전송 객체로, 계층간 데이터 교환을 위해 사용됩니다. 즉, 하나의 계층에서 다른 계층으로 데이터를 전달하는데 사용하는 객체로 일반적으로, DTO는 로직을 가지지 않고, 데이터 필드와 이에 접근할 수 있는 getter 및 setter 메서드만을 포함
public class UserDTO {
    private Integer id;
    private String username;
    private String email;

    // Getter와 Setter 메서드들
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

구조적 흐름

  1. DataSource 및 Mapper(XML) 설정: 데이터베이스 연결 정보와 SQL 매핑 정보를 설정
  2. SqlSessionFactory 및 SqlSessionTemplate 생성: 스프링 설정에서 SqlSessionFactorySqlSessionTemplate 빈을 생성하고 구성
  3. DAO 생성 및 주입: DAO 객체가 생성되고, 이 객체에 SqlSessionTemplate이 주입
  4. 애플리케이션 로직 수행: 애플리케이션 로직에서 DAO 메서드를 호출하여 데이터베이스 작업을 수행

2. JWT를 이용한 로그인, 로그아웃 공부하기

🍇 토큰 기반 인증이란?

개념

사용자가 서버에 접근할 때, 이 사용자가 인증된 사용자인지 확인하는 방법

  1. 서버 기반 인증
    1. 스프링 시큐리티
    • 세션 기반 인증 : 사용자마다 사용자의 정보를 담은 세션을 생성하고 저장해서 인증을 하는 방식
  2. 토큰 기반 인증
    1. 토큰을 사용하는 방식
    2. 토큰서버에서 클라이언트를 구분하기 위한 유일한 값, 서버가 토큰을 생성해서 클라이언트에게 제공하면, 클라이언트는 이 토큰을 가지고 있다가 여러 요청을 이 토큰과 함께 신청함
    3. 서버는 토큰만 보고 유효한 사용자인지 검사

토큰을 전달하고 인증 받는 과정

  1. 클라이언트가 서버에게 로그인 요청
    1. 아이디와 비밀번호를 전달하여 로그인 요청
  2. 서버는 클라이언트에게 토큰 생성 후 응답
    1. 서버는 아이디와 비번을 확인해 유효한 사용자인지 검증
    2. 유효한 사용자면 토큰을 생성해서 응답
import com.in4mation.festibook.dto.member.MemberDTO;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserLogin {
private String userId;// id
private String userPw;// password

public UserLogin() {

    }

public UserLogin(String userId, String userPw) {
this.userId= userId;
this.userPw= userPw;
    }

public UserLogin(MemberDTO memberDTO) {
this.userId= memberDTO.getMember_id();
this.userPw= memberDTO.getMember_password();
    }
}
import com.in4mation.festibook.dto.member.MemberDTO;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserLogin {
private String userId;// id
private String userPw;// password

publicUserLogin() {

    }

public UserLogin(String userId, String userPw) {
this.userId= userId;
this.userPw= userPw;
    }

public UserLogin(MemberDTO memberDTO) {
this.userId= memberDTO.getMember_id();
this.userPw= memberDTO.getMember_password();
    }
}
  1. 클라이언트는 토큰 저장
  2. 클라이언트는 토큰 정보와 함께 서버에게 요청
    1. 인증이 필요한 API를 사용할 때 토큰을 함께 보낸다.
  3. 서버는 토큰 유효한지 검증
  4. 서버가 클라이언트에게 응답
    1. 토큰이 유효하다면 클라이언트가 요청한 내용 처리

토큰 기반 인증의 특징

  1. 무상태성
  • 사용자의 인증 정보가 담겨 있는 토큰이 서버가 아닌 클라이언트에 있기 때문에 자원 소비가 없음
  • 서버 입장에서는 클라이언트의 인증 정보를 저장하거나 유지하지 않아도 되기 때문에 완전한 무상태에서 효율적인 검증 가능
  1. 확장성
  • 서버를 확장할 때 상태 관리를 신경 쓸 필요가 없어서 서버 확장에 용이
  • 예시로 물건을 파는 서비스가 있고 결제를 위한 서버와 주문을 위한 서버가 분리되어 있다고 가정
    • 세션 인증 기반은 각각 API을 인증해야 되는 것과 달리 토큰 기반 인증에서는 토큰을 가지는 서버가 아니라 클라이언트이기 때문에 하나의 토큰으로 결제서버와 주문 서버에게 요청을 보낼 수 있다.
    • 추가로 페이스북 로그인, 구글 로그인과 같이 토큰 기반 인증을 사용하는 다른 시스템에 접근해 로그인 방식을 확장할 수 있고 이를 이용해 다른 서비스에 권한을 공유할 수 있다.
  1. 무결성
  • 토큰 방식은 HMAC(hash-based message authentication) 기법
  • 토큰을 발급한 이후에는 토큰 정보를 변경하는 행위는 할 수 없음
  • 만약 누군가가 토큰을 한 글자라도 변경하면 서버에서는 유효하지 않은 토큰이라고 판단

🍇 JWT(JSON Web Token)

발급받은 JWT를 이용해서 인증을 하려면 HTTP 요청 헤더 중에 Authorization 키값에 Bearer + JWT 토큰값을 넣어 보내야 한다.

JWT란

  • 웹 표준으로, 두 개체 사이에서 정보를 JSON 객체안전하게 전송하기 위한 간결하고 독립적인 방법
  • 디지털 서명을 통해 검증과 신뢰 가능하다.
  • 일반적으로, 서명은 서버의 비밀 키로 해시되며, 서버만이 토큰을 생성하고 검증 가능

JWT 구조

aaaaa.bbbbb.ccccc
  1. 헤더 (Header): 헤더는 토큰의 타입과 사용되는 해싱 알고리즘을 포함
{
"typ" : "JWT",
"alg" : "HS256"
}
  1. 페이로드 (Payload) = 내용
  • 페이로드는 클레임이라고도 하는데, 이는 엔터티 (유저, 서버) 및 추가 데이터를 포함하며, 페이로드에는 토큰의 만료 시간과 같은 다양한 정보가 포함될 수 있다. 키값의 쌍으로 이루어져 있음
  • 공개 클레임 : 공개되어도 상관없는 클레임을 의미, 충돌 방지할 수 있는 이름을 가져야 하며, 보통 클레임 이름을 URI로 짓는다.
  • 비공개 클레임 : 공개되면 안 되는 클레임을 의미, 클라이언트와 서버 간의 통신에 사용
  • 등록된 클레임 : 예약된 클레임 이름을 가지며, JWT 사용자와 생산자 사이에 정의되어 있다.iss, exp, sub, aud 등이 이에 해당되며, 이들 클레임은 모두 선택적
{
  "sub": "1234567890", // 등록된 클레임: 주체(토큰이 의미하는 사용자/주체)
  "name": "John Doe",  // 비공개 클레임: 사용자 정의 클레임 (이름 정보)
  "iat": 1516239022,   // 등록된 클레임: 발급 시간 (토큰이 발급된 시간)
  "exp": 1627651727,   // 등록된 클레임: 만료 시간 (토큰이 만료되는 시간)
  "admin": true,       // 비공개 클레임: 사용자 정의 클레임 (어드민 여부)
  "https://example.com/is_root": true // 공개 클레임: 충돌을 방지하기 위해 URI 형식을 가진 클레임
}
  1. 서명 (Signature): 서명을 생성하기 위해서, 헤더와 페이로드를 Base64로 인코딩하고, 이 두 문자열을 점(.)으로 연결한 후, 비밀 키를 사용하여 이 문자열을 서명 해시값 생성

3. 구현하기 전

🍇 구현 하기 전에 해야할 것 정리해보기

  • API 명세 작업(중간 중간)
  • 관리자 아이디, 비번 픽스
  • 관리자 페이지
  • 관리자 권한 설정
  • 마이페이지 아이콘
  1. API 명세 만들면서 작업할 것
  2. 관리자 관련(제외 시킴)
    1. 아이디, 비번을 어떤 걸로 픽스할 것이냐…
    2. 관리자 페이지를 따로 만들기.. 일반 사용자한테는 눈에 안보이는 데 관리자 아이디로 로그인 했을 때 관리자 페이지가 보이는 게 좋으려나? 근데 그럼 관리자 페이지에는 뭘 넣어주지… 필터링 된 단어가 있는 게시글이나 리뷰가 있으면 관리자 페이지에서 볼 수 있게..? 근데 이건 어떻게 설정하지…? ⇒ 1. 욕설을 리스트에 박아 놓는 방법(시간 부족 시 예시 몇 개만) 2. 자연어 처리(NLP) 3. 외부 서비스 사용
    3. 관리자는 공지를 작성할 수 있게 해줘야 함 + 권한 설정 필요
    • 물론 이걸 데이터베이스 더미데이터로 박아서도 작업할 수 있긴 한데(이거는 추후에 고민해보자)
    • 그리고 관리자는 댓글들이나 리뷰가 이상하면 모든 것들을 삭제할 수 있는 권한이 필요⇒ 이런걸 나중에 팀원들이 그 페이지를 작업하면 과연 이것을 어떻게 불러올 것이냐
  3. 로그인이 되면 로그인 버튼이 사라지고 로그아웃 버튼이 나와야 하며 옆에는 마이페이지로 들어갈 수 있는 아이콘 생성
    1. 마이페이지에서 설정한 이미지가 있다면 이미지로 아니라면 기본이미지가 뜨도록 설정해야함
    2. 차라리 마이페이지 아이콘에 탭하면 나오는 형식이 나을듯
    • 그럼 모바일 버전은…? 그냥 그것도 그런식으로 하자
  4. 자동로그인을 계속 유지될 수 있게 할 수 있는 것은 뭐가 있을까…? 데이터베이스에 컬럼이 없어도 가능하려나..?
  • 토큰으로 인해 토큰 지속시간이 12시간이라 어차피 12시간은 자동로그인 지정
package com.in4mation.festibook.jwt;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.security.Signature;
import java.util.Date;

@Component
public class JwtUtils {
private static finalLoggerlogger= LoggerFactory.getLogger(JwtUtils.class);

// Java JWT (JSON Web Token)라이브러리인 JJWT에서 제공하는 API를 사용하여, HS256(HMAC SHA-256)알고리즘을 사용하는 시크릿 키를 생성
//    private static final Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static finalStringsecretKey="abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";

//accessToken만료시간 설정
public final static longACCESS_TOKEN_VALIDATION_SECOND= 1000L*60*60*12;//12시간
public static finalStringAUTHORIZATION_HEADER="Authorization";//헤더 이름

//액세스 토큰 생성 메서드
public String createAccessToken(String member_id, String name){
        System.out.println("createAccessToken");

//토큰 만료 시간 설정(access token)
Date now =newDate();
        Date expiration =newDate(now.getTime()+ACCESS_TOKEN_VALIDATION_SECOND);

// JWT생성 AccessToken생성하여 반환, member_id를 주체로 함
return Jwts.builder()
                .setSubject(member_id)
                .claim("name", name)
                .setIssuedAt(now)
                .setExpiration(expiration)
//                .signWith(secretKey)
.signWith(SignatureAlgorithm.HS256,secretKey)
                .compact();

    }

//토큰 유효성 검증 메서드
public booleanvalidateToken(String token){
//토큰 파싱 후 발생하는 예외를 캐치하여 문제가 있으면 false,정상이면 true반환
try{
            Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return true;
        }
catch(SignatureException e){
//서명이 옳지 않을 때
System.out.println("잘못된 토큰 서명입니다.");
        }
catch(ExpiredJwtException e){
//토큰이 만료됐을 때
System.out.println("만료된 토큰입니다.");
        }
catch(IllegalArgumentException | MalformedJwtException e){
//토큰이 올바르게 구성되지 않았을 때 처리
System.out.println("잘못된 토큰입니다.");
        }
return false;
    }

//토큰에서 member_id를 추출하여 반환하는 메소드
publicString getId(String token){
        System.out.println("getId");

return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().getSubject();
    }

//토큰에서 name을 추출하여 반환하는 메소드
public String getName(String token){
        System.out.println("getName");

return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().get("name").toString();
    }

// HttpServletRequest에서 Authorization Header를 통해 access token을 추출하는 메서드입니다.
public String getAccessToken(HttpServletRequest httpServletRequest) {
        String bearerToken = httpServletRequest.getHeader(AUTHORIZATION_HEADER);
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
returnbearerToken.substring(7);
        }
return null;
    }

public String determineRedirectURI(HttpServletRequest httpServletRequest, String memberURI, String nonMemberURI) {
        String token = getAccessToken(httpServletRequest);
if(token ==null) {
return nonMemberURI;//비회원용 URI로 리다이렉트
}else{
returnmemberURI;//회원용 URI로 리다이렉트
}
    }
}
  1. JWT와 시큐리티, mybatis에 연동된 mysql 이용해서 이러한 구현들은 어떻게 시작해야 할까?
  2. JWT를 통한 소셜 로그인
  3. 축제 홍보할 수 있게 등록하는 사람을 만들어줘야 하나..?(관리자보다 이게 더 나을려나..)
    1. 아니면 스케일 커지니까 관리자가 등록할 수 있는 권한을 부여 하는 방법..?
    2. 관리자 페이지보다는 이게 나을려

4. 구현해보기

  1. 일단 패키지는 팀별 논의를 통해 이런식으로 구성

🍇 JWT

1. bulid.gradle 추가

//시큐리티
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'com.google.code.gson:gson:2.8.9'

    // jjwt 라이브러리 추가
    implementation 'io.jsonwebtoken:jjwt-api:0.11.2'  // API 의존성 추가
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' // 구현 의존성 추가
    runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.11.2') { exclude group: 'org.json', module: 'json' } // JSON 처리 의존성 추가
   
    testImplementation 'org.springframework.security:spring-security-test'

2. **JwtUtils 클래스**

JSON Web Token(JWT)의 생성, 유효성 검증, 토큰에서 정보 추출 등의 작업을 처리하는 클래스

  • @Component 어노테이션을 통해 이 클래스가 Spring의 컴포넌트임을 명시
  • 비밀 키를 생성하여 HS256 알고리즘에 사용 ⇒ JJWT에서 제공하는 API 사
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.security.Signature;
import java.util.Date;

@Component
public class JwtUtils {
    private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

    // Java JWT (JSON Web Token) 라이브러리인 JJWT에서 제공하는 API를 사용하여, HS256(HMAC SHA-256) 알고리즘을 사용하는 시크릿 키를 생성
    private static final Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    //accessToken 만료시간 설정
    public final static long ACCESS_TOKEN_VALIDATION_SECOND = 1000L*60*60*12; //12시간
    public static final String AUTHORIZATION_HEADER = "Authorization"; //헤더 이름

    //액세스 토큰 생성 메서드
    public String createAccessToken(String member_id, String name){
        System.out.println("createAccessToken");

        // 토큰 만료 시간 설정(access token)
        Date now = new Date();
        Date expiration = new Date(now.getTime()+ ACCESS_TOKEN_VALIDATION_SECOND);

        // JWT 생성 AccessToken 생성하여 반환, member_id를 주체로 함
        return Jwts.builder()
                .setSubject(member_id)
                .claim("name", name)
                .setIssuedAt(now)
                .setExpiration(expiration)
                .signWith(secretKey)
                .compact();

    }

    //토큰 유효성 검증 메서드
    public boolean validateToken(String token){
        //토큰 파싱 후 발생하는 예외를 캐치하여 문제가 있으면 false, 정상이면 true 반환
        try{
            Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
            return true;
        }
        catch (SignatureException e){
            // 서명이 옳지 않을 때
            System.out.println("잘못된 토큰 서명입니다.");
        }
        catch (ExpiredJwtException e){
            // 토큰이 만료됐을 때
            System.out.println("만료된 토큰입니다.");
        }
        catch(IllegalArgumentException | MalformedJwtException e){
            // 토큰이 올바르게 구성되지 않았을 때 처리
            System.out.println("잘못된 토큰입니다.");
        }
        return false;
    }

    // 토큰에서 member_id를 추출하여 반환하는 메소드
    public String getId(String token){
        System.out.println("getId");

        return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().getSubject();
    }

    // 토큰에서 name을 추출하여 반환하는 메소드
    public String getName(String token){
        System.out.println("getName");

        return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().get("name").toString();
    }

    // HttpServletRequest에서 Authorization Header를 통해 access token을 추출하는 메서드입니다.
    public String getAccessToken(HttpServletRequest httpServletRequest) {
        String bearerToken = httpServletRequest.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    public String determineRedirectURI(HttpServletRequest httpServletRequest, String memberURI, String nonMemberURI) {
        String token = getAccessToken(httpServletRequest);
        if (token == null) {
            return nonMemberURI; // 비회원용 URI로 리다이렉트
        } else {
            return memberURI; // 회원용 URI로 리다이렉트
        }
    }
}

3. JwtInterceptor 클래스

스프링의 HandlerInterceptor 인터페이스를 구현한다. HandlerInterceptor는 요청이 들어올 때, Controller로 가기 전, 후의 처리를 담당할 수 있다. preHandle 메서드에서는 요청이 들어오면, 해당 요청의 헤더에서 Access Token을 가져와 토큰의 유효성을 검증합니다. 만약 토큰이 없거나 유효하지 않으면, 요청은 거부될 것입니다.

  • 참고 : @Autowired 어노테이션 : Spring 프레임워크에서 제공하는 어노테이션, 의존성 주입(Dependency Injection)에 사용
  • @Autowired를 사용하지 않았을 때는, 일반적으로 개발자가 직접 객체를 생성하거나 다른 방식으로 의존성을 주입해야 한다.
  • 여기에 주석 단 것 처럼 토큰이 있으면 글 작성할 수 있도록 권한을 줄 수 있음
package com.in4mation.festibook.jwt;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// JWT를 이용한 인터셉터 구현
@Component
public class JwtInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(JwtInterceptor.class);

    @Autowired
    private JwtUtils jwtUtils; //JWT 유틸리티 객체 주입

    @Autowired
    public JwtInterceptor(JwtUtils jwtUtils) {
        this.jwtUtils = jwtUtils;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        System.out.println("uri:" + uri);

//        if( !uri.equals("/api/reviews") ) return true;

//        if( uri.equals("/api/boards") ) {

            // 토큰 받기
            System.out.println("preHandle 실행");
            // 요청이 들어오면 실행되는 메서드
            String accessToken = jwtUtils.getAccessToken(request); //헤더에서 액세스 토큰을 가져옴
            System.out.println("Interceptor accessToken : " + accessToken); //요청 url 로깅을 위해 가져옴
            //로깅용 URI
            String requestURI = request.getRequestURI();

            // 비회원일 때(액세스 토큰이 없을 때)
            if (accessToken == null) {
                logger.debug("비회원 유저입니다 URI : {}", requestURI);
                System.out.println("비회원" + requestURI);
                return true;
            } else {
                logger.debug("access 존재합니다.");
                System.out.println("access 존재합니다.");
                // 액세스 토큰이 유효 시
                if (jwtUtils.validateToken(accessToken)) {
                    logger.debug("유효한 토큰 정보입니다. URI : {}", requestURI);
                    System.out.println("유효" + requestURI);
                    return true;
                } else {
                    //액세스 토큰이 유효하지 않을 시
                    logger.debug("유효하지 않은 jwt 토큰입니다. uri : {}", requestURI);
                    System.out.println("유효하지 않음" + requestURI);
                    return false;
                }
            }
//        }

//        return true;

    }
}

🍇 Login

1. MemberDTO

사용자가 로그인을 시도할 때, 아이디와 비밀번호를 체크하는 로직을 구현, 로그인이 성공하면 JWT 토큰을 생성하여 사용자에게 반환

  1. 사용자 로그인 요청 dto 필요

  • getter setter 이용하면 getMember_id 이런식으로 자동생성해서 사용 가능
package com.in4mation.festibook.dto.login;

import com.in4mation.festibook.dto.member.MemberDTO;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LoginDTO {
    private String member_id; // id
    private String member_password; // password

    public LoginDTO() {

    }

    public LoginDTO(String userId, String userPw) {
        this.member_id = userId;
        this.member_password = userPw;
    }

    public LoginDTO(MemberDTO memberDTO) {
        this.member_id = memberDTO.getMember_id();
        this.member_password = memberDTO.getMember_password();
    }

}

2. 보안 설정

  1. 시큐리티 config 설정 ⇒ 2번
  • 아래를 사용하기 위해서는 SecurityConfig 설정이 필요
@Autowired
    private AuthenticationManager authenticationManager;
  1. 보안설정

Spring Security 설정을 담당하는 클래스로 이 클래스는 웹 보안 설정, 인증 메커니즘 설정, 패스워드 인코딩, 및 보안 관련 빈들의 등록을 담당

  • AuthenticationManager는 인증 요청을 처리하는데 사용되며, 여러 AuthenticationProvider들을 관리
  • AuthenticationManager를 빈으로 등록하기 위해서는 AuthenticationManagerBuilder를 사용하여 인증에 대한 정보를 설정
  • 문제 발생 : 밑에서 자꾸 의존성 문제 생기고 순환 문제 생기 다른데 찾아봐도 없고 해서 봤더니.. .
    • Spring Application이 실패하였고, BeanCurrentlyInCreationException이 발생하고, 이 예외는 빈이 생성되는 도중에 문제가 발생했을 때 나타난다고 찾았는데 여기서 securityConfig 빈이 현재 생성 중인데, 생성할 수 없거나 순환 참조가 있을 가능성

/* // void로 쓰면 안됨!!!!
    @Autowired // AuthenticationManagerBuilder를 주입받아 사용자 세부 서비스를 설정
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(loginService).passwordEncoder(passwordEncoder());
        // 사용자의 세부 서비스를 설정하고, 비밀번호 인코더를 설정합니다.
    }*/
  • 아래와 같이 바꿔주니 잘 됐다
  @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(loginService).passwordEncoder(passwordEncoder());
    }
package com.in4mation.festibook.config;

import com.in4mation.festibook.jwt.JwtUtils;
import com.in4mation.festibook.service.login.LoginServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration // 해당 클래스를 Spring Configuration으로 등록
@EnableWebSecurity // Spring Security를 활성화
@EnableGlobalMethodSecurity(prePostEnabled = true) // 메소드 수준에서의 보안을 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   /* @Autowired // JwtUtils Bean을 주입
    private JwtUtils jwtUtils;
    @Autowired
    private UserDetailsService loginService;*/

    private final JwtUtils jwtUtils;
    private final LoginServiceImpl loginService;

    @Autowired
    public SecurityConfig(@Lazy JwtUtils jwtUtils, LoginServiceImpl loginService) {
        this.jwtUtils = jwtUtils;
        this.loginService = loginService;
    }

    @Override // HttpSecurity를 사용하여 Web Security 설정을 오버라이드한다.
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable() // CSRF(사이트 간 요청 위조) 공격 방어를 비활성화
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 세션을 생성하지 않으며, STATELESS로 설정하여, 서버가 상태를 저장하지 않게합니다.
                .and()
                .authorizeRequests() // HttpServletRequest에 따라 접근을 제한
                .antMatchers("/**").permitAll() // 모든 경로에 대해 접근을 허용
//                .antMatchers(
//                        "/api/login"
//                ).permitAll()
                .anyRequest().authenticated(); // 그 외의 모든 요청은 인증을 요구

        ;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(loginService).passwordEncoder(passwordEncoder());
    }

    @Bean // AuthenticationManager Bean을 생성하여 Spring Context에 등록
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean(); // 기본 AuthenticationManager Bean을 반환
    }

    @Bean // PasswordEncoder Bean을 생성하여 Spring Context에 등록
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 비밀번호를 인코딩하는데 사용되는 BCryptPasswordEncoder를 반환
    }
}

3. 로그인 컨트롤러

  1. 로그인 컨트롤러

관련 설명

  1. 인증 예외 처리
    사용자 인증이 실패하면 적절한 예외를 반환하도록 해야 합니다. 이를 위해 try-catch 블록을 사용하거나, AuthenticationException을 처리하는 예외 핸들러를 구현해야 합니다.
  2. HTTP 상태 코드
    인증이 성공하면 200 OK와 함께 토큰을 반환하고, 인증이 실패하면 401 Unauthorized를 반환해야 합니다.
  3. 사용자 상세 정보
    인증이 성공한 후에 Authentication 객체를 사용하여 안전하게 사용자 세부 정보를 가져올 수 있습니다.
  4. 클래스는 클라이언트로부터 로그인 요청을 받아, 사용자 인증을 수행하고 JWT 토큰을 생성하여 반환하는 역할을 한다. AuthenticationManager를 이용하여 사용자의 ID와 비밀번호를 검증하고, JwtUtils를 이용하여 JWT 토큰을 생성하며, 생성된 토큰은 JwtResponse라는 내부 클래스의 인스턴스로 담겨 클라이언트에게 반환
package com.in4mation.festibook.controller.login;

import com.in4mation.festibook.dto.member.MemberDTO;
import com.in4mation.festibook.exception.LoginException;
import com.in4mation.festibook.jwt.JwtUtils;
import com.in4mation.festibook.service.login.LoginService;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class LoginController {

    // AuthenticationManager를 스프링에서 자동으로 주입받아 사용
    // 사용자 인증을 위해 필요합니다.
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    LoginService loginService;

    // JWT 토큰 생성을 위해 필요
    @Autowired
    private JwtUtils jwtUtils;
    

    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@RequestBody MemberDTO memberDTO){
        try {
            // 사용자 인증
//            Authentication authentication = authenticationManager.authenticate(
//                    new UsernamePasswordAuthenticationToken(
//                            memberDTO.getMember_id(),
//                            memberDTO.getMember_password()
//                    )
//            );
            // member_id, password 체크
            MemberDTO member2 = loginService.checkLoin(memberDTO.getMember_id(), memberDTO.getMember_password());

            // JWT 토큰 생성 및 반환
            String jwt = jwtUtils.createAccessToken(member2.getMember_id(), member2.getMember_name());
            // 생성된 JWT 토큰을 응답 본문에 담아 반환
            return ResponseEntity.ok(new JwtResponse(jwt));
        }
        catch (LoginException e){
            System.out.println(e);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
        }
        catch (AuthenticationException e){
            // 인증 실패한 경우 에러 메세지 + 401 상태 코드 반환
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 실패 : 아이디나 비밀번호 확인해주세요");
        }
        catch(Exception e){
            // 그 외 에러의 경우 500 메세지
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("서버 내부 오류");
        }
    }

    // JWT 토큰을 담을 내부 클래스를 정의
    @Getter
    @Setter
    class JwtResponse {
        private String token;

        // 생성자를 통해 토큰을 초기화
        public JwtResponse(String token) {
            this.token = token;
        }
    }

   @GetMapping("/login-user-test")
    public MemberDTO loginUserTest() {
        MemberDTO memberDTO = MemberDTO.builder()
                .member_id("test")
                .member_email("test@test.com")
                .build();
        return memberDTO;
    }
}

4. **사용자 모델 및 레파지토리**

사용자 정보를 저장사용자 모델과, 데이터베이스와의 상호작용을 담당할 레파지토리

  1. LoginDTO: 사용자 모델 ⇒ 사용자 정보를 표현하는 모델

package com.in4mation.festibook.dto.login;

import com.in4mation.festibook.dto.member.MemberDTO;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LoginDTO {
    private String member_id; // id
    private String member_password; // password

    public LoginDTO() {

    }

    public LoginDTO(String userId, String userPw) {
        this.member_id = userId;
        this.member_password = userPw;
    }

    public LoginDTO(MemberDTO memberDTO) {
        this.member_id = memberDTO.getMember_id();
        this.member_password = memberDTO.getMember_password();
    }

}
  1. MemberDTO
package com.in4mation.festibook.dto.member;

import lombok.*;

import java.sql.Blob;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MemberDTO {
    private String member_id;   //아이디

    private String member_password;     //비밀번호

    private String member_email;    //이메일

    private String member_name;     //이름

    private String member_nickname;     //닉네임

    private Blob member_profile_image;    //프로필사진

    private String member_introduce;    //소개

    private boolean member_sns;     //소셜로그인 여부 (default = false)

    private String verificationCode; //인증번호
}
  1. **사용자 서비스 인터페이스 (**LoginService **Interface)**

package com.in4mation.festibook.service.login;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public interface LoginService { // username(UserDetailsService 인터페이스의 일부로서, Spring Security에서 제공)은 user id
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  1. **사용자 서비스 구현체 (**LoginServiceImpl **Implementation)**
package com.in4mation.festibook.service.login;

import com.in4mation.festibook.dto.login.LoginDTO;
import com.in4mation.festibook.dto.member.MemberDTO;
import com.in4mation.festibook.exception.LoginException;
import com.in4mation.festibook.repository.login.LoginMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

@Service("loginServiceImpl")
public class LoginServiceImpl implements LoginService, UserDetailsService {

    private final LoginMapper loginMapper;

    @Autowired
    public LoginServiceImpl(LoginMapper loginMapper) {
        this.loginMapper = loginMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username){
        LoginDTO user = loginMapper.findByUsername(username);

        if (user == null) {
            // user가 null인 경우 예외 발생
            throw new UsernameNotFoundException("유저를 찾을 수 없습니다.");
        }

        // 유저의 권한을 설정하는 부분
        return new org.springframework.security.core.userdetails.User(user.getMember_id(), user.getMember_password(), new ArrayList<>());
    }

    public MemberDTO checkLoin(String username, String password) throws LoginException {
        MemberDTO user = loginMapper.findByUsername2(username);

        if (user == null) {
            // user가 null인 경우 예외 발생
            throw new LoginException("유저를 찾을 수 없습니다.");
        }
        // password 암호화

        // password check
        if(!password.equals(user.getMember_password()))
            throw new LoginException("password error");

        return user;
    }

}
  1. **매퍼 인터페이스 :** LoginMapper

package com.in4mation.festibook.repository.login;

import com.in4mation.festibook.dto.login.LoginDTO;
import com.in4mation.festibook.dto.member.MemberDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface LoginMapper {
    //로그인을 할 때 회원정보 조회 필요 id = #{member_id}이 부분은 실제 컬럼이랑 동일해야함
    LoginDTO findByUsername(@Param("member_id") String username);

    MemberDTO findByUsername2(@Param("member_id") String username);
}
  1. xml Mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- UserMapper.xml -->
<mapper namespace="com.in4mation.festibook.repository.login.LoginMapper">
    <select id="findByUsername" resultType="com.in4mation.festibook.dto.login.LoginDTO">
        SELECT * FROM member_table WHERE member_id  = #{member_id}
    </select>

    <select id="findByUsername2" resultType="com.in4mation.festibook.dto.member.MemberDTO">
        SELECT * FROM member_table WHERE member_id  = #{member_id}
    </select>
</mapper>
  1. 예외처리
package com.in4mation.festibook.exception;

public class LoginException extends Exception {

    // 생성자에서 상위 클래스의 생성자를 호출하여
    // 예외 메시지를 설정합니다.
    public LoginException(String message) {
        super(message);
    }
}

리액트

  1. App.js
  • 애플리케이션 전역에서 토큰 관리를 위해 설정
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import "./App.css";
import Home from "./routers/Home";
import Festival from "./routers/Festival/Festival";
import Recommend from "./routers/Recommend/Recommend";
import Community from "./routers/Community/Community";
import Navigation from "./components/nav/Navigation";
import Login from "./routers/Login/Login";
import { AuthProvider } from './routers/Login/AuthProvider'

function App() {
    return (
        <AuthProvider>
        <Router>
            <Navigation />
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/festival" element={<Festival />} />
                <Route path="/recommend" element={<Recommend />} />
                <Route path="/community" element={<Community />} />
                <Route path="/login" element={<Login />} />
            </Routes>
        </Router>
        </AuthProvider>
    );
}

export default App;
  1. AuthProvider.js
  • 로컬에 토큰 저장 및 삭제 관리
// 토큰을 페이지 전역을 관리하기 위한 코드
import React, { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
    const [token, setToken] = useState(() => {
        console.log('초기 토큰:', localStorage.getItem('jwt'));
        //로컬에 저장
        return localStorage.getItem('jwt');
    });
    const [isLoggedIn, setIsLoggedIn] = useState(!!token); // 로그인 상태를 관리합니다.

    useEffect(() => {
        console.log('토큰 변경됨:', token);
        if (token) {
            localStorage.setItem('jwt', token);
            setIsLoggedIn(true);
        } else {
            localStorage.removeItem('jwt');
            setIsLoggedIn(false);
        }
    }, [token]);

    return (
        <AuthContext.Provider value={{ token, setToken, isLoggedIn, setIsLoggedIn }}>
            {children}
        </AuthContext.Provider>
    );
};

export const useAuth = () => {
    const context = useContext(AuthContext);
    if (context === undefined) {
        throw new Error('useAuth must be used within a AuthProvider');
    }
    return context;
};
  1. login.jsx
import React, { useEffect, useState } from 'react'
import "./Login.css";
import logoImage from '../../img/login/Loginlogo.png';
import with1 from '../../img/login/with1.png';
import with2 from '../../img/login/with2.png';
import googleLogin from '../../img/login/googleLogin.png';
import kakaoLogin from '../../img/login/kakaoLogin.png';
// toast 사용 라이브러리
import { ToastContainer, toast } from "react-toastify";
// react-toastify 제공하는 css
import 'react-toastify/dist/ReactToastify.css';
import axios from 'axios';
// .6버전에서 쓰는 것
import { useNavigate, useLocation } from 'react-router-dom';
import {useAuth} from "./AuthProvider";

/*const User = {
    id: 'testuser',
    pw: 'test2323@@@'
};*/

function Modal({ message, onClose }) {
    return (
        <div className="modalOverlay">
            <div className="modalContent">
                <p>{message}</p>
                <button onClick={onClose}>Close</button>
            </div>
        </div>
    );
}

export default function Login() {
    const [id, setId] = useState('');
    const [pw, setPw] = useState('');

    const [showModal, setShowModal] = useState(false);
    const [alertMessage, setAlertMessage] = useState('');

    const navigate = useNavigate();
    const location = useLocation();

    const { setToken, setIsLoggedIn } = useAuth(); // AuthContext에서 필요한 값과 함수를 가져옵니다.

    /*const [isLoggedIn, setIsLoggedIn] = useState(false);*/ //로그인과 로그아웃 상태 관리를 위한 상태 변수*/

    // localStorge에 토큰이 있는 경우 로그인 상태로 간주, 최상위 레벨에서 호출되어야 한다.
    useEffect(() => {
        const token = localStorage.getItem('jwt');
        if (token) setIsLoggedIn(true);
    }, []);

    // 로그아웃 함수도 최상위 레벨에 위치
    const logout = () => {
        localStorage.removeItem('jwt');
        console.log('토큰 삭제 완료:', localStorage.getItem('jwt'));
        setIsLoggedIn(false);
        toast.success('로그아웃에 성공했습니다.');
        navigate('/');
    };

    const handleId = (e) => {
        setId(e.target.value);
    };

    const handlePw = (e) => {
        setPw(e.target.value);
    };

    const onClickConfirmButton = () => {
        console.log("Button clicked!");          // 1. 로그 확인
        console.log("ID:", id, "PW:", pw);      // 2. 상태 값 확인

        const endpoint = 'http://localhost:8080/api/login';

        let data = JSON.stringify({
            "member_id": id,
            "member_password": pw
        });

        let config = {
            method: 'post',
            maxBodyLength: Infinity,
            url: endpoint,
            headers: {
                'Content-Type': 'application/json'
            },
            data : data
        };

        //  로컬 스토리지에 토큰을 저장하는 부분
        axios.request(config)
            .then((response) => {
                console.log(JSON.stringify(response.data));
                if( response.data?.token != undefined) {
                    toast.success('로그인에 성공했습니다.');
                    /*localStorage.setItem("jwt",  response.data?.token);*/
                    setToken(response.data?.token); // 상태에 토큰 저장
                    setIsLoggedIn(true);

                    setTimeout(() => {
                        navigate('/recommend');
                    }, 2000);
                } else {
                    toast.warning('로그인 실패했습니다. 아이디나 비밀번호를 확인해주세요');
                }

            })
            .catch((error) => {
                console.log(error);
                toast.warning('로그인 실패했습니다. 아이디나 비밀번호를 확인해주세요');
            });

    };

    return (

        <div className="mainContainer">
            <ToastContainer
                position="top-right"
                limit={1}
                closeButton={true}
                autoClose={3000}
                className="custom-toast-container"
                toastClassName="custom-toast"
            />
            <div className="page">

                <div className="contentWrap">
                    <div className="logoImage">
                        <img src={logoImage} alt="Logo Description"/>
                    </div>

                    <div className="logoName">
                        <span className="logoName1">기억하고 싶은 축제</span> <br/>FestiBook와 함께 해요!
                    </div>

                    <div className="input_login">
                        아이디
                    </div>

                    <div
                        className="inputWrap"
                    >
                        <input
                            className="input"
                            type="text"
                            placeholder="아이디를 입력해주세요"
                            value={id}
                            onChange={handleId}
                        />
                    </div>

                    <div className="input_password">
                        비밀번호
                    </div>

                    <div className="inputWrap">
                        <input
                            className="input"
                            type="password"
                            placeholder="비밀번호를 입력해주세요"
                            value={pw}
                            onChange={handlePw}
                        />
                    </div>

                    <div className="text">
                        <div className="find_id"><a href="http://localhost:8080/find-id"
                                                    target="_blank"
                                                    rel="noopener noreferrer">아이디 찾기</a></div>
                        <div className="find_password"><a href="http://localhost:8080/find_pass"
                                                          target="_blank"
                                                          rel="noopener noreferrer">비밀번호 찾기</a></div>
                        <div className="join"><a href="http://localhost:8080/member/register"
                                                    target="_blank"
                                                    rel="noopener noreferrer">회원가입</a></div>
                    </div>

                    <div className="buttonContainer">
                        {/*{isLoggedIn ? (
                            <button onClick={logout} className="bottomButton">Logout</button>
                        ) : (
                            <button onClick={onClickConfirmButton} className="bottomButton">Login</button>
                        )}*/}
                        <button onClick={onClickConfirmButton} className="bottomButton">Login</button>
                    </div>

                    <div className="soical_login">
                        <div className="social_img_text">
                            <div className="img_with1"> <img src={with1} alt="img description"/></div>
                            <div className="social_with_text">Or With </div>
                            <div className="img_with2"> <img src={with2} alt="img description"/></div>
                        </div>

                        <div className="social_img">
                            <div className="googleLogin"> <img src={googleLogin} alt="img description"/></div>
                            <div className="kakaoLogin"> <img src={kakaoLogin} alt="img description"/></div>
                        </div>
                    </div>

                </div>

            </div>
        </div>
    );
}

5. 추후 확장 가능성

🍇 추후 추가 할 때 대비해서 Redis

Redis는 다양한 프로그래밍 언어에서 사용할 수 있도록 클라이언트 라이브러리를 제공하며, 다양한 옵션과 설정으로 확장성과 유연성을 제공

1. Redis란?

  • 오픈 소스의 인메모리 데이터 구조 저장소로, 데이터베이스, 캐시 및 메시지 브로커로 사용될 수 있다.
  • Redis는 키-값 스토어의 형태를 가지며, 문자열, 해시, 리스트, 셋, 정렬된 셋 등 다양한 데이터 타입을 지원

2. 주요 특징

  1. 성능: Redis는 인메모리 데이터 스토어이므로, 디스크 I/O 없이 빠르게 데이터를 읽고 쓸 수 있습니다.
  2. 데이터 타입 다양성: Redis는 다양한 데이터 타입을 지원하여 다양한 상황에서 유연하게 사용할 수 있습니다.
  3. 지속성: Redis는 데이터를 디스크에 저장할 수 있어, 서버가 재시작되어도 데이터가 유지될 수 있습니다.
  4. 자동 파티셔닝: Redis는 대용량 데이터를 다룰 수 있도록 자동 파티셔닝을 지원합니다.
  5. 복제 및 분산: 데이터의 가용성과 내결함성을 향상시키기 위해 여러 노드에 데이터를 복제하고 분산할 수 있습니다.
  6. 원자성과 일관성: Redis는 트랜잭션을 지원하여 명령의 시퀀스가 중간에 실패하지 않고 완료되거나 실패함을 보장합니다.

3. 사용 사례

  • 캐싱: Redis는 데이터베이스 앞에서 캐시로서 동작하여, 데이터베이스에 대한 읽기 쓰기 부하를 줄이고 응답 시간을 개선할 수 있습니다.
  • 세션 스토어: 웹 애플리케이션은 사용자 세션을 Redis에 저장하여 세션 데이터의 빠른 접근을 가능케 할 수 있습니다.
  • 메시지 큐: Redis는 메시지 브로커로서 사용될 수 있어, 다양한 서비스와 애플리케이션 간의 메시지를 전달할 수 있습니다.
  • 실시간 분석: 대량의 데이터를 실시간으로 분석하고 처리할 수 있습니다.

4. 그렇다면 Mybatis와 Mysql, redius를 이용해서 같이 쓸 수 있는가?

  1. 예시 아키텍처
  • MySQL: 주 데이터 저장소로 사용되며, 영구적인 데이터를 저장
  • MyBatis: Java 애플리케이션과 MySQL 데이터베이스 간의 통신을 돕는 SQL 매핑 프레임워크
  • Redis: 캐시 또는 중간 데이터 저장소로서 사용되어 성능 향상을 도모
  1. 사용방법

  2. 캐싱으로 Redis 사용: 애플리케이션은 먼저 Redis를 조회하여 필요한 데이터가 있는지 확인합니다. Redis에 데이터가 있다면, 데이터베이스에 접근할 필요 없이 바로 데이터를 사용합니다. Redis에 데이터가 없다면, MySQL 데이터베이스에 접근하여 필요한 데이터를 가져오고, 이를 Redis에 저장하여 이후의 조회를 빠르게 합니다.

  3. 세션 저장소로 Redis 사용: 사용자 세션 정보를 Redis에 저장하여 세션 관리를 더 효율적으로 수행할 수 있습니다.

  4. MyBatis를 통한 데이터베이스 접근: MyBatis를 사용하여 애플리케이션 로직과 데이터베이스 간의 통신을 쉽게 구현할 수 있습니다. SQL 쿼리 및 결과 매핑을 MyBatis 설정 파일이나 애너테이션을 통해 정의할 수 있습니다.

6. 질문

  1. jwt를 이용한 로그인 구현 중입니다. 사실 그냥 전체 맥락도 이해가 덜 된 상태에서 서치하면서 구현하고 있는데 아직도 구조에 대한 이해가 안됩니다.
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    // 세션을 생성하지 않으며, STATELESS로 설정하여, 서버가 상태를 저장하지 않게합니다.
    을 하게 되면 다른 체이지에도 영향을 많이 받을까요? 찾아보니 이렇게 하게 되면 모든 요청에 JWT 토큰이 포함되어야 한다는데 이 모든 요청이 로그인을 뺀 나머지는 토큰을 이용하지 않는 방식을 쓸 수 는 없을 까요?
  • 아래와 같이 추가해뒀는데 문제가 없을까요?
.antMatchers("/**").permitAll() // 모든 경로에 대해 접근을 허용

7. 로그인 파트는 연동 완성(팀 작업 전달하기 위한 메모)

1. 회원 가입의 경우 insert가 잘 넘어오는 것까지 확인

  1. 새별님은 이후 회원가입 유효성 검사나 이메일 검증 추가 후 금주(나)에게 수정 부분 전달해주시기로 함

  1. 테이블을 수정해서 email이 같은 경우 같이 안넘어옴(이메일의 경우 unique라 unique 설정 디비 파일 부분 수정)
CREATE TABLE Member_table (
    member_id VARCHAR(50) NOT NULL,
    member_password VARCHAR(20) NOT NULL,
    member_email VARCHAR(100) NOT NULL UNIQUE,
    member_name VARCHAR(10) NOT NULL,
    member_nickname VARCHAR(80) NOT NULL,
    member_profile_image BLOB NULL, #이미지 저장
    member_introduce VARCHAR(200) NULL,
    member_sns TINYINT NOT NULL DEFAULT 0,
    verificationCode VARCHAR(10) NOT NULL DEFAULT 1,
    PRIMARY KEY (member_id)
) ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4;

  1. 아직 회원가입 후 넘어가는 창은 설정 안해둬서 에러 나와도 신경 안쓰고 데이터베이스에 값이 잘 넘어갔는지만 확인 할 것

2. 비밀번호 찾기, 아이디 찾기

  1. 아이디 찾기

  • 없다면

  1. 비밀번호 찾기를 해서 이메일 인증까지 확인

3. 로그인의 경우 토큰 넘어오는 것 확인

  1. 다만 몇가지 문제는 수정 할 예정
  2. 처음에는 토큰 없음

  1. 로그인 실패시

  1. 데이터베이스에 있는 아이디와 비번이 같다면 로그인 후 추천 페이지로 바로 넘어가게 작업
  2. 네비게이션바에 로그인이 바로 Logout 버튼으로 바뀌지 않는 넘어오지 않는 현상은 수정해야(토큰이 넘어오지 않는 현상 수정 예정)
  • 그렇기 때문에 로그인 후 밑에 와 같이 새로고침 해주셔야 일단 로그아웃으로 바뀝니다ㅠㅠㅠㅠ
    • 해결

      Login.js에서 로컬에만 저장하고 AuthProdvider.js를 이용해서 토큰을 저장안 해줬기 때문에 토큰이 바로 nav에 넘어오지 않고 새로고침해야지만 로컬에 있던 토큰이 넘어왔 것

      const { setToken, setIsLoggedIn } = useAuth(); // AuthContext에서 필요한 값과 함수를 가져옵니다.
      
      axios.request(config)
                  .then((response) => {
                      console.log(JSON.stringify(response.data));
                      if( response.data?.token != undefined) {
                          toast.success('로그인에 성공했습니다.');
                          /*localStorage.setItem("jwt",  response.data?.token);*/
                          setToken(response.data?.token); // 상태에 토큰 저장
                          setIsLoggedIn(true);
      
                          setTimeout(() => {
                              navigate('/recommend');
                          }, 2000);
                      } else {
                          toast.warning('로그인 실패했습니다. 아이디나 비밀번호를 확인해주세요');
                      }
      
                  })
                  .catch((error) => {
                      console.log(error);
                      toast.warning('로그인 실패했습니다. 아이디나 비밀번호를 확인해주세요');
                  });
      
          };

  • 로그인 후 페이지

  • 로그아웃

  • 네비게이션 바에 토큰을 못가져오는 현상 수정
profile
내 지식을 기록하여, 다른 사람들과 공유하여 함께 발전하는 사람이 되고 싶다. gitbook에도 정리중 ~

0개의 댓글