목차
JWT를 이용한 회원가입 로그인
본격적인 프로젝트를 구성하면서 가장 먼저 회원가입 로그인을 구현하려 한다.
Spring boot를 이용해서 인증 인가 메커니즘을 구현하기 위해 Spring security 와 JWT를 이용할 것이다.
가장 먼저 JWT 설정을 해주기 위해 jwt와 security관련 의존성 주입을 해주었다.
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
TokenProvider.java
InitializingBean을 사용하지 않고, @Value 애노테이션을 사용하여 초기화 방식을 처리했다.
@Component
@Getter
@Slf4j
public class TokenProvider {
// 토큰에서 권한 정보를 저장하는 클레임(Claim)의 키
private static final String AUTHORITIES_KEY = "auth";
// 생성되는 토큰의 타입을 나타내는 문자열
private static final String BEARER_TYPE = "bearer";
// 생성된 액세스 토큰의 유효 기간 (30분)
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 600 * 30;
// JWT 서명을 생성하기 위한 키
private final Key key;
// 주의점: 여기서 @Value는 `springframework.beans.factory.annotation.Value` 소속이다! lombok의 @Value와 착각하지 말 것!
// secretKey 값을 통해 JWT 서명을 생성하기 위한 키를 초기화
public TokenProvider(@Value("${jwt.secret}") String secretKey) {
log.info(secretKey);
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
// 토큰 생성
public TokenDto generateTokenDto(Authentication authentication) {
// 사용자의 권한 정보를 쉼표로 구분하여 문자열로 변환
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
// 현재 시간
long now = (new Date()).getTime();
// 토큰 만료 시간 설정 (현재 시간으로부터 30분 뒤)
Date tokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
// JWT 토큰 생성
String accessToken = Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.setExpiration(tokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
// TokenDto 객체 생성 및 반환
return TokenDto.builder()
.grantType(BEARER_TYPE)
.accessToken(accessToken)
.tokenExpiresIn(tokenExpiresIn.getTime())
.build();
}
// 토큰을 사용하여 인증 객체 생성
public Authentication getAuthentication(String accessToken) {
// 토큰에서 클레임(Claim) 파싱
Claims claims = parseClaims(accessToken);
// 권한 정보가 없는 경우 예외 처리
if (claims.get(AUTHORITIES_KEY) == null) {
throw new RuntimeException("권한 정보가 없는 토큰입니다.");
}
// 토큰의 권한 정보를 가져와서 GrantedAuthority 객체로 변환하여 Collection 생성
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// UserDetails 객체 생성
UserDetails principal = new User(claims.getSubject(), "", authorities);
// UsernamePasswordAuthenticationToken을 사용하여 인증 객체 생성
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
// 토큰의 유효성 검증
public boolean validateToken(String token) {
try {
// 토큰의 서명 검증
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
// 토큰의 클레임 파싱
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
WebSecurityConfig.java
- WebSecurityConfigurerAdapter을 상속받아서 구현하는 방식은 spring security 5.7 이상부터 사용을 권하지 않는다고 한다.
- react로 프론트 개발을 진행하였기 때문에 cors설정도 해주었다.
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@Component
public class WebSecurityConfig {
// TokenProvider 객체
private final TokenProvider tokenProvider;
// JWT 인증 진입점
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// JWT 접근 거부 핸들러
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
// 비밀번호 인코더 빈 등록
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// SecurityFilterChain 빈 등록
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(new JwtSecurityConfig(tokenProvider))
.and()
.cors(cors -> {
CorsConfigurationSource configurationSource = request -> {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(List.of("*"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(Duration.ofHours(1));
return configuration;
};
cors.configurationSource(configurationSource);
});
return http.build();
}
}
JwtFilter.java
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
// Authorization 헤더의 이름
public static final String AUTHORIZATION_HEADER = "Authorization";
// Bearer 토큰 접두사
public static final String BEARER_PREFIX = "Bearer ";
// TokenProvider 객체
private final TokenProvider tokenProvider;
// 토큰 추출
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
// Bearer 접두사 제거 후 토큰 반환
return bearerToken.substring(7);
}
return null;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 토큰 추출
String jwt = resolveToken(request);
// 토큰이 유효하고 인증 정보를 가져올 수 있는 경우
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
// 인증 정보를 SecurityContext에 설정
Authentication authentication = tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 다음 필터로 이동
filterChain.doFilter(request, response);
}
}
JwtSecurityConfig.java
package com.example.Capstone.config;
import com.example.Capstone.jwt.JwtFilter;
import com.example.Capstone.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
// TokenProvider 객체
private final TokenProvider tokenProvider;
// HttpSecurity 구성
@Override
public void configure(HttpSecurity http) {
// JwtFilter 객체 생성
JwtFilter customFilter = new JwtFilter(tokenProvider);
// UsernamePasswordAuthenticationFilter 앞에 JwtFilter 추가
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
AuthService.java
@Service
@RequiredArgsConstructor
@Transactional
public class AuthService {
private final AuthenticationManagerBuilder managerBuilder;
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final TokenProvider tokenProvider;
public MemberResponseDto signup(MemberRequestDto requestDto) {
if (memberRepository.existsByEmail(requestDto.getEmail())) {
throw new RuntimeException("이미 가입되어 있는 유저입니다");
}
Member member = requestDto.toMember(passwordEncoder);
return MemberResponseDto.of(memberRepository.save(member));
}
public TokenDto login(MemberRequestDto requestDto) {
UsernamePasswordAuthenticationToken authenticationToken = requestDto.toAuthentication();
Authentication authentication = managerBuilder.getObject().authenticate(authenticationToken);
return tokenProvider.generateTokenDto(authentication);
}
}
MemberService.java
-아래와 같이 설정해주면서 현재 로그인된 사용자 정보를 가져올 수 있도록 하였다.
public MemberResponseDto getMyInfoBySecurity() {
return memberRepository.findById(SecurityUtil.getCurrentMemberId())
.map(MemberResponseDto::of)
.orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
}
'프로젝트 > 공유 캘린더' 카테고리의 다른 글
공유 캘린더 만들기 #5 (API 문서화 및 UI 구성) (0) | 2023.08.03 |
---|---|
공유 캘린더 만들기 #4 (Service 및 스케줄러 디테일 구성) (0) | 2023.07.17 |
공유 캘린더 만들기 #3 (Entity 구성) (0) | 2023.07.15 |
공유 캘린더 만들기 #1 (Spring boot + Docker + AWS) (0) | 2023.07.06 |
목차
1. 프로젝트 소개 및 목적
- 일반적으로 캘린더 하면 생각나는 기능에 사용자들끼리 일정을 공유하고 소통 할 수 있도록 하는 공유 캘린더를 만들고자 한다. 예를 들어 친구들과 약속을 잡는다 했을때 일정을 함께 공유하고 추가하여 서로에게 보이도록, 또한 일정이 다가왔을때 알림을 줄 수 있도록 한다.
단일 일정 뿐만 아니라 그룹일정도 구성하여, 그룹 인원들이 스케줄을 공유 할 수 있도록 한다.
2. 사용 기술
- Spring boot
- Spring security
- JWT
- Spring Data JPA
- Docker
- AWS EC2, S3, Code Deploy, RDS(MySQL)
- Github actions
- Swagger
본 프로젝트에서 Back-End 부분을 담당했기 때문에 프론트 부분은 포함되어있지 않습니다.
프로젝트 초기 구성
프로젝트가 프론트 둘 백엔드 하나로 구성되어 있기 때문에 가장 먼저 서버 배포를 구현하였습니다.
서버 배포는 AWS EC2 인스턴스를 이용했고, git에 배포하는 과정도 자동 배포화를 이용하여 효율적인 프로젝트 구성을 준비하였습니다.
AWS 프리티어를 사용하고 있으므로, 메모리가 부족 할 경우를 대비하여
AWS 공식 홈페이지에 있는 스왑 파일을 이용한 메모리 확보 방법을 사용했습니다.
https://repost.aws/ko/knowledge-center/ec2-memory-swap-file
스왑 파일을 사용하여 Amazon EC2 인스턴스의 스왑 공간으로 메모리 할당
Amazon Elastic Compute Cloud(Amazon EC2) 인스턴스에서 스왑 파일로 사용할 메모리를 할당하려고 합니다. 어떻게 해야 하나요?
repost.aws
자동 배포화 하는 과정은 아래 글을 참고하시면 됩니다.
https://jeonsg99.tistory.com/15
Spring boot + Docker + Github Actions + AWS 를 이용한 자동 배포화
과정 1. Github main 브랜치에 Push 2. Github Actions에서 AWS S3에 빌드 파일 및 Dockerfile, deploy.sh 등 업로드 3. Github Actions이 AWS CodeDeploy에 배포 요청 4. CodeDeploy가 배포 실행 5. 도커 빌드 및 실행 위와 같은
jeonsg99.tistory.com
'프로젝트 > 공유 캘린더' 카테고리의 다른 글
공유 캘린더 만들기 #5 (API 문서화 및 UI 구성) (0) | 2023.08.03 |
---|---|
공유 캘린더 만들기 #4 (Service 및 스케줄러 디테일 구성) (0) | 2023.07.17 |
공유 캘린더 만들기 #3 (Entity 구성) (0) | 2023.07.15 |
공유 캘린더 만들기 #2 (JWT를 이용한 회원가입 로그인) (0) | 2023.07.15 |