GitHub - whitewise95/voyage99: 항해99에서 진행한 과제 및 프로젝트 모음

항해99에서 진행한 과제 및 프로젝트 모음. Contribute to whitewise95/voyage99 development by creating an account on GitHub.

github.com

1. 프로젝트 소개

본인의 개성을 더해줄 버킷리스트 작성!! 주요 기능

 

2. 프로젝트 목적

FE개발자(리액트)와 BE개발자(스프링)가 5주차까지 배운 내용을 기반으로 협력을 통해 개발하는 프로젝트



3. 프로젝트 개요

다들 목표를 말만하고 못지키느라 힘드시죠?? 작심삼일은 여기서 해결하세요!



4. 개발 환경(BE)

언어 및 개발툴

  • java
  • 인텔리제이

프레임워크

서버환경

형상관리 툴 그리고 DB

라이브러리(gradle)

  • lombok
    implementation 'org.projectlombok:lombok:1.18.22'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok:1.18.22'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.22'
  • mysql
    runtimeOnly 'mysql:mysql-connector-java'
  • H2
    runtimeOnly 'com.h2database:h2'
  • security
implementation 'org.springframework.boot:spring-boot-starter-security'
  • jwt
 implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'



5. 와이어프레임



6. API 설계

로그인 API

버킷리스트 CURD



7. ERD



8. BE 기여도(주관적) 총 3 명

  • 회원가입 - 100%
  • 아이디중복검사 - 100%
  • 닉네임 중복검사 - 0%
  • 로그인(시큐리티) - 50%
  • 게시글 작성 - 0%
  • 게시글 수정 - 0%
  • 게시글 삭제 - 0%
  • 게시글 조회 - 0%
  • 프로젝트 초기 세팅 - 100%
  • 서버 배포 - 100%
  • 형상관리 - 50%



#COMMENT

1.0버전 COMMENT

1) 힘들었던점

BE서버와 FE서버가 따로 있어 CORS문제를 많이 겪었다.

  • 1-1) 시큐리티를 사용했을 땐 SecurityConfig에 CORS를 설정해줘야한다.
  • 1-2) 시큐리티를 사용하지 않을 땐 WebConfig를 만들어 CORS를 설정해줘야한다.
  • 1-3) 프론트와 서버가 함께 있을 땐 시큐리티를 사용해도 세션쪽 문제를 겪지 않았는데 CORS 때문에 세션을 받으려면 설정해줘야하는 것들이 많다.
  • 처음엔 세션문제 때문에 해결하지 못해서 jwt로 넘어가 jwt를 사용하기로 하였다.

2) 성장한 점

  • 2-1) 협력을 한다는게 정말 어렵다는 것을 알게 되었다.
    • 개발자로 일했을 땐 모르게 많아 그냥 말을 듣고 지시에만 개발을 했지만 같은 위치에서 협력하여 개발하는건 의견 차이도 많고 합의점을 찾는게 힘들었 던 것 같다.
  • 2-2) 상대방을 존중하는 방법을 배웠다.
    • 처음에는 내가 맞다는 씩으로 얘기를 했다면 내가 틀린 점 또는 정확하지 않을 수도 있으니 나는 이렇게 생각하고 있는데 이 부분은 어떻게 생각하는지 여쭤보는 방식으로 말하게 된 거같다.
  • 2-3) 프론트엔트 개발자와 협력하는 방법을 알게되었다.
    • 물론 프론트엔드 개발자도 잘하지만 내가 이런 API를 만드려고하는데 요청값은 이렇게 줘야하고 응답값은 프론트단에서 이렇게 사용하도록 이렇게 내려 줄 예정인데 괜찮은지 협의하는 과정을 알게되었다.
  • 2-4) 프로젝트 초기 설계 및 형상관리 브랜치 설계등에 자심감을 얻게 되었다.
    • 프로젝트 초기에 gradle 및 공동으로 사용하는 부분을 먼저 설계하는 방법에 익숙해졌고 기존에 협업툴 소스트리 기본 사용법은 익숙했지만 병합은 안해보고 브랜치 나누는 설계도 해보지않아 이번 기회에 익숙해졌다.

3) Develop 계획

  1. view단을 좀 더 나의 스타일로 변경하고 싶어서 변경해 볼 예정이다.
  2. 세션방식으로 변경해서 시큐리티를 이용해 볼 예정이다.

 

 

프로젝트 주요 기능

 

