20221007 [Spring Boot, JPA]

Yeoonnii·2022년 10월 10일
0

TIL

목록 보기
43/52
post-thumbnail

게시글 일괄추가

board_select.html

일괄추가 버튼생성
<a th:href="@{/board/insertbatch.do}"><button>일괄추가</button></a>

dto/BoardList.java

DTO 생성
➡️ DTO는 entity와 다르다!
Controller와 View간에 전송받기 위한 용도일 뿐 DB저장/테이블 생성과는 관계 없다

@Data
public class BoardList {
    List<Board> list;
}

BoardController.java

GET으로 보낼때 3개의 비어있는 Board를 생성한다
3개의 비어있는 Board를 BoardList에 담아 ModelAndView로 리턴해준다
➡️ POST에서 받을 때 사용하는 @ModelAttribute는 클래스화된 객체만 받을 수 있다
List<Board> list는 클래스화된 객체가 아니기 때문에 클래스 형태의 DTO BoardList를 생성하여 생성된 list를 담아 리턴해준다

// 일괄추가 페이지 이동
        @GetMapping(value="/insertbatch.do")
        public ModelAndView insertBatch() {

            // 일괄등록될 3개의 게시물 생성함
            List<Board> list = new ArrayList<Board>();
            for(int i=0; i<2; i++){
                Board board = new Board();
                list.add(board);
            }
            // list타입을 반환받을 수 없다
            // class형태로 반환받아야 함 => POST에서 @ModelAttribute 사용하기 때문
            // BoardList에 추가하여 리턴!
            BoardList boardList = new BoardList();
            boardList.setList(list);
            return new ModelAndView("board_insertbatch", "obj", boardList);
        }

board_insertbatch.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>게시글 일괄등록</title>
</head>

<body>
    <a th:href="@{/home.do}">홈으로</a>
    <a th:href="@{/board/insert.do}"><button>게시글 일괄등록</button></a>
    <a th:href="@{/board/select.do}">글목록</a>
    <hr />
    게시글 일괄등록
    <hr />
    
    <form th:action="@{/board/insertbatch.do}" method="post">
        <!-- controler에서 list를 obj로 보냈으니 obj 안의 list를 꺼내줘야 한다 -->
        <!-- list[0].title, list[0].content, list[0].writer  -->
        <!-- list[1].title, list[1].content, list[0].writer  -->
        <!-- list[2].title, list[2].content, list[0].writer  -->
        <th:block th:each="tmp, idx : ${obj.list}">
            <input type="text"  name="|list[$idx.index].title|" placeholder="제목"  /><br />
            <input type="text"  name="|list[$idx.index].content|" placeholder="내용" /><br />
            <input type="text"  name="|list[$idx.index].writer|" placeholder="작성자"  /><br />
            <hr />
        </th:block>
        <hr />
        <input type="submit" value="일괄추가" />
    </form>
</body>
</html>

BoardController.java

    // 일괄 추가하기
    @PostMapping(value="/insertbatch.do")
    public String insertBatchPOST(
            @ModelAttribute BoardList boardList){
        System.out.println(boardList.getList().toString());
        // 일괄추가
        bRepository.saveAll(boardList.getList());
        return "redirect:/board/select.do";
    }

게시글 최신순으로 정렬

BoardController.java

기본 findAllMethod 사용중이니 정렬이 안된다
➡️ List<Board> list = bRepository.findAll();

Repository에 게시글 정렬하여 조회하는 메서드 생성하여 사용하기

BoardRepository.java

Repository에 No내림차순 정렬하여 조회하는 상속메서드 생성

// 조건은 없음! 전체데이터를 내림차순으로만 정렬
    List<Board> findByOrderByNoDesc();

BoardController.java

생성된 메서드를 사용한다
List<Board> list = bRepository.findByOrderByNoDesc();

결과화면


게시글 일괄수정

board_select.html

일괄 수정은 일괄 삭제와 동일한 과정으로 진행한다
➡️ 효율적으로 코드를 작성 하려면 일괄 삭제와 일괄 수정 코드 작성시 중복되는 코드를
하나의 스크립트로 같이 사용하고 type만 따로 주면 된다

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>게시판 글목록</title>
</head>

