스프링부트 독학-10장 OAuth2로 로그인/로그아웃 구현하기-a

jaegeunsong97·2023년 9월 4일
0

출처

신서영개발자님의 스프링부트 책

새롭게 알게된 내용 정리

OAuth

  • OAuth : 제 3의 서비스에 계정 관리를 맡기는 방식
    • 용어정리
      • 리소스오너 : resource owner
        • 자신의 정보를 사용하도록 인증 서버에 허가하는 주체
        • 서비스를 이용하는 사용자가 리소스 오너
        • 송재근
      • 리소스 서버 : resource server
        • 리소스 오너의 정보를 가지며, 리소스 오너의 정보에 접근할 수 있는 토큰을 발급하는 역할을 하는 애플리케이션
        • 스프링 부트
      • 인증 서버 : authorization server
        • 클라이언트에게 리소스 오너의 정보에 접근할 수 있는 토큰을 발급하는 역할을 하는 애플리케이션
        • 카카오 인증 서버
      • 클라이언트 애플리케이션 : client application
        • 인증 서버에게 인증을 받고 리소스 오너의 리소스를 사용하는 주체를 의미
        • 지금 만들고 있는 서비스가 이에 해당
  • OAuth를 사용하면 인증 서버에서 발급받은 토큰을 사용해서, 리소스 서버에 리소스 오너의 정보를 요청하고 응답받아 사용 가능
    • 리소스 오너의 정보를 취득하는 방법 4가지
      • 권한 부여 코드 승인 타입(중요) : authorization code grant type
        • OAuth2.0에서 가장 잘 알려진 인증방식
        • 클라이언트가 리소스에 접근하는 데 사용
          • 정확하게는 스프링부트가 송재근의 정보에 접근하는데 사용
        • 권한에 접근할 수 있는 코드리소스 오너에 대한 엑세스 토큰을 발급받는 형식
        • 사용자의 데이터가 외부로 나가지 않아 안전
      • 클라이언트 자격증명 승인 타입(중요) : client credentials grant
        • 클라이언트가 컨텍스트 외부에서 엑세스 토큰을 얻어 특정 리소스에 접근을 요청할 때 사용하는 방식
        • 송재근이 액세스 토큰을 얻어 사용하는 방식
      • 암시적 승인 타입 : implicit grant type
        • 서버가 없는 자바스크립트 웹 애플리케이션 클라이언트에서 주로 사용하는 방법
        • 클라이언트가 요청을 보내면 리소스 오너의 인증 과정 이외에는 권한 크도 교환 등의 별다른 인증 과정을 거치지 않고 엑세스 토큰을 제공받는 방식
      • 리소스 소유자 암호 자격증명 승인 타입 : resource owner password credentials
        • 클라이언트의 패스워드를 이용해서 엑세스 토큰에 대한 사용자의 자격 증명을 교환하는 방식

  • 권한 부여 코드 승인 타입
    • 1. 권한 요청 : 스프링 부트 서버가 특정 사용자 데이터에 접근하기 위해 권한서버(= 카카오, 구글 권한 서버)에 요청을 보내는 것
      • 요청 URL는 권한 서버마다 다름
        • client_id : 인증 서버가 클라이언트에 할당한 고유 식별자
          • 66asd128f2jf2&
        • redirect_uri : 로그인 성공 시, 바로 이동하는 URI
        • response_type : 클라이언트가 제공받길 원하는 응답 타입
          • 나 이거줘!
          • 인증코드 받을 때는 code값 포함
        • scpoe : 제공받고자 하는 리소스 오너의 정보 목록
          • email
          • profile
    • 2. 데이터 접근용 권한 부여
      • 인증 서버에 요청이 처음인 경우(최초 1회만 진행)
        • 사용자에게 보이는 페이지를 로그인 페이지로 변경하고
        • 사용자의 데이터에 접근 동의를 얻음
        • 최초 1회 진행 후, 인증 서버에서는 동의 내용을 가지고 있기 때문에 로그인만 진행
        • 로그인 성공? -> 권한 부여 서버는 데이터에 접근할 수 있게 인증 및 권한 부여 수신
    • 3. 인증 코드 제공
      • 사용자 로그인 성공 : 권한 요청 시, redirect_url로 리다이렉션
    • 4. 액세스 토큰 응답
      • 인증 코드 받으면 액세스 토큰으로 교환해야함
      • 액세스 토큰 : 로그인 세션에 대한 보안 자격을 증명하는 식별코드
      • /token POST 요청을 보냄
        • client_sercret : OAuth 서비스에 등록할 때 제공받는 비밀키
        • grant_type : 권한유형 확인할는데 사용
          • authorization code로 설정하기
      • 권한 서버는 요청 값을 기반으로 유효한 값인지 확인 후, 유효한 정보면 액세스 토큰으로 응답
    • 5. 액세스 토큰으로 API 응답 & 반환
      • 구글로 부터 받은 액세스 토큰으로 리소스 오너의 정보를 가지고 올 수 있음
      • 필요할 때마다 API호출을 통해 정보를 가져오고 리소스 서버는 토큰이 유효한지 검사한 뒤에 응답
