이번 스프링 시큐리티는 Spring Data JPA를 통한 로그인 입니다

Spring Boot 3.0.2 버전 , Spring Security 6 사용하여 기존 5.7 이하에서 사용하던,

Deprecated 된 부분들(websecurityconfigureradapter , authorizeRequests)를 바꾸어 작성 해보았습니다.

( 급하신 분들은 MemberSecurityConfig.java 파일만 참고하시면 될 것 같습니다 )

 

 

깃허브 레포지토리에서 해당 소스나 패키지 구조 참고 가능합니다

( ps. 2023.02.21 14:03 로그인 기억하기 추가 깃허브 참조 )

 

https://github.com/wjsskagur/spring_security1

 

GitHub - wjsskagur/spring_security1

Contribute to wjsskagur/spring_security1 development by creating an account on GitHub.

github.com

 

* Spring Boot 3 버전부터 java 17 아래 버전으론 컴파일이 되지 않습니다 *

데이터베이스는 h2 기반으로 작성하였습니다.

 

Gradle 입니다

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.2'
    id 'io.spring.dependency-management' version '1.1.0'
}

group = 'security1'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

 

 

 

application.properties 입니다

# h2 DataBase 접속정보 ( h2 DataBase 설치 필요 )
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:~/Documents/h2/jun
spring.datasource.username=jun
spring.datasource.password=qwer1234

# jpa 기본 설정
# 영속성 컨텍스트 유지
spring.jpa.open-in-view=true
# sql 보기
spring.jpa.show-sql=true
# 쿼리 가독성
spring.jpa.properties.hibernate.format_sql=true
# 테이블 자동생성 끄기
spring.jpa.hibernate.ddl-auto=none

 

 

 

 

 

이후 전체적인 프로젝트 구조 및 파일 입니다.

아래 파일들은 코드와 같이 작업 할 파일들입니다.

 

 

자세한 구조는 GitHub 참조 바랍니다

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

html 3개의 소스입니다

먼저 로그인 페이지 templates/login/login.html

로그인 기억하기 사용을 위한 remember-me 을 사용하였습니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>로그인 페이지</title>
</head>
<body>
    <h1>로그인 페이지</h1>
    <form action="/member/login/login" method="post">
        <div>
            <label for="username">아이디</label>
            <input type="text" name="username" id="username">
        </div>
        <div>
            <label for="password">비밀번호</label>
            <input type="password" name="password" id="password">
        </div>
        <div>
            <input type="checkbox" name="remember-me" id="rememberMe">
            <label for="rememberMe">자동 로그인</label>
        </div>
        <p style="display: none" class="error_txt" id="error_text"
           th:if="${#strings.length(session.loginErrorMessage)>0}"
           th:text="${session.loginErrorMessage}">로그인하시려면 아이디와 비밀번호를 입력해주세요</p>
        <button type="submit">로그인</button>
    </form>
</body>
<script>
    const error = document.getElementById('error_text');
    if(error !== undefined) alert(error.innerText);
</script>
</html>

 

로그인 완료시 이동할 페이지 templates/main/main.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그인성공</title>
</head>
<body>
    <h1>로그인 성공</h1>
    <p>로그인 성공</p>
    <a href="/member/text">회원 정보 확인하기</a>
    <a href="/member/login/logout">로그아웃</a>
</body>
</html>

 

로그인 된 회원정보를 출력할 페이지

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그인한 회원 정보</title>
</head>
<body>
    <p th:text="${'회원 로그인 아이디 : ' + member.loginId}"></p>
    <p th:text="${'회원 이름 : ' + member.name}"></p>
    <p th:text="${'회원 이메일 : ' + member.email}"></p>
</body>
</html>

 

해당 프로젝트에 사용할 DDL 입니다

DROP TABLE IF EXISTS MEMBER_TB;

