enyo9rt / NewsCommunity-bFinal

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

๐Ÿ’š ํ• ๋จธ๋‹ˆ๋Š” ๋‹ค ๋“ค์–ด์ฃผ์…”

์Šคํฌ์ธ  ๋‰ด์Šค ์ปค๋ฎค๋‹ˆํ‹ฐ


1. ์ œ์ž‘ ๊ธฐ๊ฐ„ & ์ฐธ์—ฌ ์ธ์›

  • 2022๋…„ 6์›” 24์ผ ~ 7์›” 29์ผ
  • 4์ธ ํŒ€ ํ”„๋กœ์ ํŠธ

2. ์‚ฌ์šฉ ๊ธฐ์ˆ 

Back-end

  • Java 11
  • Spring Boot 2.7.2
  • Gradle 7.4.1
  • Spring Data JPA
  • MySQL 8.0.28
  • Spring Security

Front-end

  • HTML5
  • CSS3
  • Javascript
  • Bulma
  • BootStrap
  • JQuery

3. ์„œ๋น„์Šค ์†Œ๊ฐœ

๋„ค์ด๋ฒ„์˜ ์Šคํฌ์ธ  ๋‰ด์Šค๋ฅผ ์Šคํฌ๋ž˜ํ•‘ํ•˜์—ฌ ํด๋กœ๋ฐ” ์š”์•ฝ API๋กœ ๊ฐ ๋‰ด์Šค๋ฅผ ์š”์•ฝํ•ด ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.
ํšŒ์›๋“ค์ด ๋‰ด์Šค์— ๋Œ€ํ•œ ์˜๊ฒฌ์„ ๋Œ“๊ธ€๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋งˆ์Œ์— ๋“œ๋Š” ๋‰ด์Šค๋ฅผ ๋ถ๋งˆํฌํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ํšŒ์›์˜ ๋Œ“๊ธ€๊ณผ ๋ถ๋งˆํฌ๋ฅผ ๋ชจ์•„๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ ์ •๋ณด ๋ณด๊ธฐ
API ๋ช…์„ธ์„œ ๋ณด๊ธฐ

default.mp4

4. ERD ์„ค๊ณ„


5. ๋‹ด๋‹น ๊ธฐ๋Šฅ

์ธ์ฆ๊ณผ ๋”๋ถˆ์–ด ํšŒ์›๊ณผ ๊ด€๋ จ๋œ ๊ธฐ๋Šฅ (ํšŒ์›๊ฐ€์ž…, ํ”„๋กœํ•„ ๋“ฑ) ์„ ๋‹ด๋‹นํ–ˆ์Šต๋‹ˆ๋‹ค.
์•„๋ž˜์˜ ํ† ๊ธ€ ํ•ญ๋ชฉ์—์„œ ์ฃผ๋œ ๊ธฐ๋Šฅ๋“ค์˜ ์ฝ”๋“œ์™€ ๊ฐ„๋žตํ•œ ์„ค๋ช…์„ ๋ณด์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํšŒ์› ์ƒ์„ฑ


  • ํŠธ๋žœ์žญ์…˜์œผ๋กœ ํšŒ์› ๊ฐ์ฒด๋ฅผ ์ €์žฅํ•  ๋•Œ ๊ธฐ๋ณธ ๊ถŒํ•œ๊ณผ ํ”„๋กœํ•„์„ ํ•จ๊ป˜ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
    public String signUp(SignupRequestDto dto) throws HibernateException {
    User user = new User(dto);
    saveUser(user);
    Role role = getRole(new Role(RoleType.USER).getName());
    try {
    if (role == null) {
    saveRole(new Role(RoleType.USER));
    }
    addRoleToUser(user.getUsername(), RoleType.USER);
    // ๊ธฐ๋ณธ ํ”„๋กœํ•„ ์ถ”๊ฐ€
    defaultProfile(user);
    } catch (DataIntegrityViolationException | ConstraintViolationException e) {
    log.error("Faild to sign up");
    throw new HibernateException("Failed to add role or profile to user cause=", e.getCause());
    }
    return "success";
    }