// 권한 요청을 위한 파라미터, Client(스프링서버) -> 인증서버(구글, 카카오)
GET spring-authorization-server.example/authorize?
	client_id=66asd128f2jf2&
    redirect_uri=http://localhost:8080/myapp&
    response_type=code&code값
    scope=profile
// 인증 코드, 권한 요청 시, redirect_url로 리다이렉션
GET http://localhost:8080/myapp?code=asd1234452dasd1234452d
// 액세스 토큰 요청
POST spring-authorization-server.example.com/token
{
	"client_id": "66asd128f2jf2&",
    "client_secret": "aabb112233",
    "redirect_uri": "http://localhost:8080/myapp",
    "grant_type": "authorization_code",
    "code": "asd1234452d"
}
.
.
.
// 액세스 토큰 응답
{
	"access_token" : "aasdffg",
    "token_type": "Bearer ",
    "expires_in": 3600,
    "scope": "openod profile",
    .
    .
    .
}
// 리소스 오너의 정보를 가져오기 위한 요청
GET spring-authorization-resource-server.example.com/userinfo
Header: Authorization: Bearer aasdffg
  • 쿠키 : 사용자가 어떠한 웹사이트 방문 시, 해당 웹사이트의 서버에서 여러분의 로컬환경에 저장하는 작은 데이터
    • 방문의 유무 체크
    • 로그인 상태 유지
    • Key, Value
    • 만료 기간, 도메인 등의 정보
    • HTTP 요청을 통해 쿠키의 특정 키에 값을 추가 가능
      • HttpServletRequest
    • 순서
      • Client -> Server
        • GET /members
      • Server -> Client
        • Set-Cookie: member_id=1
      • Client -> Server
        • GET /members
        • Cookie: member_id=1

토큰 발급받기

책보면서 따라하기

마지막에 나오는 client-id + client-secret

gitgnore에 yml에 넣기 -> 외부노출 절대 X

spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
    defer-datasource-initialization: true
  datasource:
    url: jdbc:h2:mem:testdb
    username: song
  h2:
    console:
      enabled: true
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 여기에 넣기~!
            client-secret: 여기에 넣기~!
            scope:
              - email
              - profile
jwt:
  issuer: jaegeunsong97@gmail.com # 이슈 발급자
  secret_key: study-springboot # 비밀키

스프링 시큐리티로 OAuth2 구현하고 적용

  • 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
  • 쿠키 관리 클래스 구현
    • OAuth2 인증 플로우 -> 쿠키 사용할 일 있음
    • 그때마다 쿠키를 사용하고 삭제? -> 비효율적
      • 따라서 쿠키 관리 클래스 생성
  • /util/CookieUtil
    • addCookie() : 요청값(이름, 값, 만료기간)을 바탕으로 HTTP 응답에 쿠키를 추가
    • deleteCookie() : 쿠키 이름을 입력받아 쿠키 삭제
      • 실제 삭제 불가
      • 따라서 넘어온 쿠키를 빈값 + 만료 시간 0 으로 만들기
        • 생성하자마자 만료
    • serialize() : 객체를 직렬화해 쿠키의 값으로 들어갈 값으로 변환
    • deserialize() : 쿠키를 역직렬화해 객체로
package me.songjaegeun.springbootdeveloper.util;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.SerializationUtils;

import java.util.Base64;

public class CookieUtil {

