Skip to content

Commit

Permalink
Merge pull request #17 from Caramel1004/feature/#5
Browse files Browse the repository at this point in the history
[#5][FEATURE] Spring Security로 로그인 필터 추가
  • Loading branch information
Caramel1004 authored Apr 18, 2024
2 parents 7ba5db1 + 3b1b4e3 commit d62fbbf
Show file tree
Hide file tree
Showing 25 changed files with 638 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import java.util.List;

import static com.moin.remittance.domain.vo.HttpResponseCode.*;
import static com.moin.remittance.util.ExchangeRateCalculator.*;
import static com.moin.remittance.application.v1.util.ExchangeRateCalculator.*;

@Service
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.moin.remittance.application.v1.util;

public class Bcrypt {
// BCrypt
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.moin.remittance.util;
package com.moin.remittance.application.v1.util;

import org.springframework.stereotype.Component;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.moin.remittance.util;
package com.moin.remittance.application.v1.util;


public class JwtUtil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,12 @@ public TransactionLogV2DTO getRemittanceLogList(String userId) {
.reduce(BigDecimal::add)
.orElse(new BigDecimal(0))
)
.todayTransferCount(remittanceHistory.size())
.todayTransferCount(
remittanceHistory.stream()
.filter(e -> e.getRequestedDate().isEqual(OffsetDateTime.now()))
.toList()
.size()
)
.history(remittanceHistory)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@
@Service
public interface MemberServiceV2 {
void saveUser(MemberDTO dto);

String getAuthToken(String userId, String password);
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,4 @@ public void saveUser(MemberDTO member) {
.idValue(bCryptPasswordEncoder.encode(member.getIdValue()))
.build());
}

@Override
public String getAuthToken(String userId, String password) {
// 유저 존재 여부 체크
boolean hasUser = memberRepositoryV2.existsByUserIdAndPassword(userId, password);

if (!hasUser) { // 찾은 유저가 없으면 throw
throw new NotFoundMemberException(BAD_NOT_MATCH_MEMBER);
}

// return JwtUtil.createToken(userId, key, 30);
return "아직 토큰 구현 안됨";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.moin.remittance.core.configration;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JwtConfigProps {

@Schema(description = "jwt 암호화 키")
@Value("${spring.jwt.SECRET_KEY}")
public String SECRET_KEY;

@Value("${spring.jwt.AUTH_LOGIN_END_POINT}")
public String AUTH_LOGIN_END_POINT;

@Value("${spring.jwt.AUTH_TOKEN_HEADER}")
public String AUTH_TOKEN_HEADER;

@Value("${spring.jwt.AUTH_TOKEN_PREFIX}")
public String AUTH_TOKEN_PREFIX;

@Value("${spring.jwt.AUTH_TOKEN_TYPE}")
public String AUTH_TOKEN_TYPE;
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
package com.moin.remittance.core.configration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.moin.remittance.core.jwt.application.AuthUserDetailService;
import com.moin.remittance.core.jwt.filter.JwtAuthenticationFilter;
import com.moin.remittance.core.jwt.filter.JwtRequestFilter;
import com.moin.remittance.core.jwt.provider.JwtTokenProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import java.util.Collections;
import java.util.List;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurityConfig {

private final AuthenticationConfiguration authenticationConfiguration;
private final JwtTokenProvider jwtTokenProvider;
private final AuthUserDetailService authUserDetailService;
private final JwtConfigProps jwtConfigProps;
private final ObjectMapper objectMapper;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

Expand Down Expand Up @@ -69,17 +87,39 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
* 로그인과 회원가입은 허용해주어야 인증되지 않은 회원이 서비스를 이용할 수 있도록 하는 엔드포인트 이기때문에 이 두개의 엔드포인트는 허용해줍니다.
* */
httpSecurity.authorizeHttpRequests(req -> req
.anyRequest().permitAll()
// .requestMatchers("/api/v1/signup", "/api/v1/test", "/h2-console").permitAll()
// .requestMatchers("/h2-console").permitAll()
.requestMatchers("/api/v2/user/login", "/api/v2/user/signup").permitAll()
.anyRequest().authenticated()
);

/*
* 인증 방식 설정
* - 인메모리 방식
* - JDBC 방식
* - 커스텀 방식
* 적용: 커스텀 방식
* */
httpSecurity.userDetailsService(authUserDetailService);

/*
* 필터 등록
* */
httpSecurity
.addFilterBefore(new JwtRequestFilter(jwtTokenProvider, jwtConfigProps), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(
new JwtAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtTokenProvider, jwtConfigProps, objectMapper),
UsernamePasswordAuthenticationFilter.class);

return httpSecurity.build();
}

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.moin.remittance.core.jwt.application;


import com.moin.remittance.core.jwt.provider.AuthUserDetailsProvider;
import com.moin.remittance.domain.entity.member.v2.MemberEntityV2;
import com.moin.remittance.exception.NotFoundMemberException;
import com.moin.remittance.repository.v2.MemberRepositoryV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import static com.moin.remittance.domain.vo.HttpResponseCode.BAD_NOT_MATCH_MEMBER;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthUserDetailService implements UserDetailsService {

private final MemberRepositoryV2 memberRepositoryV2;

/**
* Spring Security => 유저 정보 조회 => 인증 처리
* */
@Override
public UserDetails loadUserByUsername(String userId) throws InternalAuthenticationServiceException {
log.info("'" + userId + "' 조회 중......");

MemberEntityV2 member = memberRepositoryV2.findByUserId(userId);

/*
* @Exception NotFoundMemberException: DB에 일치하는 유저 없는 경우
* */
if(member == null) {
throw new NotFoundMemberException(BAD_NOT_MATCH_MEMBER);
}

log.info("'" + userId + "' 존재");

return new AuthUserDetailsProvider(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.moin.remittance.core.jwt.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.moin.remittance.core.configration.JwtConfigProps;
import com.moin.remittance.core.jwt.provider.AuthUserDetailsProvider;
import com.moin.remittance.core.jwt.provider.JwtTokenProvider;
import com.moin.remittance.domain.dto.responsebody.HttpResponseBody;
import com.moin.remittance.exception.NotFoundMemberException;
import com.moin.remittance.exception.dto.ErrorResponseDTO;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
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.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;


import java.io.IOException;
import java.util.Iterator;

import static com.moin.remittance.domain.vo.HttpResponseCode.BAD_NOT_MATCH_MEMBER;
import static com.moin.remittance.domain.vo.HttpResponseCode.SUCCESS_MEMBER_LOGIN;


@Slf4j
@Component
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

@Autowired
private final AuthenticationManager authenticationManager;

private final JwtTokenProvider jwtTokenProvider;

private final JwtConfigProps jwtConfigProps;

private final ObjectMapper objectMapper;

public JwtAuthenticationFilter(AuthenticationManager authenticationManager,
JwtTokenProvider jwtTokenProvider,
JwtConfigProps jwtConfigProps, ObjectMapper objectMapper
) {
this.authenticationManager = authenticationManager;
this.jwtTokenProvider = jwtTokenProvider;
this.jwtConfigProps = jwtConfigProps;
this.objectMapper = objectMapper;

super.setAuthenticationManager(authenticationManager);

log.info("authenticationManager" + authenticationManager);
setFilterProcessesUrl(jwtConfigProps.AUTH_LOGIN_END_POINT);
}

/**
* 🔐 인증 시도 메소드
* /login 엔드포인트로 요청 => 필터 => 인증 시도
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

String userId = obtainUsername(request);
String password = obtainPassword(request);

log.info("userId: " + userId);
log.info("password: " + password);

// 인증정보 객체 생성: 스프링 시큐리티에서 username, password 검증하기 위해서는 token에 담아야 함
Authentication authentication = new UsernamePasswordAuthenticationToken(userId, password);
log.info("authentication: " + authentication);

// 사용자 인증 여부를 판단하는 객체
authentication = authenticationManager.authenticate(authentication);

log.info("인증 여부: " + authentication.isAuthenticated());

if (!authentication.isAuthenticated()) {
log.info("인증 실패!!!");
throw new NotFoundMemberException(BAD_NOT_MATCH_MEMBER);
}

return authentication;
}

@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain,
Authentication authentication
) throws IOException, ServletException {
AuthUserDetailsProvider member = (AuthUserDetailsProvider) authentication.getPrincipal();

Iterator<? extends GrantedAuthority> iterator = member.getAuthorities().iterator();
String idType = iterator.next().getAuthority();
String userId = member.getUsername();

String jwt = jwtTokenProvider.createAuthorizationToken(userId, idType, 60 * 60 * 30);

log.info("jwt: " + jwt);


//스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(member, null);

SecurityContextHolder.getContext().setAuthentication(authToken);

// JSON 으로 변환하여 응답 본문에 작성
response.addHeader(jwtConfigProps.AUTH_TOKEN_HEADER, jwtConfigProps.AUTH_TOKEN_PREFIX + jwt);
response.setStatus(SUCCESS_MEMBER_LOGIN.getStatusCode());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(HttpResponseBody.builder()
.statusCode(SUCCESS_MEMBER_LOGIN.getStatusCode())
.message(SUCCESS_MEMBER_LOGIN.getMessage())
.codeName(SUCCESS_MEMBER_LOGIN.getCodeName())
.token(jwt)
.build()
)
);
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
// JSON 으로 변환하여 응답 본문에 작성
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(ErrorResponseDTO.builder()
.code(BAD_NOT_MATCH_MEMBER.getStatusCode())
.message(BAD_NOT_MATCH_MEMBER.getMessage())
.codeName(BAD_NOT_MATCH_MEMBER.getCodeName())
.build()
)
);
}
}
Loading

0 comments on commit d62fbbf

Please sign in to comment.