Java/Spring Boot

Spring Boot 기초(security) - AWS 풀스택 과정 81일차

awspspgh 2024. 11. 20. 09:26
목차
1. security

 

1. security

header.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!-- th:fragment="이름" : 레이아웃에서 사용할 조각 -->
<div th:fragment="header" class="container">
  <nav class="navbar navbar-expand-lg bg-body-tertiary">
    <div class="container-fluid">
      <a class="navbar-brand" href="#">Boot</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">

          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="/">Home</a>
          </li>

          <li class="nav-item" sec:authorize="isAuthenticated()">
            <a class="nav-link" href="/board/register">BoardRegister</a>
          </li>

          <li class="nav-item">
            <a class="nav-link" href="/board/list">BoardList</a>
          </li>

          <th:block sec:authorize="isAnonymous()">
          <li class="nav-item">
            <a class="nav-link" href="/user/signup">SignUp</a>
          </li>

          <li class="nav-item">
            <a class="nav-link" href="/user/login">Login</a>
          </th:block>

          <li class="nav-item" sec:authorize="hasRole('ROLE_ADMIN')">
            <a class="nav-link" href="/user/list">userList</a>
          </li>

          <li class="nav-item" sec:authorize="isAuthenticated()">
            <a class="nav-link" href="/user/modify" th:with="auth=${#authentication.getPrincipal()}">([[${auth.userVO.email}]])modify</a>
          </li>

          </li><li class="nav-item" sec:authorize="isAuthenticated()">
            <a class="nav-link" href="/user/last">Log out</a>
          </li>


        </ul>
      </div>
    </div>
  </nav>
  <h5>Header Area</h5>