<body>
    <a th:href="@{/home.do}">홈으로</a>
    <a th:href="@{/board/insert.do}"><button>게시글 등록</button></a>

    <input type="button" value="게시글 일괄삭제" th:onclick="|javascript:handleBatch(1)|" />
    <a th:href="@{/board/insertbatch.do}"><button>게시글 일괄추가</button></a>
    <input type="button" value="게시글 일괄수정" th:onclick="|javascript:handleBatch(2)|" />

    <table border="1">
        <tr>
            <th>글번호</th>
            <th>글제목</th>
            <th>작성자</th>
            <th>글내용</th>
            <th>조회수</th>
            <th>등록일</th>
            <th>버튼</th>
        </tr>
        <tr th:each="obj, idx : ${list}">
            <td><input type="checkbox" th:value="${obj.no}" class="chk" /> </td>
            <td th:text="${obj.no}"></td>
            <td>
                <a th:href="@{/board/selectone.do(no=${obj.no})}" th:text="${obj.title}"></a>
            </td>
            <!-- <td th:text="${obj.title}"></td> -->
            <td th:text="${obj.writer}"></td>
            <td th:text="${obj.content}"></td>
            <td th:text="${obj.hit}"></td>
            <td th:text="${obj.regdate}"></td>
            <td>
                <form th:action="@{/board/delete.do}" method="post">
                    <input type="hidden" name="no" th:value="${obj.no}" />
                    <input type="submit" value="삭제" />
                </form>
                <form th:action="@{/board/update.do}" method="get">
                    <input type="hidden" name="no" th:value="${obj.no}" />
                    <input type="submit" value="수정" />
                </form>
                <!-- title누르면 댓글작성으로 이동 -->
                <form th:action="@{/boardimage/insert.do}" method="get">
                    <input type="hidden" name="no" th:value="${obj.no}" />
                    <input type="submit" value="이미지등록" />
                </form>
            </td>
        </tr>
    </table>
    </form>

    <script>
        const handleBatch = (type) => {
            // form태그 생성
            const form = document.createElement("form");
            if (type == 1) {
                form.action = "[[@{/board/deletebatch.do}]]";
                form.method = "post";
            } else if (type == 2) {
                form.action = "[[@{/board/updatebatch.do}]]";
                form.method = "get";
            }

            form.style.display = "none";

            document.body.appendChild(form);

            // 위의 체크박스 전체 가져오기
            const chk = document.getElementsByClassName("chk");
            for (let i = 0; i < chk.length; i++) {
                if (chk[i].checked) { //체크된 항목만 찾기
                    const check = document.createElement("input");
                    check.type = "checkbox";
                    check.name = "chk";
                    check.value = chk[i].value;
                    check.checked = true;

                    form.appendChild(check); // 체크된 항목만 form에 추가
                }
            }
            // form을 body에 추가
            document.body.appendChild(form);
            // form 전송
            form.submit();
        }
    </script>
</body>

</html>

BoardController.java

일괄수정페이지로 이동

GET으로 보낼때 3개의 비어있는 Board를 생성한다
3개의 비어있는 Board를 BoardList에 담아 ModelAndView로 리턴해준다
➡️ POST에서 받을 때 사용하는 @ModelAttribute는 클래스화된 객체만 받을 수 있다
List<Board> list는 클래스화된 객체가 아니기 때문에 클래스 형태의 DTO BoardList를 생성하여 생성된 list를 담아 리턴해준다

    // 게시글 일괄수정 페이지로 이동
    @GetMapping(value = "/updatebatch.do")
    public ModelAndView insertBatch(
            @RequestParam(name = "chk") List<Long> chk) {
        List<Board> list = bRepository.findAllById(chk);

        // list타입을 반환받을 수 없다
        // class형태로 반환받아야 함 => POST에서 @ModelAttribute 사용하기 때문 
        // BoardList에 추가하여 리턴!
        BoardList boardList = new BoardList();
        boardList.setList(list);
        return new ModelAndView("board_updatebatch", "obj", boardList);
    }

board_updatebatch.html

BoardController에서 보내준 3개의 비어있는 Board에 사용자가 입력한 데이터를 채워 넘겨준다

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>게시글 일괄수정</title>
</head>