์ธ์ฆ

  • Spring Security๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•„ํ„ฐ์—์„œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
    html form์œผ๋กœ ์ž…๋ ฅ๋ฐ›์€ ๊ฐ’์„ HttpServletRequest ๊ฐ์ฒด์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
    dto ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์œ ํšจ์„ฑ ๊ฒ€์ฆ ํ›„ UserDetailsService์— ์ „๋‹ฌํ•˜์—ฌ ์กฐํšŒํ•˜๊ณ  UserDetails ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ User ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. UsernamePasswordAuthenticationToken์„ ์ƒ์„ฑ, AuthenticationManager์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    UsernamePasswordAuthenticationToken authenticationToken = null;
    try {
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    SigninRequestDto requestDto = new SigninRequestDto(username, password);
    User user = (User) userDetailsService.loadUserByUsername(requestDto.getUsername());
    authenticationToken = new UsernamePasswordAuthenticationToken(user, requestDto.getPassword());
    } catch (IllegalArgumentException e) {
    throw AuthException.builder()
    .message(e.getMessage())
    .code("A401")
    .build();
    } catch (NullPointerException e) {
    throw AuthException.builder()
    .message("์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์•„์ด๋”” ํ˜น์€ ๋น„๋ฐ€๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค.")
    .code("A401")
    .build();
    }
    return authenticationManager.authenticate(authenticationToken);
    }

  • ์ธ์ฆ์— ์„ฑ๊ณตํ•˜๋ฉด JWT๋ฅผ ๋ฐœ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค.
    ํ† ํฐ์€ ์ ‘๊ทผ ํ† ํฐ๊ณผ ๊ฐฑ์‹  ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž ID์™€ ํ•จ๊ป˜ DB์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
    ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์—์„œ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ID๋ฅผ ํ•„์š”๋กœ ํ•˜๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ค๋”์— ์‚ฌ์šฉ์ž ID๋„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
    User user = (User) authentication.getPrincipal();
    Algorithm algorithm = Algorithm.HMAC256("secretKey".getBytes());
    String access_token = JWT.create()
    .withSubject(user.getUsername())
    .withExpiresAt(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
    .withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
    .sign(algorithm);
    String refresh_token = JWT.create()
    .withSubject(user.getUsername())
    .withExpiresAt(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000))
    .sign(algorithm);
    String username = user.getUsername();
    Tokens existingTokens = tokensRepository.findByUsername(username);
    if (existingTokens == null) {
    // ์œ ์ €๊ฐ€ ํ† ํฐ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š์œผ๋ฉด ์ƒ์„ฑ ํ›„ DB ์ €์žฅ
    Tokens newTokens = Tokens.builder()
    .username(username)
    .accessToken(access_token)
    .refreshToken(refresh_token)
    .build();
    tokensRepository.save(newTokens);
    } else {
    // ์œ ์ €๊ฐ€ ํ† ํฐ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉด ๋ณ€๊ฒฝ ํ›„ DB ์ €์žฅ
    existingTokens.update(access_token, refresh_token);
    tokensRepository.save(existingTokens);
    }
    // ์‘๋‹ต ํ—ค๋”์— ํ† ํฐ๊ณผ ์‚ฌ์šฉ์ž ID ์ถ”๊ฐ€
    byte[] usernameHeader = username.getBytes(StandardCharsets.UTF_8);
    response.setHeader("token", access_token);
    response.setHeader("username", Base64.getEncoder()
    .encodeToString(usernameHeader));
    ResponseCookie refresh = ResponseCookie.from("ref_uid", refresh_token)
    .maxAge(7 * 24 * 60 * 60)
    .httpOnly(true)
    .secure(true)
    .sameSite("None")
    .path("/")
    .build();
    response.setHeader(SET_COOKIE, refresh.toString());
    response.setContentType(APPLICATION_JSON_VALUE);
    new ObjectMapper().writeValue(response.getOutputStream(), "success");
    }
    }