    // 요청값(이름, 값, 만료 기간)을 바탕으로 쿠키 추가
    public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
        Cookie cookie = new Cookie(name, value);
        cookie.setPath("/");
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }

    // 쿠키의 이름을 입력받아 쿠키 삭제
    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
        Cookie[] cookies = request.getCookies();

        if (cookies == null) {
            return;
        }

        // 생성되자마자 만료되게 만들기
        for (Cookie cookie : cookies) {
            if (name.equals(cookie.getName())) {
                cookie.setValue("");
                cookie.setPath("/");
                cookie.setMaxAge(0);
                response.addCookie(cookie);
            }
        }
    }

    // 객체를 직렬화 해 쿠키의 값으로 변환
    public static String serialize(Object obj) {
        return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(obj));
    }

    // 쿠키를 역직렬화해 객체로 변환
    public static <T> T deserialize(Cookie cookie, Class<T> cls) {
        return cls.cast(SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
    }
}
  • OAuth2 서비스 구현
    • user 조회 DB에 존재 O : 리소스 서버에서 제공해주는 이름으로 update
    • user 조회 DB에 존재 X : 새로 만들어 DB에 저장
package me.songjaegeun.springbootdeveloper.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false)
    private Long id;
    @Column(name = "email", nullable = false, unique = true)
    private String email;
    @Column(name = "password")
    private String password;
    @Column(name = "nickname", unique = true)
    private String nickname; // oauth2

    @Builder
    public User(String email, String password, String nickname) {
        this.email = email;
        this.password = password;
        this.nickname = nickname; // oauth2
    }

    /**
     * OAuth2
     */
    public User update(String nickname) {
        this.nickname = nickname;
        return this;
    }

    /**
     * Security
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("user"));
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • /config/oauth/OAuth2UserCustomService
    • 리소스 서버에서 보내주는 사용장의 정보를 불러오는 메소드 loadUser()를 통해 사용자 조회
      • loadUser() : DefaultOAuth2UserService에서 제공
        • OAuth2로 접속한 객체의 식별자, 이름, 이메일, 프로필 사진 링크 담고 있음
    • users 테이블에 사용자 정보가 있다면 이름 update 없으면 saveOrUpdate 메소드 실행 해서 DB에 추가
package me.songjaegeun.springbootdeveloper.config.oauth;

import lombok.RequiredArgsConstructor;
import me.songjaegeun.springbootdeveloper.domain.User;
import me.songjaegeun.springbootdeveloper.repository.UserRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Map;

@RequiredArgsConstructor
@Service
public class OAuth2UserCustomService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // 요청을 바탕으로 유저 정보를 담은 객체 반환
        OAuth2User user = super.loadUser(userRequest); // OAuth2UserRequest에서 사용자 조회
        saveOrUpdate(user);
        return user;
    }

    // 유저가 있으면 업데이트, 없으면 유저 생성
    private User saveOrUpdate(OAuth2User oAuth2User) {
        Map<String, Object> attributes = oAuth2User.getAttributes();
        String email = (String) attributes.get("email");
        String name = (String) attributes.get("name");

        User user = userRepository.findByEmail(email)
                .map(entity -> entity.update(name))
                .orElse(User.builder()
                        .email(email)
                        .nickname(name)
                        .build());
        return userRepository.save(user);
    }
}
  • OAuth2 설정 파일 작성

    • OAuth2와 JWT 사용 : 기존 Spring Security 구현하면 다르게 바꾸기
  • 폼 로그인 방식인 WebSecurityConfig 전부 주석

  • /config/WebOAuthSecurityConfig

    • filterChain() : 토큰 방식으로 인증, 기존 폼 로그인, 세션 기능 비활성화
    • addFilterBefore() : 헤더값을 확인할 커스텀 필터 추가
      • TokenAuthenticationFilter
    • authorieRequests() : 토큰 재발급 URL은 인증 없이, 나머지 API 인증 필요
    • oauth2Login() : OAuth2에 필요한 정보를 세션이 아닌 쿠키에 저장해서 사용하도록 인증 요청과 관련된 상태를 저장할 저장소를 설정
      • 인증 성공 -> successHandler()
      • 실패 -> exceptionHandling()
    • exceptionHandling() : /api로 시작 url -> 인증 실패 401 발생
  • /config/oauth/OAuth2AuthorizationRequestBasedOnCookieRepository

    • OAuth2에 필요한 정보를 세션이 아닌 쿠키에 저장해서 사용하도록 인증 요청과 관련된 상태를 저장할 저장소
    • AuthorizationRequestRepositry : 권한 인증 흐름에서 클라이언트의 요청을 유지하는 데 사용
      • 해당 구현을 통해 쿠키를 사용해 OAuth 정보를 가져오고 저장
package me.songjaegeun.springbootdeveloper.config.oauth;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import me.songjaegeun.springbootdeveloper.util.CookieUtil;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.web.util.WebUtils;

// OAuth2에 필요한 정보를 쿠키에 담아서 저장하는 저장소
public class OAuth2AuthorizationRequestBasedOnCookieRepository implements
        AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
    // AuthorizationRequestRepository : 권한 인증 흐름에서 클라이언트 요청 유지하게 해줌
    
    public final static String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
    private final static int COOKIE_EXPIRE_SECONDS = 18000;

    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
        return this.loadAuthorizationRequest(request);
    }

    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
        return CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class);
    }

    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
        if (authorizationRequest == null) {
            removeAuthorizationRequestCookies(request, response);
            return;
        }

        CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
    }

    public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
        CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
    }
}
  • 인증 성공시 핸들러

  • UserService

package me.songjaegeun.springbootdeveloper.service;

import lombok.RequiredArgsConstructor;
import me.songjaegeun.springbootdeveloper.domain.User;
import me.songjaegeun.springbootdeveloper.dto.AddUserRequest;
import me.songjaegeun.springbootdeveloper.repository.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;

    public Long save(AddUserRequest dto) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

        return userRepository.save(User.builder()
                .email(dto.getEmail())
                .password(encoder.encode(dto.getPassword()))
                .build()).getId();
    }

    public User findById(Long userId) {
        return userRepository.findById(userId).orElseThrow(
                () -> new IllegalArgumentException("Unexpected user")
        );
    }

    public User findByEmail(String email) {
        return userRepository.findByEmail(email).orElseThrow(
                () -> new IllegalArgumentException("Unexpected user")
        );
    }
}
  • /config/oauth/OAuth2SuccessHandler
    • 스프링 시큐리티 기본 로직 : authenticationSuccessHandler 지정 않하면 로그인 성공 이후 -> SimpleUrlAuthenticationSuccessHandler 사용
      • 일반적인 로직은 동일하게
      • 토큰 관련된 작업만 추가로 처리하기 위해 SimpleUrlAuthenticationSuccessHandler 상속받고 onAuthenticationSuccess() 메소드를 오버라이드
package me.songjaegeun.springbootdeveloper.config.oauth;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import me.songjaegeun.springbootdeveloper.config.jwt.TokenProvider;
import me.songjaegeun.springbootdeveloper.domain.RefreshToken;
import me.songjaegeun.springbootdeveloper.domain.User;
import me.songjaegeun.springbootdeveloper.repository.RefreshTokenRepository;
import me.songjaegeun.springbootdeveloper.service.UserService;
import me.songjaegeun.springbootdeveloper.util.CookieUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.time.Duration;

@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    // SimpleUrlAuthenticationSuccessHandler : 일반적인 로직 동일하게, 추가를 위해 onAuthenticationSuccess Override

    public static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token";
    public static final Duration REFRESH_TOKEN_DURATION = Duration.ofDays(14);
    public static final Duration ACCESS_TOKEN_DURATION = Duration.ofDays(1);
    public static final String REDIRECT_PATH = "/articles";

    private final TokenProvider tokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;
    private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository;
    private final UserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        User user = userService.findByEmail((String) oAuth2User.getAttributes().get("email"));
        
        // 1. 리프레시 토큰 생성 -> DB저장 -> 쿠키에 저장
        String refreshToken = tokenProvider.generateToken(user, REFRESH_TOKEN_DURATION);
        saveRefreshToken(user.getId(), refreshToken);
        addRefreshTokenToCookie(request, response, refreshToken); // 토큰 만료 시, 재발급 위해 refreshToken 저장
        
        // 2. 액세스 토큰 생성 -> 패스에 액세스 토큰 추가
        String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
        String targetUrl = getTargetUrl(accessToken);
        
        // 3. 인증 관련 설정값, 쿠키 제거
        clearAuthenticationAttributes(request, response);

        // 4. 리다이렉트
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
    
    // 생성된 리프레시 토큰을 전달받아 DB에 저장
    private void saveRefreshToken(Long userId, String newRefreshToken) {
        RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId)
                .map(entity -> entity.update(newRefreshToken))
                .orElse(new RefreshToken(userId, newRefreshToken));
        refreshTokenRepository.save(refreshToken);
    }

    // 생성된 리프레시 토큰을 쿠키에 저장
    private void addRefreshTokenToCookie(HttpServletRequest request, HttpServletResponse response, String refreshToken) {
        int cookieMaxAge = (int) REFRESH_TOKEN_DURATION.toSeconds();
        CookieUtil.deleteCookie(request, response, REFRESH_TOKEN_COOKIE_NAME);
        CookieUtil.addCookie(response, REFRESH_TOKEN_COOKIE_NAME, refreshToken, cookieMaxAge);
    }

    // 인증 관련 설정값, 쿠키 제거
    private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
        super.clearAuthenticationAttributes(request);
        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response); // OAuth 인증을 위한 정보 삭제
    }

    // 액세스 토큰을 패스에 추가
    private String getTargetUrl(String token) {
        return UriComponentsBuilder.fromUriString(REDIRECT_PATH)
                .queryParam("token", token)
                .build()
                .toUriString();
    }
}
  • 글에 글쓴이 추가
package me.songjaegeun.springbootdeveloper.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false)
    private Long id;
    @Column(name = "title", nullable = false)
    private String title;
    @Column(name = "content", nullable = false)
    private String content;
    @Column(name = "author", nullable = false)
    private String author; // oauth

    @CreatedDate
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @Builder
    public Article(String author, String title, String content) {
        this.author = author; // oauth
        this.title = title;
        this.content = content;
    }

    /**
     * 의미있는 메소드
     */
    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }
}
package me.songjaegeun.springbootdeveloper.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import me.songjaegeun.springbootdeveloper.domain.Article;

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class AddArticleRequest {

    private String title;
    private String content;

    public Article toEntity(String author) { // oauth
        return Article.builder()
                .title(title)
                .content(content)
                .author(author) // oauth
                .build();
    }
}
public class BlogService {
	.
	.
    public Article save(AddArticleRequest request, String userName) {
        return blogRepository.save(request.toEntity(userName));
    }
    .
    .
public class BlogApiController {
	.
	.
    @PostMapping("/api/articles")
    public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request, Principal principal) {
    	// principal 현재 인증 정보를 가지고 있음
        Article savedArticle = blogService.save(request, principal.getName());
        return ResponseEntity.status(HttpStatus.CREATED).body(savedArticle);
    }
    .
    .
package me.songjaegeun.springbootdeveloper.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import me.songjaegeun.springbootdeveloper.domain.Article;

import java.time.LocalDateTime;

@NoArgsConstructor
@Getter
public class ArticleViewResponse {