</div>

 

 detail.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<div layout:fragment="content" class="container-md">
    <h1>Boot Detail page</h1>
    <hr>

    <th:block th:with="bvo=${bdto.bvo}">
    <form action="/board/modify" method="post" id="modForm" enctype="multipart/form-data">
        <div class="mb-3">
            <label for="r" class="form-label">Created At</label>
            <input type="text" class="form-control" name="regDate" id="r" th:value="${bvo.regDate}" readonly>
        </div><div class="mb-3">
            <input type="hidden" class="form-control" name="bno" id="n" th:value="${bvo.bno}">
        </div>
        <div class="mb-3">
            <label for="title" class="form-label">Title</label>
            <input type="text" class="form-control" name="title" id="title" th:value="${bvo.title}" readonly>
        </div>
        <div class="mb-3">
            <label for="writer" class="form-label">Writer</label>
            <input type="text" class="form-control" name="writer" id="writer" th:value="${bvo.writer}" readonly>
        </div>
        <div class="mb-3">
            <label for="content" class="form-label">Content</label>
            <textarea class="form-control" name="content" id="content" rows="3" readonly>[[${bvo.content}]]</textarea>
        </div>
        <!-- file print line -->
        <div class="mb-3">
            <ul class="list-group">
                <li th:each="fvo:${bdto.flist}" class="list-group-item">
                    <div th:if="${fvo.fileType > 0}" class="ms-2 me-auto">
                        <img th:src="@{|/upload/${fvo.saveDir}/${fvo.uuid}_${fvo.fileName}|}" alt="img" />
                    </div>
                    <div th:unless="${fvo.fileType > 0}" class="ms-2 me-auto">
                        <!-- icon -->
                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-paperclip" viewBox="0 0 16 16">
                            <path d="M4.5 3a2.5 2.5 0 0 1 5 0v9a1.5 1.5 0 0 1-3 0V5a.5.5 0 0 1 1 0v7a.5.5 0 0 0 1 0V3a1.5 1.5 0 1 0-3 0v9a2.5 2.5 0 0 0 5 0V5a.5.5 0 0 1 1 0v7a3.5 3.5 0 1 1-7 0z"/>
                        </svg>
                    </div>
                    <div class="ms-2 me-auto">
                        <div class="fw-bold">[[${fvo.fileName}]]</div>
                        [[${fvo.regDate}]]
                    </div>
                    <span class="badge text-bg-success rounded-pill">[[${fvo.fileSize}]]Bytes</span>
                    <button type="button" th:data-uuid="${fvo.uuid}" class="btn btn-outline-danger bnt-nm file-x" disabled>x</button>
                </li>
            </ul>
        </div>
        <!-- file 추가 라인 -->
        <div class="mb-3">
            <input type="file" class="form-control" name="files" id="file" multiple style="display:none;">
        </div>
        <button type="button" id="trigger" class="btn btn-primary" disabled>File Upload</button> <br>
        <button type="button" id="listBtn" class="btn btn-primary">List</button>
        <!-- de-tail page에서 modify 상태로 변경하기 위한 버튼 -->
        <button type="button" id="modBtn" class="btn btn-warning">Modify</button>
        <a th:href="@{/board/delete(bno=${bvo.bno})}">
            <button type="button" id="delBtn" class="btn btn-danger">Delete</button>
        </a>
    </form>
        <!-- comment line -->
        <!-- post -->
        <div class="input-group mb-3" sec:authorize="isAuthenticated()">
            <span class="input-group-text" id="cmtWriter" th:text="${#authentication.getPrincipal().userVO.nickName}"></span>
            <input type="text" id="cmtText" class="form-control" placeholder="Add Comment..." aria-label="Username" aria-describedby="basic-addon1">
            <button type="button" id="cmtAddBtn" class="btn btn-secondary">post</button>
        </div>
        <!-- spread -->
        <ul class="list-group list-group-flush" id="cmtListArea">
            <li class="list-group-item">
                <div class="ms-2 me-auto">
                    <div class="fw-bold">writer</div>
                    Content
                </div>
                <span class="badge text-bg-primary rounded-pill">regDate</span>
            </li>
        </ul>
        <!-- moreBtn-->
        <div>
            <button type="button" id="moreBtn" data-page="1" class="btn btn-dark" style="visibility:hidden">MORE + </button>
        </div>
        <!-- modal -->
        <div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h1 class="modal-title fs-5" id="cmtWriterMod">${authNick }</h1>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-body">
                        <input type="text" class="form-control" id="cmtTextMod">
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                        <button type="button" id="cmtModBtn" class="btn btn-primary">changes</button>
                    </div>
                </div>
            </div>
        </div>
    <script th:inline="javascript" >
        let bnoVal = [[${bvo.bno}]];
        console.log(bnoVal);
        let nickName = "";
        console.log(nickName);
    </script>

    <th:block sec:authorize="isAuthenticated()" th:with="auth=${#authentication.getPrincipal().userVO}">
        <script th:inline="javascript" >
            nickName = [[${auth.nickName}]];
            console.log(nickName);
        </script>
    </th:block>

    </th:block>

    <script th:src="@{/js/boardDetail.js}"></script>
    <script th:src="@{/js/boardRegister.js}"></script>
    <script th:src="@{/js/boardComment.js}"></script>

    <script>
        spreadCommentList(bnoVal);
    </script>