<body>
    <a th:href="@{/home.do}">홈으로</a>
    <a th:href="@{/board/select.do}">글목록</a>
    <hr />
    게시글 일괄수정
    <hr />
    
    <form th:action="@{/board/updatebatch.do}" method="post">
        <!-- controler에서 list를 obj로 보냈으니 obj 안의 list를 꺼내줘야 한다 -->
        <!-- list[0].title, list[0].content, list[0].writer  -->
        <!-- list[1].title, list[1].content, list[0].writer  -->
        <!-- list[2].title, list[2].content, list[0].writer  -->
        <th:block th:each="tmp, idx : ${obj.list}">
            <input type="text"  th:name="|list[${idx.index}].no|" th:value="${tmp.no}" readonly /><br />
            <input type="text"  th:name="|list[${idx.index}].title|" th:value="${tmp.title}" /><br />
            <input type="text"  th:name="|list[${idx.index}].content|" th:value="${tmp.content}"/><br />
            <input type="text"  th:name="|list[${idx.index}].writer|" th:value="${tmp.writer}"  /><br />
            <hr />
        </th:block>
        <hr />
        <input type="submit" value="일괄수정" />
    </form>
</body>
</html>

BoardController.java

  1. boardList의 목록을 이용하여 기존 데이터 읽기
    💡 이때 for문을 사용하지 않고 바로 저장하는 경우 bRepository.save
    기존 정보가 초기화 되어 저장된다
  2. 기존데이터에 받은 데이터로 변경하기
    ➡️ Board tmp = bRepository.findById + tmp.set~
  3. 일괄추가
    ➡️ bRepository.saveAll
    // 일괄수정하기
    @PostMapping(value = "/updatebatch.do")
    public String updateBatch(
        @ModelAttribute BoardList boardList) {
        System.out.println(boardList.getList().toString());

        List<Board> data = new ArrayList<>();

        // 1.boardList의 목록을 이용하여 기존 데이터 읽기
        // 이떄 for문을 사용하지 않고 바로 save 하면 기존 정보가 초기화 된다
        for(Board board : boardList.getList()){
            // 2. 기존데이터에 받은 데이터로 변경하기
            Board tmp = bRepository.findById(board.getNo()).orElse(null);
            tmp.setTitle(board.getTitle());
            tmp.setContent(board.getContent());
            tmp.setWriter(board.getWriter());
            data.add(tmp);
        }

        // 일괄추가
        bRepository.saveAll(boardList.getList());
        return "redirect:/board/select.do";
    }

Spring Security를 이용한 사용자 회원가입

프로젝트 만들떄 가장 먼저 해야하는것
1. 권한설정(fliter)
2. 로그인/로그아웃 만들기
3. 사용자 별 권한설정

pom.xml

Spring Security 라이브러리 추가

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

config/SecurityConfig.java

Spring Security 필터는 config 폴더에서 설정
fliter가 설정되지 않으면 로그인 페이지 사라지지 않는다
➡️ fliter 설정되어야 user로그인 페이지 사라짐

  • @Configuration 어노테이션 명시해준다
  • 암호화 메서드는 로그인시 사용한다
    ➡️ 가입시 암호화된 암호를 사용하기 위해
  • @Bean ➡️ 서버구동시 자동으로 객체 생성
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    SecurityLoginService securityLoginService;
    
    // 필터 설정 하기
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) 
            throws Exception{
        
        // 권한설정
        // 127.0.0.1:8080/ROOT/admin/****  ADMIN
        // 127.0.0.1:8080/ROOT/seller/***  SELLER
        http.authorizeHttpRequests()
            .antMatchers("/admin", "/admin/**").hasAnyRole("ADMIN")
            .antMatchers("/seller", "/seller/**").hasAnyRole("SELLER")
            .antMatchers("/customer", "/customer/**").hasAnyRole("CUSTOMER")
            .anyRequest().permitAll();

        // 로그인
        http.formLogin()
            .loginPage("/member/login.do")  ///GET화면의 URL
            .loginProcessingUrl("/member/login.do")  //th:action에 들어가는 URL
            .usernameParameter("uid") //아이디 name값에 들어가는 값
            .passwordParameter("upw") //비밀번호 name에 들어가는 값
            .defaultSuccessUrl("/")
            .permitAll();
        
        // 로그아웃
        http.logout()
            .logoutUrl("/member/logout.do")
            .logoutSuccessUrl("/")
            .clearAuthentication(true)
            .invalidateHttpSession(true)
            .permitAll();

        // 직접 생성한 service 등록
        http.userDetailsService(securityLoginService);

        return http.build();
    }


    // 암호화 설정 => 암호의 hash알고리즘 설정
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