    private Long id;
    private String title;
    private String content;
    private LocalDateTime createdAt;
    private String author;

    public ArticleViewResponse(Article article) {
        this.id = article.getId();
        this.title = article.getTitle();
        this.content = article.getContent();
        this.createdAt = article.getCreatedAt();
        this.author = article.getAuthor();
    }
}
  • sql 추가
INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목1', '내용1', 'user1', NOW(), NOW())
INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목2', '내용2', 'user2', NOW(), NOW())
INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목3', '내용3', 'user3', NOW(), NOW())
  • article.html
    • 글쓴이 정보 가져오게 하기
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>블로그 글</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
    <h1 class="mb-3">My Blog</h1>
    <h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>

<div class="container mt-5">
    <div class="row">
        <div class="col-lg-8">
            <article>
                <input type="hidden" id="article-id" th:value="${article.id}">
                <header class="mb-4">
                    <h1 class="fw-bolder mb-1" th:text="${article.title}"></h1>
                    <div class="text-muted fst-italic mb-2" th:text="|Posted on ${#temporals.format(article.createdAt, 'yyyy-MM-dd HH:mm')} By ${article.author}|"></div>
                </header>
                <section class="mb-5">
                    <p class="fs-5 mb-4" th:text="${article.content}"></p>
                </section>
                <button type="button" id="modify-btn"
                        th:onclick="|location.href='@{/new-article?id={articleId}(articleId=${article.id})}'|"
                        class="btn btn-primary btn-sm">수정</button>
                <button type="button" id="delete-btn"
                        class="btn btn-secondary btn-sm">삭제</button>
            </article>
        </div>
    </div>
</div>

<script src="/js/article.js"></script>
</body>
profile
블로그 이전 : https://medium.com/@jaegeunsong97

0개의 댓글