CREATE TABLE MEMBER_TB
(
    MEMBER_ID       INT AUTO_INCREMENT NOT NULL,
    MEMBER_LOGIN_ID VARCHAR(20) NOT NULL,
    MEMBER_ROLE     VARCHAR(20) DEFAULT 'ROLE_MEMBER' NOT NULL,
    MEMBER_NAME     VARCHAR(20) NOT NULL,
    MEMBER_PASSWORD VARCHAR(400) NOT NULL,
    MEMBER_EMAIL    VARCHAR(100) NOT NULL,
    IS_USED         CHAR(1) DEFAULT 'Y' NOT NULL,
    IS_DEL          CHAR(1) DEFAULT 'N' NOT NULL,
    ISRT_DATE       DATETIME DEFAULT NOW() NOT NULL,
    UPDT_DATE       DATETIME ,
    CONSTRAINT MEMBER_PK PRIMARY KEY (MEMBER_ID)
);

-- 비밀번호 1111
INSERT INTO MEMBER_TB (MEMBER_LOGIN_ID, MEMBER_NAME, MEMBER_PASSWORD, MEMBER_EMAIL)
VALUES ('member1', '전남혁', '$2a$12$umem9giXuB0lDzAQ1ofzmeVqwHHFX76sbMObVEWpcIOPc6O.47NGa', 'all_step@naver.com');

-- 비밀번호 1234
INSERT INTO MEMBER_TB (MEMBER_LOGIN_ID, MEMBER_NAME, MEMBER_PASSWORD, MEMBER_EMAIL)
VALUES ('member2', '김철수', '$2a$12$R0ZgpAnBKh8CX0sATNRY8OyXPfke6GsXOxOA18gWyJ7RrnzOGnDOu', '');

 

이어서 엔티티 입니다

member/entity/Member

@Entity // 엔티티 생성시
@Getter
@Setter
@Builder // 추후 Builder 사용시
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Table(name = "MEMBER_TB")
public class Member {

    @Id // 엔티티 내부에서 아이디임을 선언
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 시퀀스 전략 선언
    @Column(name = "MEMBER_ID") // 아이디에 해당하는 컬럼명 선언
    private Long id;

    @Column(name = "MEMBER_LOGIN_ID")
    private String loginId;

    @Column(name = "MEMBER_ROLE")
    private String role;

    @Column(name = "MEMBER_NAME")
    private String name;

    @Column(name = "MEMBER_PASSWORD")
    private String password;

    @Column(name = "MEMBER_EMAIL")
    private String email;

    @Column(name = "IS_USED")
    private String isUsed;

    @Column(name = "IS_DEL")
    private String isDel;

    @Column(name = "ISRT_DATE")
    private LocalDateTime isrtDate;

    @Column(name = "UPDT_DATE")
    private LocalDateTime updtDate;
}

 

이어서 레포지토리 입니다

 member/repository/MemberRepository

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByLoginId(String username);
}

 

Spring Security에서 사용자를 담아둘 클래스입니다

securty/auth/MemberPrincipalDetails

// Spring Security 에 있는 UserDetails 를 구현한 클래스,
// 이 클래스를 통해 Spring Security 에서 사용자의 정보를 담아둠
public class MemberPrincipalDetails implements UserDetails {


    // member 패키지에 선언해놓은 member 엔티티를 사용하기 위해 선언
    private final Member member;

    public MemberPrincipalDetails(Member member) {
        this.member = member;
    }

    // 생성자
    public Member getMember() {
        return member;
    }

    // member 계정의 권한을 담아두기위해
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(member.getRole()));
        return authorities;
    }

    // member 계정의 비밀번호를 담아두기 위해
    @Override
    public String getPassword() {
        return member.getPassword();
    }

    // member 계정의 아이디를 담아두기 위해
    @Override
    public String getUsername() {
        return member.getLoginId();
    }

    // 계정이 만료되지 않았는지를 담아두기 위해 (true: 만료안됨)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정이 잠겨있지 않았는지를 담아두기 위해 (true: 잠기지 않음)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 계정의 비밀번호가 만료되지 않았는지를 담아두기 위해 (true: 만료안됨)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정이 활성화되어있는지를 담아두기 위해 (true: 활성화됨)
    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

 

스프링 시큐리티가 로그인 요청을 가로챌때 동작하는 서비스 입니다

security/auth/MemberPrincipalDetailService