CORS 설정

  • spring security를 사용하고 있어 WebSecurityConfigurerAdapter를 상속받고 있는 클래스에서 CORS를 설정해줬다.
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("http://localhost:3000");
        configuration.addAllowedHeader("Content-Type");
        configuration.addAllowedHeader("Custom-Header");
        configuration.addAllowedMethod(HttpMethod.POST);
        configuration.addAllowedMethod(HttpMethod.GET);
        configuration.addAllowedMethod(HttpMethod.POST);
        configuration.addAllowedMethod(HttpMethod.PUT);
        configuration.addAllowedMethod(HttpMethod.OPTIONS);
        configuration.addAllowedMethod(HttpMethod.DELETE);
        configuration.setAllowCredentials(true);
        configuration.addExposedHeader(AUTH_HEADER);
        configuration.addAllowedHeader(AUTH_HEADER);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

로그인 구현

    1. 로그인을 시도 했을 경우 Filter에의해 인터셉트된다.
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    final private ObjectMapper objectMapper;

    public LoginFilter(final AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
        objectMapper = new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /*
     *
     * UsernamePasswordAuthenticationToken 객체는 클라이언트에서 가져온 정보를 진짜 인증을 담당할
     * AuthenticationManager(ProviderManager) 인터페이스에게
     * 인증용 객체(UsernamePasswordAuthenticationToken)로 만들어 주는 로직이다.
     * */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        UsernamePasswordAuthenticationToken authRequest;
        try {
            JsonNode requestBody = objectMapper.readTree(request.getInputStream());
            String username = requestBody.get("username").asText();
            String password = requestBody.get("password").asText();
            authRequest = new UsernamePasswordAuthenticationToken(username, password);
        } catch (Exception e) {
            throw new RuntimeException("username, password 입력이 필요합니다. (JSON)");
        }

        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

 

    1. UserDetailsService를 implements하고 있는 클래스의 로직이 수행되어 로그인된 정보를 이용해 DB에서 유저를 찾습니다.
@Service
public class PrincipalDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Autowired
    public PrincipalDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username).orElseThrow(
                () -> new UsernameNotFoundException("해당 유저가 없습니다."));

        return new PrincipalDetails(userEntity);
    }
}

 

    1. SuccessHandler를 만들어 로그인이 성공했을 때 토큰을 생성할 수 있는데 로직을 구현
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    public static final String AUTH_HEADER = "Authorization";
    public static final String TOKEN_TYPE = "BEARER";

    @Override
    public void onAuthenticationSuccess(final HttpServletRequest request,
                                        final HttpServletResponse response,
                                        final Authentication authentication) {

        if (!Optional.ofNullable(authentication.getPrincipal()).isPresent()) {
            response.addHeader(AUTH_HEADER, null);
        } else {
            final PrincipalDetails userDetails = ((PrincipalDetails) authentication.getPrincipal());
            // Token 생성
            final String token = JwtTokenUtils.generateJwtToken(userDetails);
            response.addHeader(AUTH_HEADER, TOKEN_TYPE + " " + token);
        }
    }
}

 


토근사용

로그인이 필요한 기능은 토큰을 헤더에 담아 기능요청을 합니다.

  • 토큰을 헤더에 담아 보냈는지 확인하는 메소드
    private void validCheck(String token) {
        if (!validToken(token)) {
            throw new IllegalArgumentException("로그인이 필요한 기능입니다.");
        }
    }

    public boolean validToken(String token) {
        if (token == null || token.equals("") || token.length() < HEADER_PREFIX.length()) {
            return false;
        }
        return true;
    }



  • token 앞에 BEARER가 붙었는지 유효성체크 붙었으면 subString
    public String tokenProcess(String token) {
        String bearer = token.substring(
                0,
                TOKEN_TYPE.length()
        );

        if (bearer.equals("BEARER")) {
            return token.substring(
                    TOKEN_TYPE.length()
            );
        }
        throw new IllegalArgumentException("유효하지 않은 토큰");
    }



  • 토큰 유효기간 및 검증 로직
@Component
public class JwtDecoder {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    public String decodeUsername(String token) {
        Claims claimMap = null;
        try {
            claimMap = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(JWT_SECRET)) // Set Key
                    .parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
                    .getBody();

            //Date expiration = claims.get("exp", Date.class);
            //String data = claims.get("data", String.class);
        } catch (ExpiredJwtException e) { // 토큰이 만료되었을 경우
            System.out.println(e);
        } catch (Exception e) { // 그외 에러났을 경우
            System.out.println(e);
        }
        return claimMap.get(CLAIM_USER_NAME).toString();
    }
}
복사했습니다!