Application.java

생성한 config파일 사용위해 application에 config파일 등록

// 서비스,컨트롤러 환경설정
@ComponentScan(basePackages = {
	"com.example.service", 
	"com.example.controller",
	"com.example.config",

})

entity/Member.java

@Entity
@Data
@Table(name = "MEMBERTBL")
public class Member {
    @Id
    @Column(length = 30)
    String userid;
    
    @Column(length = 200)
    String userpw;
    
    int age;

    @Column(length = 15)
    String phone;
    
    @Column(length = 1)
    String gender; //M, F
    
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm.ss.SSS")
    @CreationTimestamp
    // updatable => 수정시에도 날짜 갱신/변경여부
    @Column(name = "REGDATE", updatable = false)
    private Date regdate = null;
    
    @Column(length = 20)
    String role; //ADMIN, CUSTOMER, SELLER
    
    @Column(length = 1)
    int block;

    @JsonBackReference(value = "member1")
    @OneToMany(mappedBy = "member") 
    List<Item> item;
}

MemberController.java

SecurityConfig에서 만들어진 암호화 알고리즘을 @Autowired 하여 사용
➡️ 암호화 알고리즘을 Controller에서 생성하여 사용하는것 보다
SecurityConfig에서 @Bean으로 생성하여 @Autowired하여 사용하는게 더 좋다

@Controller
@RequestMapping(value = {"/member"})
public class MemberController {

    @Autowired PasswordEncoder passwordEncoder;
    @Autowired MemberRepository mRepository;

    @GetMapping(value = {"/join.do"})
    public String joinGET(){
        return "member_join";
    }

    @PostMapping(value="/join.do")
    public String joinPOST(@ModelAttribute Member member) {
        // 사용자가 입력한 비밀번호 암호화 하기
        member.setUserpw(passwordEncoder.encode(member.getUserpw()));

        mRepository.save(member);
        return "redirect:/";
    }

member_join.html

회원가입 페이지 생성 ➡️ member entity보면서 생성

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>회원가입</title>
</head>

<body>
    <h3>회원가입</h3>

    <hr />
    <a th:href="@{/home.do}"><button>홈화면</button></a>
    <form th:action="@{/member/join.do}" method="post">
            <input type="text"  name="userid" placeholder="아이디"  /><br />
            <input type="text"  name="userpw" placeholder="암호" /><br />
            <input type="text"  name="age" placeholder="나이"  /><br />
            <input type="text"  name="phone" placeholder="연락처" /><br />
            <input type="text"  name="gender" placeholder="성별(M/F)" /><br />
            <select name="role">
                <option value="CUSTOMER">고객</option>
                <option value="SELLER">판매자</option>
                <option value="ADMIN">관리자</option>
            </select>
            <br />
        <hr />
        <input type="submit" value="일괄추가" />
    </form>
</body>
</html>

MemberRepository.java

@Repository
public interface MemberRepository extends JpaRepository<Member, String> {
    
}

Spring Security CSRF

CSRF

CSRF란 웹 애플리케이션의 취약점 중 하나로,
이용자가 의도하지 않은 요청을 통한 공격을 의미한다

즉 CSRF 공격이란, 인터넷 사용자(희생자)가 자신의 의지와는 무관하게 공격자가 의도한 행위(POST, PUT, DELETE 등)를 특정 웹사이트에 요청하도록 만드는 공격이다

CSRF protection은 Spring Security에서 default로 설정된다
즉, protection을 통해 GET요청을 제외한 상태를 변화시킬 수 있는 POST, PUT, DELETE 요청으로부터 보호한다

CSRF Token

csrf protection을 적용하였을 때
임의의 CSRF Token을 발급한 후 POST, PUT, DELETE 요청일 경우 Token 값을 확인한 후 클라이언트가 정상적인 요청을 보낸것인지 확인한다

html에서 CSRF Token이 포함되어야 POST, PUT, DELETE 요청을 받아들이게 되며 이 과정을 거쳐 위조 요청을 방지하게 된다

board_select.html

Spring Security에서 발급된 CSRF Token 확인
GET 요청에는 CSRF Token이 없고 POST요청에만 CSRF Token이 존재하는것을 확인 할 수 있다


Spring Security를 이용한 사용자 로그인