// UserDetailsService 를 구현한 클래스
// 스프링 시큐리티가 로그인 요청을 가로챌 때, username, password 변수 2개를 가로채는데
// password 부분 처리는 알아서 함
@Service
public class MemberPrincipalDetailService implements UserDetailsService {

    // JPARepository 를 상속받은 인터페이스를 자동으로 DI (의존성 주입)
    @Autowired
    private MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 넘겨받은 id 로 DB 에서 회원 정보를 찾음
        Member member = memberRepository.findByLoginId(username);
        System.out.println("username : " + username);
        System.out.println("member : " + member);
        // 없을경우 에러 발생
        if(member == null)
            throw new UsernameNotFoundException(username + "을 찾을 수 없습니다.");

        if(!"Y".equals(member.getIsUsed()))
            throw new UsernameNotFoundException("사용할 수 없는 계정입니다.");

        if(!"N".equals(member.getIsDel()))
            throw new UsernameNotFoundException("삭제된 계정입니다.");

        // MemberPrincipalDetails 에 Member 객체를 넘겨줌
        return new MemberPrincipalDetails(member);
    }
}

 

 

 

로그인  정보를 가로챈 후 인증버로를 반환 하기위한 클래스입니다

security/provider/MemberAuthenticationProvider

@Component
// AuthenticationProvider 를 구현한 클래스
public class MemberAuthenticatorProvider implements AuthenticationProvider {

    @Autowired
    private MemberPrincipalDetailService memberPrincipalDetailService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName(); // 사용자가 입력한 id
        String password = (String) authentication.getCredentials(); // 사용자가 입력한 password

        // 생성해둔 MemberPrincipalDetailService 에서 loadUserByUsername 메소드를 호출하여 사용자 정보를 가져온다.
        MemberPrincipalDetails memberPrincipalDetails = (MemberPrincipalDetails) memberPrincipalDetailService.loadUserByUsername(username);

        // ====================================== 비밀번호 비교 ======================================
        // 사용자가 입력한 password 와 DB 에 저장된 password 를 비교한다.

        // db 에 저장된 password
        String dbPassword = memberPrincipalDetails.getPassword();
        // 암호화 방식 (BCryptPasswordEncoder) 를 사용하여 비밀번호를 비교한다.
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        if(!passwordEncoder.matches(password, dbPassword)) {
            System.out.println("[사용자] 비밀번호가 일치하지 않습니다.");
            throw new BadCredentialsException("[사용자] 아이디 또는 비밀번호가 일치하지 않습니다.");
        }
        // ========================================================================================

        // 사용자가 입력한 id 와 password 가 일치하면 인증이 성공한 것이다.
        // 인증이 성공하면 MemberPrincipalDetails 객체를 반환한다.
        Member member = memberPrincipalDetails.getMember();
        if (member == null || "N".equals(member.getIsUsed())) {
            System.out.println("[사용자] 사용할 수 없는 계정입니다.");
            throw new BadCredentialsException("[사용자] 사용할 수 없는 계정입니다.");
        }

        // 인증이 성공하면 UsernamePasswordAuthenticationToken 객체를 반환한다.
        // 해당 객체는 Authentication 객체로 추후 인증이 끝나고 SecurityContextHolder.getContext() 에 저장된다.
        return new UsernamePasswordAuthenticationToken(memberPrincipalDetails, null, memberPrincipalDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 

 

 

로그인 실패시 에러 메세지를 띄우기 위한 커스텀 핸들러 입니다

security/config/MemberAuthFailureHandler

public class MemberAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws ServletException, IOException {
        // 실패 메세지를 담기 위한 세션 선언
        HttpSession session = request.getSession();
        // 세션에 실패 메세지 담기
        session.setAttribute("loginErrorMessage", exception.getMessage());
        // 로그인 실패시 이동할 페이지
        setDefaultFailureUrl("/member/login/loginForm?error=true&t=h");
        // 로그인 실패시 이동할 페이지로 이동
        super.onAuthenticationFailure(request, response, exception);
    }
}

 

 

 

로그인 성공시 작동하는 커스텀 핸들러 입니다 ( 해당 프로젝트에선 사용하지 않습니다 )

security/config/MemberAuthSuccessHandler

public class MemberAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 로그인 성공시 이동할 페이지
        setDefaultTargetUrl("/member/main");
        // 로그인 성공시 이동할 페이지로 이동
        super.onAuthenticationSuccess(request, response, authentication);
    }

}

 

 