์ธ๊ฐ€

  • Spring Security๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•„ํ„ฐ์—์„œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
    ์ ‘๊ทผ ํ† ํฐ์„ ํ’€์–ด ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ํ™•์ธํ•˜๊ณ  DB์— ์ €์žฅ๋œ ํ† ํฐ ๊ฐ’์œผ๋กœ ์žฌํ™•์ธ ํ•ฉ๋‹ˆ๋‹ค.
    ์ •์ƒ์ ์ธ ์ ‘๊ทผ์ด๋ผ๋ฉด UserDetails ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ User ๊ฐ์ฒด์™€ ๊ถŒํ•œ์œผ๋กœ UsernamePasswordAuthenticationToken์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    SecurityContext์— ๋ณด๊ด€ํ•ฉ๋‹ˆ๋‹ค.
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // ์ธ๊ฐ€ ๊ณผ์ •์„ ๊ฑฐ์น  ํ•„์š”๊ฐ€ ์—†๋Š” ์š”์ฒญ
    if (request.getServletPath().equals("/api/login") ||
    request.getServletPath().startsWith("/api/signup") ||
    request.getServletPath().equals("/api/token/refresh")) {
    filterChain.doFilter(request, response);
    } else {
    // ์ ‘๊ทผ ํ† ํฐ์ด ์žˆ์œผ๋ฉด ์š”์ฒญ์— ํ•„์š”ํ•œ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
    String authorizationHeader = request.getHeader(AUTHORIZATION);
    if (authorizationHeader != null && authorizationHeader.startsWith(AuthConstants.TOKEN_TYPE)) {
    try {
    String access_token = authorizationHeader.substring(AuthConstants.TOKEN_TYPE.length());
    Algorithm algorithm = Algorithm.HMAC256("secretKey".getBytes());
    JWTVerifier verifier = JWT.require(algorithm).build();
    DecodedJWT decodedJWT = verifier.verify(access_token);
    String username = decodedJWT.getSubject();
    // ํ•ด๋‹น ์œ ์ €์˜ ํ—ˆ์šฉ๋œ ํ† ํฐ๊ฐ’๊ณผ ๋น„๊ต
    Tokens tokens = tokensRepository.findByUsername(username);
    if (tokens == null)
    throw AuthException.builder().message("ํ—ˆ์šฉ ํ† ํฐ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.").invalidValue("์‚ฌ์šฉ์ž ID: " + username).code("A403").build();
    String allowedToken = tokens.getAccessToken();
    if(!allowedToken.equals(access_token))
    throw AuthException.builder().message("ํ—ˆ์šฉ๋œ ์ ‘๊ทผ ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค.").invalidValue("์ ‘๊ทผ ํ† ํฐ: " + access_token).code("A404").build();
    String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
    Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
    stream(roles).forEach(role -> {authorities.add(new SimpleGrantedAuthority(role));});
    User user = (User) userDetailsService.loadUserByUsername(username);
    UsernamePasswordAuthenticationToken authenticationToken =
    new UsernamePasswordAuthenticationToken(user, null, authorities);
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    filterChain.doFilter(request, response);
    } catch (TokenExpiredException e) {
    throw AuthException.builder().message("์ ‘๊ทผ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.").code("A406").build();
    } catch (JWTVerificationException e) {
    throw AuthException.builder().message("์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค.").code("A402").build();
    }
    } else {
    filterChain.doFilter(request, response);
    }
    }
    }
    }