</div>

 

 list.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<div layout:fragment="content" class="container-md">
    <h1>User List Page</h1>
    <hr>
    <div class="col" th:each="uvo:${userList}">
        <div class="card border-dark mb-3" style="max-width: 540px;">
            <div class="row g-0">
                <div class="col-md-4">
                    <img src="/image/boo.png" class="img-fluid rounded-start" alt="...">
                </div>
                <div class="col-md-8">
                    <div class="card-body">
                        <h5 class="card-title">[[${uvo.nickName}]]</h5>
                        <p class="card-text">[[${uvo.email}]] ([[${uvo.regDate}]]) </p>
                        <p class="card-text">Last Login : [[${uvo.lastLogin}]]</p>

                            <span class="badge rounded-pill text-bg-success" th:each="authList:${uvo.authList}">[[${authList.auth}]]</span>

                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

 

 modify.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content" class="container-md">
    <h1>User List Page</h1>
    <hr>
    <div class="mt-4 p-5 rounded" th:with="auth=${#authentication.getPrincipal()}">
        <div class="row">
            <div class="col">
                <div class="card border-dark mb-3" style="max-width: 540px;" >
                    <div class="row g-0">
                        <div class="col-md-4">
                            <img src="/image/boo.png" class="img-fluid rounded-start" alt="...">
                        </div>
                        <div class="col-md-8">
                            <form action="/user/modify" method="post">
                                <h5 class="card-body">
                                    <input type="email" name="email" class="form-control" th:value="${auth.userVO.email}" readonly>
                                    <input type="text" name="nickName" class="form-control"  th:value="${auth.userVO.nickName}">
                                    <input type="text" name="pwd" class="form-control" placeholder="Enter Password">
                                </h5>
                                    <span class="badge rounded-pill text-bg-success" th:each="authList:${auth.userVO.authList}">[[${authList.auth}]]</span>
                                <button type="submit" class="btn btn-primary btn-sm">modify</button>
                                <a th:href="@{/user/remove}"><button type="button" class="btn btn-danger btn-sm">delete</button></a>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

</html>

 

 boardComment.js

console.log("boardComment.js in");
console.log(bnoVal);
console.log(nickName);

document.getElementById('cmtAddBtn').addEventListener('click',()=>{
    const cmtText = document.getElementById('cmtText');
    if(cmtText == null || cmtText == ''){
        alert('댓글 내용을 입력해주세요.')
        cmtText.focus();
        return false;
    }
    const cmtData = {
        bno : bnoVal,
        writer : document.getElementById('cmtWriter').innerText,
        content : cmtText.value
    };
    postCommentToServer(cmtData).then(result => {
        if(result > 0){
            alert("댓글 등록 성공");
            cmtText.value="";
        }
        // 댓글 뿌리기 호출
        spreadCommentList(bnoVal);
    })
});

function spreadCommentList(bnoVal, page=1){
    getCommentListFromServer(bnoVal, page).then(result =>{
        console.log(result);
        const ul = document.getElementById('cmtListArea');
        if(result.cmtList.length > 0){
            if(page == 1){
                ul.innerHTML=""; // 기존 샘플값 비우기.
            }
            for(let cvo of result.cmtList){
                let li = `<li data-cno = "${cvo.cno}" class="list-group-item">`;
                li += `<div class="ms-2 me-auto">`;
                li += `<div class="fw-bold">${cvo.writer}</div>`;
                li += `${cvo.content}`;
                li += `</div>`;
                li += `<span class="badge text-bg-primary rounded-pill">${cvo.regDate}</span>`;
                if(nickName == cvo.writer){
                    li += `<button type="button" class="btn btn-outline-warning btn-sm mod" data-bs-toggle="modal" data-bs-target="#myModal">%</button>`;
                    li += `<button type="button" class="btn btn-outline-danger btn-sm del">X</button>`;
                }
                li += `</li>`;
                ul.innerHTML += li;
            }

            // page 처리
            let moreBtn= document.getElementById('moreBtn');
            // 전체 페이지가 현재 페이지보다 크다면... (나와야 할 페이지가 존재한다면...)
            if(result.pgvo.pageNo < result.realEndPage){
                moreBtn.style.visibility = "visible"; // 표시
                moreBtn.dataset.page = page + 1;
            }else{
                moreBtn.style.visibility = "hidden"; // 숨김
            }

        }else{
            let li = `<li class="list-group-item">Comment List Empty</li>`;
            ul.innerHTML = li;
        }
    });
};