  1. 서비스 생성 SecurityLoginService
  2. SecurityConfig.java에서
    @Autowired SecurityLoginService securityLoginService; 하여 사용

config/SecurityConfig.java

로그인의 화면은 반드시 환경설정에 설정된 주소에서만 가능하다
➡️ 설정된 주소 확인하며 html작성하기
환경설정과 일치하게 주소를 작성해야 로그인 가능하다
@Autowired하여 직접 생성한 service 등록
http.userDetailsService(securityLoginService);

// 로그인
        http.formLogin()
            .loginPage("/member/login.do")  ///GET화면의 URL
            .loginProcessingUrl("/member/login.do")  //th:action에 들어가는 URL
            .usernameParameter("uid") //아이디 name값에 들어가는 값
            .passwordParameter("upw") //비밀번호 name에 들어가는 값
            .defaultSuccessUrl("/")
            .permitAll();
...
        // 직접 생성한 service 등록
        http.userDetailsService(securityLoginService);

        return http.build();

service/SecurityLoginService.java

package com.example.service;

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.example.entity.Member;
import com.example.repository.MemberRepository;


// 로그인 화면에서 전달되어 호출되는 서비스
@Service
public class SecurityLoginService implements UserDetailsService{

    @Autowired
    MemberRepository mRepository;

    
    // 로그인 화면에서 전달되어 호출되는 오버라이드 메서드
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("=====================username===================");
        System.out.println(username);

        // 위에서 memberid 를 통해 member정보 꺼내기
        Member member = mRepository.findById(username).orElse(null);

        if(member != null){ // 조회된 회원정보가 있는 경우
            // 권한정보는 그냥 만들면 안된다! 배열형태로 만들어줘야함
            String[] str = { member.getRole() };
            Collection<GrantedAuthority> role = AuthorityUtils.createAuthorityList(str);
            
            // 조회된 정보에서 아이디, 암호, 권한정보를 가져온다
            User user = new User(member.getUserid(), member.getUserpw(), role);
            return user;

        }
        else {  // 조회된 회원정보가 없는경우
            // 오류처럼 보이지 않게 하기 위해
            String[] str = { "_" };
            Collection<GrantedAuthority> role =  AuthorityUtils.createAuthorityList(str);
            User user = new User("", "_", role );
            return user; 
        }
    }
}

Application.java

생성한 service파일 사용위해 application에 service파일 등록

// 서비스,컨트롤러 환경설정
@ComponentScan(basePackages = {
	"com.example.service", 
	"com.example.controller",
	"com.example.config",

})

Spring Security를 이용한 사용자 로그아웃

config/SecurityConfig.java

로그아웃 주소 설정

        // 로그아웃
        http.logout()
            .logoutUrl("/member/logout.do")
            .logoutSuccessUrl("/")
            .clearAuthentication(true)
            .invalidateHttpSession(true)
            .permitAll();

권한별 주소분배

프로젝트시 페이지 설계시 주소 분배 기준 정하기

1. 공통주소 설정

권한에 관계 없는 페이지

  • 127.0.0.1:8080/ROOT/home.do
  • 127.0.0.1:8080/ROOT/login.do
  • 127.0.0.1:8080/ROOT/join.do

2. 권한 별 주소 설정

권한을 필요로하는 방법은 권한별로 주소를 분리

  • admin 권한 별 주소
    127.0.0.1:8080/ROOT/admin/home.do
    127.0.0.1:8080/ROOT/admin/insert.do
    127.0.0.1:8080/ROOT/admin/select.do

  • customer 권한 별 주소
    127.0.0.1:8080/ROOT/customer/home.do

  • seller 권한 별 주소
    127.0.0.1:8080/ROOT/seller/home.do

0개의 댓글