ํ”„๋กœํ•„ ์ˆ˜์ •

  • ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ID๋กœ ๊ธฐ์กด ํ”„๋กœํ•„ ์ •๋ณด๋ฅผ ์ฐพ๊ณ , ์ž…๋ ฅ๋ฐ›์€ ํ”„๋กœํ•„ ์ •๋ณด๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
    ํ”„๋กœํ•„ ์‚ฌ์ง„์€ aws sdk ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ s3์— ์—…๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.
    public String updateProfile(String username, ProfileRequestDto requestDto) {
    UserProfile existingProfile = getUser(username).getProfile(); // ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ๊ธฐ์กด ํ”„๋กœํ•„ ์ฐพ๊ธฐ
    if (existingProfile == null) throw InvalidRequestException.builder()
    .message("์‚ฌ์šฉ์ž์˜ ํ”„๋กœํ•„์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
    .invalidValue("์‚ฌ์šฉ์ž ID: " + username)
    .code("U401")
    .build();
    MultipartFile file = requestDto.getFile();
    if (file != null) {
    isImage(file); // ํŒŒ์ผ์ด ์ด๋ฏธ์ง€์ธ์ง€ ํ™•์ธ
    // ๋ฒ„ํ‚ท์— ์ €์žฅ๋  ๊ฒฝ๋กœ, ํŒŒ์ผ๋ช… ๊ทธ๋ฆฌ๊ณ  ํŒŒ์ผ์˜ metadata ์ƒ์„ฑ
    String path = String.format("%s/%s", bucketName, username);
    String fileName = String.format("%s", file.getOriginalFilename());
    Map<String, String> metadata = extractMetadata(file);
    try {
    fileStore.save(path, fileName, Optional.of(metadata), file.getInputStream()); // ๋ณ€๊ฒฝ ํŒŒ์ผ ์ €์žฅ
    } catch (IOException e) {
    throw new IllegalStateException("ํ”„๋กœํ•„ ์‚ฌ์ง„ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e.getCause());
    }
    }
    // ํ”„๋กœํ•„ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ ์šฉ ํ›„ DB ์ €์žฅ
    existingProfile.update(requestDto);
    profileRepository.save(existingProfile);
    return "success";
    }
    }



6. ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

6.1 ํ•ต์‹ฌ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

  • ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ
    UserDetails๊ฐ€ null์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฌธ์ œ
    ๋Œ“๊ธ€ ๋“ฑ ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์—์„œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๊ฐ€ ํ•„์š”ํ•˜์—ฌ @AuthenticationPrincipal ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ SecurityContext์˜ UserDetails ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.
    Authentication ๊ฐ์ฒด์˜ Principal์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ณผ์ •์—์„œ ๊ธฐ์กด์—๋Š” username ์ž์ฒด, ์ฆ‰ String์„ ๋„ฃ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— null์ด ๋ฐ˜ํ™˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
    ์ด๋ฅผ UsernamePasswordAuthenticationToken ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ UserDetails๋ฅผ ๊ตฌํ˜„ํ•œ User ๊ฐ์ฒด๋ฅผ ๋„ฃ์–ด์คŒ์œผ๋กœ์จ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ์„œ๋น„์Šค ์›ํ™œํžˆ ์ด์šฉ ๋ถˆ๊ฐ€
    ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์˜ ๊ฒฝ์šฐ principal์ด null์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์˜ค๋ฅ˜ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ
    ์ด ์„œ๋น„์Šค์˜ ๊ฒฝ์šฐ, ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋„ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ œํ•œ ์—†์ด ์ด์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.
    ๊ทธ๋Ÿฌ๋‚˜ @AuthenticationPrincipal ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ํ•„์š”๋กœ ํ•˜๋Š” ์กฐํšŒ API์˜ ๊ฒฝ์šฐ anonymousUser ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋˜์–ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
    ์ด๋ฅผ ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์„ ์ปค์Šคํ…€ํ•˜์—ฌ ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์˜ ๊ฒฝ์šฐ null์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•จ์œผ๋กœ์จ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.


6.2 ๊ทธ ์™ธ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

ํšŒ์› ID ์ค‘๋ณต ์กฐํšŒ ๋ถˆ๊ฐ€
  • ์ค‘๋ณต ์กฐํšŒ url์ด SecurityConfig ๋‚ด permitAll() ๋ˆ„๋ฝ๋˜์–ด ์ถ”๊ฐ€ํ•˜์—ฌ ํ•ด๊ฒฐ
ํ•„ํ„ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹
  • ์ปจํŠธ๋กค๋Ÿฌ์™€ ํ†ต์ผ๋˜์ง€ ์•Š์•„ ExceptionHandlerFilter ํด๋ž˜์Šค ์ƒ์„ฑ, ์ปค์Šคํ…€ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ ํ•ด๊ฒฐ
๊ฐ„ํ—์ ์œผ๋กœ ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ์ด ์ œ๋Œ€๋กœ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š๋Š” ๋ฌธ์ œ
  • AJAX์— async ์˜ต์…˜์„ ์ฃผ์–ด ๋™๊ธฐ์‹ ์ฒ˜๋ฆฌ๋กœ ํ•ด๊ฒฐ

7. ํšŒ๊ณ  / ๋Š๋‚€์ 

์ตœ์ข… ํ”„๋กœ์ ํŠธ ํšŒ๊ณ 

About


Languages

Language:Java 100.0%Language:Procfile 0.0%