document.addEventListener('click',(e)=> {
    if(e.target.id == 'moreBtn'){
        let page = parseInt(e.target.dataset.page);
        spreadCommentList(bnoVal, page);
    }
    if(e.target.classList.contains('del')){
        let cno = e.target.closest('li').dataset.cno;
        removeCommentToServer(cno).then(result => {
            if(result > 0){
                alert("삭제 성공!!");
                spreadCommentList(bnoVal);
            }
        })
    }
    if(e.target.classList.contains('mod')){
        let li = e.target.closest('li');
        let modWriter = li.querySelector(".fw-bold").innerText;
        document.getElementById('cmtWriterMod').innerText = modWriter;
        let cmtText =li.querySelector('.fw-bold').nextSibling; // 쿼리 값의 같은 부모의 다른 형제 값
        document.getElementById('cmtTextMod').value = cmtText.nodeValue; // 그냥 넣으면 Object로 입력
        
        // 수정 버튼 id = cmtModBtn dataset
        document.getElementById('cmtModBtn').setAttribute("data-cno", li.dataset.cno);
    }
    if(e.target.id == 'cmtModBtn'){
        let cmtModData={
            cno : e.target.dataset.cno,
            content : document.getElementById('cmtTextMod').value
        }
        console.log(cmtModData);

        // 비동기 처리
        updateCommentToServer(cmtModData).then(result =>{
            if(result > 0){
                alert("수정 성공!!");
            }
            // 모달창 닫기
            document.querySelector('.btn-close').click();
            spreadCommentList(bnoVal);
        });
    }
});

async function updateCommentToServer(cmtModData) {
    try{
        const url = "/comment/update"
        const config ={
            method : "put",
            headers : {
                'content-type':'application/json; charset=utf-8'
            },
            body : JSON.stringify(cmtModData)
        }
        const resp = await fetch(url, config);
        const result = await resp.text();
        return result;
    } catch(error){
        console.log(error);
    }

}

async function removeCommentToServer(cno) {
    try{
        const url = "/comment/remove/"+cno;
        const config={
            method:'delete'
        }
        const resp = await fetch(url, config);
        const result = await resp.text();
        return result;
    } catch(error){
        console.log(error);
    }
}

async function postCommentToServer(cmtData) {
    try {
        const url = "/comment/post";
        const config = {
            method: "post",
            headers:{
                'Content-Type':'application/json; charset=utf-8'
            },
            body: JSON.stringify(cmtData)
        };
        const resp = await fetch(url, config);
        const result = await resp.text();
        return result;
    } catch (error) {
        console.log(error);
    }
}

async function getCommentListFromServer(bnoVal, page) {
    try{
        const resp = await fetch("/comment/list/"+bnoVal+"/"+page);
        const result = await resp.json();
        return result;
    } catch(error){
        console.log(error);
    }
}

 

 UserController.java

package com.ezen.spring.controller;

import com.ezen.spring.domain.UserVO;
import com.ezen.spring.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.security.Principal;

@Slf4j
@RequestMapping("/user/*")
@RequiredArgsConstructor
@Controller
public class UserController {
    private final UserService usv;
    private final PasswordEncoder passwordEncoder;

    @GetMapping("/signup")
    public String signup(){
        return "/user/register";
    }

    @PostMapping("/signup")
    public String singup(UserVO userVO){
        log.info(">>>> userVO {}",userVO);
        userVO.setPwd(passwordEncoder.encode(userVO.getPwd()));
        int isOk = usv.insert(userVO);
        return "/index";
    }

    @GetMapping("/login")
    public String login(){
        return "/user/login";
    }

    @GetMapping("/list")
    public String list(Model m){
        m.addAttribute("userList",usv.getList());
        return "/user/list";
    }

    @GetMapping("/modify")
    public void modify(){}

    @PostMapping("/modify")
    public String modify(UserVO userVO){
        log.info(">>> userVO >>> {}", userVO);
        int isOk = 0;
        if(userVO.getPwd() == null || userVO.getPwd().isEmpty()){
            isOk = usv.modifyNoPwd(userVO);
        } else{
            userVO.setPwd(passwordEncoder.encode(userVO.getPwd()));
            isOk = usv.modify(userVO);
        }
        return "redirect:/user/logout";
    }

