일단, redius까지는 구성하지 않으려고 한다. 어쨋든 이게 다른 팀원들이랑 작업하는데.. redius까지 구성하기엔 좀 힘들어할 수도 있겠다라는 생각이 들었음
🔗 관련 정리 글 : 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");
}
}
🔗 관련 정리 글 : 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는 Java 객체와 SQL 데이터베이스 사이의 매핑을 담당하는 프레임워크
SqlSession
객체를 생성SqlSessionFactory
가 생성SqlSessionFactory
는 필요에 따라 SqlSession
객체를 생성되며, SqlSession
은 실제로 SQL 쿼리를 실행MyBatis-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;
}
}
SqlSessionFactory
와 SqlSessionTemplate
빈을 생성하고 구성SqlSessionTemplate
이 주입사용자가 서버에 접근할 때, 이 사용자가 인증된 사용자인지 확인하는 방법
세션 기반 인증
: 사용자마다 사용자의 정보를 담은 세션을 생성하고 저장해서 인증을 하는 방식토큰
은 서버에서 클라이언트를 구분하기 위한 유일한 값, 서버가 토큰을 생성해서 클라이언트에게 제공하면, 클라이언트는 이 토큰을 가지고 있다가 여러 요청을 이 토큰과 함께 신청함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();
}
}
HMAC(hash-based message authentication) 기법
발급받은 JWT를 이용해서 인증을 하려면 HTTP 요청 헤더 중에 Authorization 키값에 Bearer + JWT 토큰값을 넣어 보내야 한다.
JSON 객체
로 안전하게 전송하기 위한 간결하고 독립적인 방법aaaaa.bbbbb.ccccc
{
"typ" : "JWT",
"alg" : "HS256"
}
공개 클레임
: 공개되어도 상관없는 클레임을 의미, 충돌 방지할 수 있는 이름을 가져야 하며, 보통 클레임 이름을 URI로 짓는다.비공개 클레임
: 공개되면 안 되는 클레임을 의미, 클라이언트와 서버 간의 통신에 사용등록된 클레임
: 예약된 클레임 이름을 가지며, JWT 사용자와 생산자 사이에 정의되어 있다.iss
, exp
, sub
, aud
등이 이에 해당되며, 이들 클레임은 모두 선택적{
"sub": "1234567890", // 등록된 클레임: 주체(토큰이 의미하는 사용자/주체)
"name": "John Doe", // 비공개 클레임: 사용자 정의 클레임 (이름 정보)
"iat": 1516239022, // 등록된 클레임: 발급 시간 (토큰이 발급된 시간)
"exp": 1627651727, // 등록된 클레임: 만료 시간 (토큰이 만료되는 시간)
"admin": true, // 비공개 클레임: 사용자 정의 클레임 (어드민 여부)
"https://example.com/is_root": true // 공개 클레임: 충돌을 방지하기 위해 URI 형식을 가진 클레임
}
Base64
로 인코딩하고, 이 두 문자열을 점(.
)으로 연결한 후, 비밀 키를 사용하여 이 문자열을 서명 해시값 생성공지
를 작성할 수 있게 해줘야 함 + 권한 설정 필요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로 리다이렉트
}
}
}
패키지
는 팀별 논의를 통해 이런식으로 구성 //시큐리티
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'
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로 리다이렉트
}
}
}
스프링의
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;
}
}
사용자가 로그인을 시도할 때, 아이디와 비밀번호를 체크하는 로직을 구현, 로그인이 성공하면 JWT 토큰을 생성하여 사용자에게 반환
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();
}
}
@Autowired
private AuthenticationManager authenticationManager;
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를 반환
}
}
관련 설명
- 인증 예외 처리
사용자 인증이 실패하면 적절한 예외를 반환하도록 해야 합니다. 이를 위해try-catch
블록을 사용하거나,AuthenticationException
을 처리하는 예외 핸들러를 구현해야 합니다.- HTTP 상태 코드
인증이 성공하면200 OK
와 함께 토큰을 반환하고, 인증이 실패하면401 Unauthorized
를 반환해야 합니다.- 사용자 상세 정보
인증이 성공한 후에Authentication
객체를 사용하여 안전하게 사용자 세부 정보를 가져올 수 있습니다.- 클래스는 클라이언트로부터 로그인 요청을 받아, 사용자 인증을 수행하고 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;
}
}
사용자 정보를 저장할
사용자 모델
과, 데이터베이스와의 상호작용을 담당할레파지토리
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();
}
}
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; //인증번호
}
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;
}
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;
}
}
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);
}
<?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>
package com.in4mation.festibook.exception;
public class LoginException extends Exception {
// 생성자에서 상위 클래스의 생성자를 호출하여
// 예외 메시지를 설정합니다.
public LoginException(String message) {
super(message);
}
}
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;
// 토큰을 페이지 전역을 관리하기 위한 코드
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;
};
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>
);
}
Redis는 다양한 프로그래밍 언어에서 사용할 수 있도록 클라이언트 라이브러리를 제공하며, 다양한 옵션과 설정으로 확장성과 유연성을 제공
키-값 스토어의 형태
를 가지며, 문자열, 해시, 리스트, 셋, 정렬된 셋 등 다양한 데이터 타입을 지원사용방법
캐싱으로 Redis 사용: 애플리케이션은 먼저 Redis를 조회하여 필요한 데이터가 있는지 확인합니다. Redis에 데이터가 있다면, 데이터베이스에 접근할 필요 없이 바로 데이터를 사용합니다. Redis에 데이터가 없다면, MySQL 데이터베이스에 접근하여 필요한 데이터를 가져오고, 이를 Redis에 저장하여 이후의 조회를 빠르게 합니다.
세션 저장소로 Redis 사용: 사용자 세션 정보를 Redis에 저장하여 세션 관리를 더 효율적으로 수행할 수 있습니다.
MyBatis를 통한 데이터베이스 접근: MyBatis를 사용하여 애플리케이션 로직과 데이터베이스 간의 통신을 쉽게 구현할 수 있습니다. SQL 쿼리 및 결과 매핑을 MyBatis 설정 파일이나 애너테이션을 통해 정의할 수 있습니다.
.antMatchers("/**").permitAll() // 모든 경로에 대해 접근을 허용
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;
해결
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('로그인 실패했습니다. 아이디나 비밀번호를 확인해주세요');
});
};