위에 작성한 파일들이 사용되는 파일입니다

security/config/MemberSecurityConfig

로그인 기억하기 사용을 위한 rememberMe 도 같이 사용하였습니

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class MemberSecurityConfig {
    /*
    * 중요
    * Spring Security 5.7.0 버전부터
    * WebSecurityConfigurerAdapter가 deprecated 되기 때문에
    * 이와 같은 방법으로 구현
    * */


    // 생성해둔 MemberAuthenticatorProvider를 주입받는다.
    // 해당 클래스로 MemberPrincipalDetailsService 내부 로직을 수행하며
    // 인증 처리도 같이 진행된다
    @Autowired
    MemberAuthenticatorProvider memberAuthenticatorProvider;

    // 로그인 기억하기 사용을 위해 MemberAuthenticatorProvider 내부
    // MemberPrincipalDetailsService 선언
    @Autowired
    MemberPrincipalDetailService memberPrincipalDetailService;


    // in memory 방식으로 인증 처리를 진행 하기 위해 기존엔 Override 하여 구현했지만
    // Spring Security 5.7.0 버전부터는 AuthenticationManagerBuilder를 직접 생성하여
    // AuthenticationManager를 생성해야 한다.
    @Autowired
    public void configure (AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(memberAuthenticatorProvider);
    }

    // 5.7.0 부터 Override 하지 않고
    // SecurityFilterChain을 직접 생성하여 구현
    // 그 외 authorizeRequests 가 deprecated 되었기 때문에
    // authorizeHttpRequests 로 변경
    @Bean
    public SecurityFilterChain memberSecurityFilterChain (HttpSecurity http) throws Exception {
        http.csrf().disable();

        http.authorizeHttpRequests(authorize -> {
            try {
                authorize
                        .requestMatchers("/css/**", "/images/**", "/js/**", "/member/login/**", "/member/attachment/**", "/member/files/**")
                        .permitAll() // 해당 경로는 인증 없이 접근 가능
                        .requestMatchers("/member/**") // 해당 경로는 인증이 필요
                        .hasRole("MEMBER") // ROLE 이 MEMBER 가 포함된 경우에만 인증 가능
                    .and()
                        .formLogin()
                        .loginPage("/member/login/loginForm") // 로그인 페이지 설정
                        .loginProcessingUrl("/member/login/login") // 로그인 처리 URL 설정
                        .defaultSuccessUrl("/member/main") // 로그인 성공 후 이동할 페이지
//                        .successHandler(new MemberAuthSuccessHandler()) // 로그인 성공 후 처리할 핸들러
                        .failureHandler(new MemberAuthFailureHandler()) // 로그인 실패 후 처리할 핸들러
                        .permitAll()
                    .and()
                        .logout()
                        .logoutUrl("/member/login/logout") // 로그아웃 처리 URL 설정
                        .logoutSuccessUrl("/member/login/loginForm?logout=1") // 로그아웃 성공 후 이동할 페이지
                        .deleteCookies("JSESSIONID"); // 로그아웃 후 쿠키 삭제
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        http.rememberMe()
                .key("namhyeok") // 인증 토큰 생성시 사용할 키
                .tokenValiditySeconds(60 * 60 * 24 * 7) // 인증 토큰 유효 시간 (초)
                .userDetailsService(memberPrincipalDetailService) // 인증 토큰 생성시 사용할 UserDetailsService
                .rememberMeParameter("remember-me"); // 로그인 페이지에서 사용할 파라미터 이름

        return http.build();
    }
}

 

실행 결과입니다.

테스트 아이디 : member1 , 비밀번호 : 1111

자동로그인 체크후 로그인 시도

 

로그인 성공화면

 

자동 로그인 체크 후 remember-me 라는 쿠키가 생성, JSESSIONID를 지워도 remember-me 가 있어 다시 생성됨

 

로그인 성공 후 담겨있는 데이터를 model에 담은뒤 thymeleaf로 출력

 

다음 Spring Security 글은 DB에 저장되어있는 권한을 동적으로 부여하는 법에 대해 작성하겠습니다.

두서 없는글 읽어주셔서 감사합니다.

 

 

 

GitHub - wjsskagur/spring_security1

Contribute to wjsskagur/spring_security1 development by creating an account on GitHub.

github.com

 

반응형

첫 글로 Java PDF 생성시 많이 사용하고 있는 itext를 이용하여 HTML페이지를 CSS와 폰트도 함께 적용하여,

PDF를 만들고 또한 라이브 서버 jar 배포시 대응 하기 위한 내용도 같이 포함하여 진행해 보겠습니다.

 

우선 시작하기에 앞서 환경은 아래와 같습니다.

 

IDE : intelliJ

자바 : Java 11

스프링 부트 : Spring Boot 2.7.7

템플릿 엔진 : Thymeleaf

빌드 도구 : Gradle

그외 Lombok , spring boot web

 

작성자의 OS는 Mac입니다. ( Window의 경우 파일 디렉토리 확인 바랍니다. )

 

 

사용한 itext 라이브러리 입니다.

// ====================================================================================================
// HTML TO PDF LIBRARY
implementation 'com.itextpdf:itextpdf:5.5.13.3'
implementation 'com.itextpdf.tool:xmlworker:5.5.13.3'
implementation 'com.itextpdf:pdfa:7.2.3'
// ====================================================================================================

 

 

 

대략적인 프로젝트 구조입니다.

itext 패키지에 컨트롤러와 DTO ,

실제 기능이 구현될 Util을 만들어 두었습니다.

그리고 호출될 CSS와 호출해볼 이미지를 미리 준비해 놓았습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

templates 하위 index.html의 소스입니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Hello itext pdf</title>
</head>
<body>
    <h1>Hello itext pdf</h1>
    <p>Click <a href="/attachment/pdf">here</a> to download the pdf</p>
</body>
</html>

itextPdf.css에서 적용할 간단한 스타일 입니다.

body{font-family: AppleSDGothicNeo, sans-serif; font-size:12px; color:#222; background:#fff; height: 100%;}
p{background-color: #e73a3a;}
img{border:0; vertical-align:middle;}

 

사용할 DTO 입니다

@Getter
@Setter
public class ItextPdfDto {
    private String pdfCode; // pdf 종류가 여러개일 경우 html 을 구분하기 위한 코드
    private String pdfFilePath; // pdf 파일이 저장될 경로
    private String pdfFileName; // pdf 파일명
}

 

 

이어서

ItextPdfUtil 파일에 하나씩 기술 하겠습니다.

 

임의로 정의된 html을 담아둘 메소드입니다

* 현재 글에선 hyeok만 사용 할 예정입니다*

// 사용할 html 코드를 가져오는 메소드
public String getHtml(String code) {

    String return_html = "";

    switch (code) {
        case "jeon" :
            return_html = "<html>" +
                    "<body>" +
                    "<h1>jeon</h1>" +
                    "</body>" +
                    "</html>";
            break;
        case "nam" :
            return_html = "<html>" +
                    "<body>" +
                    "<h1>nam</h1>" +
                    "<p>CSS 테스트 입니다.</p>" +
                    "</body>" +
                    "</html>";
            break;
        case "hyeok" :
            return_html = "<html>" +
                    "<body>" +
                    "<h1>hyeok</h1>" +
                    "<p>이미지 테스트 합니다.</p>" +
                    "<img src='http://localhost:8080/images/test.png' />" +
                    "</body>" +
                    "</html>";
            break;
    }

    return return_html;
}

 

 

 

실제 itext가 사용되는 메소드 입니다 ( ItextPdfUtil 안에 있습니다 )

/*
     * iText 라이브러리를 사용한 PDF 파일 생성
     * CSS , Font 설정 기능 포함
     * */
    public void createPDF(ItextPdfDto itextPdfDto) {

        // 최초 문서 사이즈 설정
        Document document = new Document(PageSize.B4, 30, 30, 30, 30);

        try {
            // PDF 파일 생성
            PdfWriter pdfWriter = PdfWriter.getInstance(document, new FileOutputStream(itextPdfDto.getPdfFilePath()+itextPdfDto.getPdfFileName()));
            // PDF 파일에 사용할 폰트 크기 설정
            pdfWriter.setInitialLeading(12.5f);
            // PDF 파일 열기
            document.open();

            // XMLWorkerHelper xmlWorkerHelper = XMLWorkerHelper.getInstance();

            // CSS 설정 변수 세팅
            CSSResolver cssResolver = new StyleAttrCSSResolver();
            CssFile cssFile = null;

            try {
                /*
                 * CSS 파일 설정
                 * 기존 방식은 FileInputStream을 사용했으나, jar 파일로 빌드 시 파일을 찾을 수 없는 문제가 발생
                 * 따라서, ClassLoader를 사용하여 파일을 읽어오는 방식으로 변경
                 */
                InputStream cssStream = getClass().getClassLoader().getResourceAsStream("static/css/ItextPdf.css");

                // CSS 파일 담기
                cssFile = XMLWorkerHelper.getCSS(cssStream);
//                cssFile = XMLWorkerHelper.getCSS(new FileInputStream("src/main/resources/static/css/test.css"));
            } catch (Exception e) {
                throw new IllegalArgumentException("PDF CSS 파일을 찾을 수 없습니다.");
            }

            if(cssFile == null) {
                throw new IllegalArgumentException("PDF CSS 파일을 찾을 수 없습니다.");
            }

            // CSS 파일 적용
            cssResolver.addCss(cssFile);

            // PDF 파일에 HTML 내용 삽입
            XMLWorkerFontProvider fontProvider = new XMLWorkerFontProvider(XMLWorkerFontProvider.DONTLOOKFORFONTS);

            /*
             * 폰트 설정
             * CSS 와 다르게, fontProvider.register() 메소드를 사용하여 폰트를 등록해야 함
             * 해당 메소드 내부에서 경로처리를 하여 개발, 배포 시 폰트 파일을 찾을 수 있도록 함
             * */
            try {
                fontProvider.register("static/font/AppleSDGothicNeoR.ttf", "AppleSDGothicNeo");
            } catch (Exception e) {
                throw new IllegalArgumentException("PDF 폰트 파일을 찾을 수 없습니다.");
            }

            if(fontProvider.getRegisteredFonts() == null) {
                throw new IllegalArgumentException("PDF 폰트 파일을 찾을 수 없습니다.");
            }

            // 사용할 폰트를 담아두었던 내용을
            // CSSAppliersImpl에 담아 적용
            CssAppliers cssAppliers = new CssAppliersImpl(fontProvider);

            // HTML Pipeline 생성
            HtmlPipelineContext htmlPipelineContext = new HtmlPipelineContext(cssAppliers);
            htmlPipelineContext.setTagFactory(Tags.getHtmlTagProcessorFactory());

            // ========================================================================================
            // Pipelines
            PdfWriterPipeline pdfWriterPipeline = new PdfWriterPipeline(document, pdfWriter);
            HtmlPipeline htmlPipeline = new HtmlPipeline(htmlPipelineContext, pdfWriterPipeline);
            CssResolverPipeline cssResolverPipeline = new CssResolverPipeline(cssResolver, htmlPipeline);
            // ========================================================================================


            // ========================================================================================
            // XMLWorker
            XMLWorker xmlWorker = new XMLWorker(cssResolverPipeline, true);
            XMLParser xmlParser = new XMLParser(true, xmlWorker, StandardCharsets.UTF_8);
            // ========================================================================================


            /* HTML 내용을 담은 String 변수
            주의점
            1. HTML 태그는 반드시 닫아야 함
            2. xml 기준 html 태그 확인( ex : <p> </p> , <img/> , <col/> )
            위 조건을 지키지 않을 경우 DocumentException 발생
            */
            String htmlStr = getHtml(itextPdfDto.getPdfCode());

            // HTML 내용을 PDF 파일에 삽입
            StringReader stringReader = new StringReader(htmlStr);
            // XML 파싱
            xmlParser.parse(stringReader);
            // PDF 문서 닫기
            document.close();
            // PDF Writer 닫기
            pdfWriter.close();

        } catch (DocumentException e1) {
            throw new IllegalArgumentException("PDF 라이브러리 설정 에러");
        } catch (FileNotFoundException e2) {
            e2.printStackTrace();
            throw new IllegalArgumentException("PDF 파일 생성중 에러");
        } catch (IOException e3) {
            e3.printStackTrace();
            throw new IllegalArgumentException("PDF 파일 생성중 에러2");
        } catch (Exception e4) {
            e4.printStackTrace();
            throw new IllegalArgumentException("PDF 파일 생성중 에러3");
        }
        finally {
            try {
                document.close();
            } catch (Exception e) {
                System.out.println("PDF 파일 닫기 에러");
                e.printStackTrace();
            }
        }

    }

 

 

추가적인 메소드로 PDF 파일이 매번 생성되지 않게 하기 위한 메소드 입니다.

/*
 * PDF 유무를 체크한 후
 * PDF 파일이 없을 경우 PDF 파일 생성 메소드 실행
 */
public File checkPDF (ItextPdfDto pdfDto) {
    File file = new File(pdfDto.getPdfFilePath(),pdfDto.getPdfFileName());
    int fileSize = (int) file.length();
    if (fileSize == 0) {
        createPDF(pdfDto);
        file = new File(pdfDto.getPdfFilePath(),pdfDto.getPdfFileName());
    }
    return file;
}

 

 

생성된 파일을 다운로드 하기위한 컨트롤러 입니다.

@Controller
@RequiredArgsConstructor
public class ItextPdfController {
    @Autowired
    private final ItextPdfUtil itextPdfUtil;

    @RequestMapping("/attachment/pdf")
    public void pdfDownload(HttpServletResponse response) {

        // 미리 준비한 DTO 선언
        ItextPdfDto itextPdfDto = new ItextPdfDto();
        // pdf 파일이 저장될 경로 ( Mac 기준 )
        itextPdfDto.setPdfFilePath("/Users/jeon/Documents/JavaProject/Blog/pdf/");

        // pdf 파일이 저장될 경로 ( Windows 기준 )
        // itextPdfDto.setPdfFilePath("C:\\Users\\hyeok\\Desktop\\pdf");

        // pdf 파일명 ( 테스트를 위해 랜덤으로 생성 )
        itextPdfDto.setPdfFileName(new Random().nextInt() + ".pdf");
        // itextPdfDto.setPdfFileName("test.pdf");

        // getHtml 에서 호출될 코드명
        itextPdfDto.setPdfCode("hyeok");

        // ======================= PDF 존재 유무 체크 =======================
        // 없다면 PDF 파일 만들기
        File file = itextPdfUtil.checkPDF(itextPdfDto);
        int fileSize = (int) file.length();
        // ===============================================================


        // ===============================================================
        // 파일 다운로드를 위한 header 설정
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment; filename="+itextPdfDto.getPdfFileName()+";");
        response.setContentLengthLong(fileSize);
        response.setStatus(HttpServletResponse.SC_OK);
        // ===============================================================

        // 파일 다운로드
        BufferedInputStream in = null;
        BufferedOutputStream out = null;

        // PDF 파일을 버퍼에 담은 후 다운로드
        try{
            in = new BufferedInputStream(new FileInputStream(file));
            out = new BufferedOutputStream(response.getOutputStream());
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            byte[] buffer = new byte[4096];
            int read = 0;
            while ((read = in.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
                Objects.requireNonNull(out).flush();
                out.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

 

 

이상으로 Java itext를 사용한 PDF만들기 ( HTML을 PDF로 만들기 ) 입니다.

아래 링크에서 원본 소스 확인 가능합니다.

 

GitHub : https://github.com/wjsskagur/itextPdf

 

 

감사합니다.

 

반응형

+ Recent posts