    @GetMapping("/remove")
    public String remove(Principal principal){
        log.info(">>> principal >> {}", principal);
        int isOk = usv.remove(principal.getName());
        return "redirect:/user/logout";
    }

    @GetMapping("/last")
    public String lastUpdate(Principal principal){
        int isOk = usv.lastUpdate(principal.getName());
        return "redirect:/user/logout";
    }
}

 

 UserService.java

package com.ezen.spring.service;

import com.ezen.spring.domain.UserVO;

import java.util.List;

public interface UserService {

    int insert(UserVO userVO);

    List<UserVO> getList();

    int modifyNoPwd(UserVO userVO);

    int modify(UserVO userVO);

    int remove(String name);

    int lastUpdate(String name);
}

 

 UserServiceImpl.java

package com.ezen.spring.service;

import com.ezen.spring.domain.UserVO;
import com.ezen.spring.repository.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Service
public class UserServiceImpl implements UserService{
    private final UserMapper userMapper;

    @Transactional
    @Override
    public int insert(UserVO userVO) {
        int isOk = userMapper.insert(userVO);
        if(isOk > 0){
            userMapper.authInsert(userVO.getEmail());
        }
        return isOk;
    }

    @Transactional
    @Override
    public List<UserVO> getList() {
        List<UserVO> userList = userMapper.getList();
        for(UserVO userVO : userList){
            userVO.setAuthList(userMapper.selectAuths(userVO.getEmail()));
        }
        log.info(">>> userList >>> {}", userList);
        return userList;
    }

    @Override
    public int modifyNoPwd(UserVO userVO) {
        return userMapper.updateNoPwd(userVO);
    }

    @Override
    public int modify(UserVO userVO) {
        return userMapper.update(userVO);
    }

    @Override
    public int remove(String name) {
        int isOk = userMapper.removeAuth(name);
        if(isOk > 0){
            userMapper.remove(name);
        }
        return isOk;
    }

    @Override
    public int lastUpdate(String name) {
        return userMapper.lastUpdate(name);
    }
}

 

 UserMapper.java

package com.ezen.spring.repository;

import com.ezen.spring.domain.AuthVO;
import com.ezen.spring.domain.UserVO;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {
    int insert(UserVO userVO);

    void authInsert(String email);

    UserVO selectEmail(String username);

    List<AuthVO> selectAuths(String username);

    List<UserVO> getList();

    int updateNoPwd(UserVO userVO);

    int update(UserVO userVO);

    int remove(String name);

    int removeAuth(String name);

    int lastUpdate(String name);
}

 

 userMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ezen.spring.repository.UserMapper">
    <insert id="insert">
        insert into user(email, pwd, nick_name)
        value(#{email}, #{pwd}, #{nickName})
    </insert>
    <insert id="authInsert">
        insert into auth(email, auth)
        values(#{email}, 'ROLE_USER')
    </insert>
    <select id="selectEmail" resultType="com.ezen.spring.domain.UserVO">
        select * from user where email = #{email}
    </select>
    <select id="selectAuths" resultType="com.ezen.spring.domain.AuthVO">
        select * from auth where email = #{email}
    </select>
    <select id="getList" resultType="com.ezen.spring.domain.UserVO">
        select * from user
    </select>
    <update id="updateNoPwd">
        update user set nick_name = #{nickName} where email = #{email}
    </update>
    <update id="update">
        update user set pwd=#{pwd}, nick_name = #{nickName} where email= #{email}
    </update>
    <delete id="remove">
        delete from user where email = #{email}
    </delete>
    <delete id="removeAuth">
        delete from auth where email = #{email}
    </delete>
    <update id="lastUpdate">
        update user set last_login = now() where email = #{email}
    </update>
</mapper>

 

출력