국비/Project - 1 게시판

2023.05.10 73일차 Project

춘핑이 2023. 5. 11. 12:01

73일차

인증하는 방법을 배우고 있다. 토큰을 주고 토큰을 받아서 처리한다.
스프링 시큐리티 라이브러리를 활용하고 잇다.
가입한 회원정보로 로그인하고 글 수정 삭제 등등
자기 글이 아니면 CRUD를 제한하고 있다.

37. FOREIGN KEY

가입한 회원이 로그인해야 쓸수잇다.
그런데 글쓸때 회원이 가입전에 쓴 글이 잇엇다.
글쓴이를 멤버 테이블의 멤버 중 하나로 변경해주고 외래키 설정을 해주자.

외래키 설정

ALTER TABLE Board
ADD FOREIGN KEY (writer) REFERENCES Member(id);

37.1 글삭제

멤버 탈퇴를 하려고 하는데 본래 쓴글이 회원의 id를 참조하고 잇으니
탈퇴하려하면 문제가 발생하게 된다.
외래키 제약사항을 위배하기 때문이다. 그래서 먼저 탈퇴하기 전에 게시글을 지워줄 필요가 있다.

서비스가 다른 서비스를 호출해도되고 mapper의 메소드를 호출해도된다.

Board서비스를 호출해보자.

@Override
public boolean remove(Member member) {
    Member dbMember = mapper.selectById(member.getId());
    int cnt = 0;
    //dbMember.getPassword().equals(member.getPassword()
    if (passwordEncoder.matches(member.getPassword(), dbMember.getPassword())) {
        // 암호가 같으면?

        //이 회원이 작성한 게시물 row 삭제
        boardService.removeByWriter(member.getId());

        //회원 테이블 삭제
        cnt = mapper.deleteById(member.getId());
    }
    return cnt == 1;
}

37.2 BoardSerivce

입력받은 writer의 글의 id를 모두 조회해서 리스트를만든다.
각 글의 id마다 돌면서 BoardSerivce의 remove메소드를 실행시킨다.
첨부파일 삭제 & 글삭제 메소드이다.

@Override
public void removeByWriter(String writer) {
    // 파일명 조회
    List<Integer> idList = mapper.selectIdByWriter(writer);

    for (Integer id : idList) {
        remove(id);
    }
}

37.3 BoardMapper

@Select("""
        SELECT id
        FROM Board
        WHERE writer = #{writer}
        """)
List<Integer> selectIdByWriter(String writer);

38. 제한

남의 아이디를 삭제하면 안된다. 제한을 줘야한다.
로그인하고 같은 아이디 일때만 삭제하게 제한을 주자.

@PostMapping("remove")
@PreAuthorize("isAuthenticated() and (authentication.name eq #member.id)")
public String remove(Member member, RedirectAttributes rttr) {
    boolean ok = service.remove(member);
    if (ok) {
        rttr.addFlashAttribute("message", "회원탈퇴하였습니다.");
        return "redirect:/list";
    } else {
        rttr.addFlashAttribute("message", "회원탈퇴시 문제가 발생했습니다..");
        return "redirect:/member/info?id=" + member.getId();
    }
}

39. 탈퇴 후 로그아웃

탈퇴를 해도 로그인 상태로 남아있다.
로그아웃 상태로 만들어주어야한다.

여러 방법이 있는데 코드로 로그아웃을 하려면 request가필요하다.
HttpServletRequest request의 logout()메소드를 실행시키면된다.

@PostMapping("remove")
@PreAuthorize("isAuthenticated() and (authentication.name eq #member.id)")
public String remove(Member member, RedirectAttributes rttr, HttpServletRequest request) throws ServletException {
    boolean ok = service.remove(member);
    if (ok) {
        rttr.addFlashAttribute("message", "회원탈퇴하였습니다.");
        //로그아웃
        request.logout();
        return "redirect:/list";
    } else {
        rttr.addFlashAttribute("message", "회원탈퇴시 문제가 발생했습니다..");
        return "redirect:/member/info?id=" + member.getId();
    }
}

40. 권한

특정권한을 줘서 다른사람보다 많은 일을 할 수 있도록 해주자.
UserDetailsService로 설정을 할 수 있다.
authorities(컬렉션)를 설정해주면된다.
이컬렉션의 elemet타입은 GrantedAuthority또는 그 하위타입이어야한다.
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/GrantedAuthority.html

인터페이스 이기때문에 구현 클래스를 봐야한다.
XXXAuthority로 다양하게 구현 클래스가 있는데 우리는 SimpleGrantedAuthority을 사용할 것이다.
파라미터를 스트링으로 받는 생성자가 있다.

new SimpleGrantedAuthority("")객체를 만들어서 넣어줘야한다.

@Bean
public UserDetailsService userDetailsService() {
    PasswordEncoder encoder = passwordEncoder();

    String pw1 = encoder.encode("pw1");
    String pw2 = encoder.encode("pw2");

    System.out.println("pw1:" + pw1);
    System.out.println("pw2:" + pw2);

    UserDetails user1 = User.builder()
            .username("user1")
            .password(pw1)
            .authorities(List.of(new SimpleGrantedAuthority("admin"),
                    new SimpleGrantedAuthority("manager")))
            .build();

    UserDetails user2 = User.builder()
            .username("user2")
            .password(pw2)
            .authorities(List.of(new SimpleGrantedAuthority("manager"),
                    new SimpleGrantedAuthority("user")))
            .build();

    return new InMemoryUserDetailsManager(user1, user2);
}

이 권한에 따라서 어떤일을 하거나 못하거나 설정을 해줘야한다.

<sec:authorize access="hasAuthority('admin')"> 

hasAuthority설정으로 넣어주면된다.
여러 권한을 넣고 싶으면 and hasAuthority()하거나
hasAnyAuthority(String…​ authorities)을 넣으면된다.

<sec:authorize access="hasAuthority('admin')"> 
    <div>
        admin이 볼수 있는 컨텐츠
    </div>
</sec:authorize>

<sec:authorize access="hasAuthority('manager')"> 
    <div>
        manager가 볼 수 있는 컨텐츠
    </div>
</sec:authorize>

<sec:authorize access="hasAuthority('user')"> 
    <div>
        user가 볼 수 있는 컨텐츠
    </div>
</sec:authorize>

<sec:authorize access="isAuthenticated()"> 
    <div>
        로그인하면 볼 수 있는 컨텐츠
    </div>
</sec:authorize>

40.1 권한 테이블

회원에게 어떤 회원이 어떤 권한이 있는지 저장을 해야한다.
Member테이블에 authority를 추가할 수 있는데
여러 권한을 가지고 있다면 원자적이지 않게 된다.
그래서 여러 권한이 있을 수 있으니 아예 다른 테이블을 만들어주는게 더 원자적이게 보인다.

권한 테이블

CREATE TABLE MemberAuthority (
    memberId VARCHAR(20) NOT NULL,
    authority VARCHAR(30) NOT NULL,
    FOREIGN KEY (memberId) REFERENCES Member(id),
    PRIMARY KEY (memberId, authority)
);

멤버를 조회할때 권한도 조회하도록 join문을 사용해주자.
멤버 DTO는 여러 권한을 가질 수 있으니 list로 받아주자.
그래서 여러 result가 들어갈 수 있게 reusltmap을 해줘야한다.

@Data
public class Member {
    private String id;
    private String password;
    private String email;
    private String nickName;
    private LocalDateTime inserted;
    private List<String> authority;
}

<mapper namespace="com.example.demo.mapper.MemberMapper">
    <resultMap type="com.example.demo.domain.Member" id="memberMap">
        <id column="id" property="id"/>
        <result column="password" property="password"/>
        <result column="email" property="email"/>
        <result column="nickName" property="nickName"/>
        <result column="inserted" property="inserted"/>
        <collection property="authority" ofType="string">
            <result column="authority"/>
        </collection>
    </resultMap>
</mapper>

@Select("""
        SELECT m.*, ma.authority FROM Member m 
        LEFT JOIN MemberAuthority ma
        ON m.id = ma.memberid
        WHERE id = #{id}
        """)
@ResultMap("memberMap")
Member selectById(String id);

40.2 권한부여

회원에게 권한을 부여해야한다.
멤버정보를 잘 얻어왓다면 권한을 부여하자.

CustomUserDetailsService에서 UserDetails객체를 수정해야한다.

authorities(member.getAuthority().stream().map(a -> new SimpleGrantedAuthority(a)).toList())
authorities(member.getAuthority().stream().map(SimpleGrantedAuthority::new).toList())
단순히 흐르는게 생성자의 파라미터로 들어가는 것이기 때문에 생성자 참조로 변경한것이다.

member의 Authority리스트에서 스트림을 얻어서 각각 String을 SimpleGrantedAuthority의 생성자를 사용넣어주기
mapToInt 같은거할때 map이다. 컬렉션 map이아니라 오랜만에 봐서 헷갈렷다.

List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
for (String auth : member.getAuthority()) {
    authorityList.add(new SimpleGrantedAuthority(auth));
}
authorities(authorityList)

한 코드와 같다.

40.3 권한 설정

이제 회원목록은 admin만 볼 수 있게 설정을 해주자.

<sec:authorize access="hasAuthority('admin')">
    <li class="nav-item">
        <a class="nav-link ${current eq 'memberList' ? 'active' : ''}" href="/member/list">회원목록</a>
    </li>
</sec:authorize>

view에서만 설정하면 링크를 직접 쳐서 접속할 수 있으니 컨트롤러에서도 막아줘야한다.

@GetMapping("list")
@PreAuthorize("hasAuthority('admin')")
public void memberList(Model model) {
    List<Member> list = service.listMember();
    model.addAttribute("memberList", list);
}

관리자라면 남의 정보도 볼 수 있어야한다.
자신의 정보 보기도 같은 경로를 사용하고 있기 때문에 권한은 and가 아닌 or 연산자로 연결해줘야한다.

@GetMapping("info")
@PreAuthorize("isAuthenticated() and (authentication.name eq #id) or hasAuthority('admin')")
public void memberList(String id, Model model) {
    Member member = service.getInfo(id);
    model.addAttribute("member", member);
}

하지만 수정은 본인만 할 수 있도록 버튼은 막아주자.

<sec:authorize access="authentication.name eq #member.id">
    <div>
        <a class="btn btn-secondary" href="/member/update?id=${member.id}">수정</a>
        <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#confirmModal">탈퇴</button>
    </div>
</sec:authorize>

41. 배포

또 다시 만든 어플리이션을 배포해보자

run as - maven install - target - war파일

scp -i 키파일 프로젝트.war bitnami@서버주소:.

ssh -i 키파일 bitnami@서버주소

ps aux 로 원래거 번호보고 끄기 -> kill 번호
ps aux | grep "java" 결과중 행에 java가 포함된 것만 띄우기

java -jar 프로젝트.war > log.txt &
log.txt에 log남기면서 프로젝트 실행

42. 자바스크립트 적용

프로젝트에 자바 스크립트를 적용한적이 없는데 프로젝트에 적용해보도록 하자.
회원가입시 id입력 패스워드입력 별명 이메일
패스워드, 패스워드 확인
두개다 맞을때만 가입 되게 하기

구현할 기능은 다음과 같다.
패스워드에 입력한 값
패스워드 확인에 입력한 값이 같으면
submit버튼활성화
패스워드가 같다는 메시지 출력
그렇지 않으면 비활성화
패스워드가 다르다는 메세지 출력

비밀번호 확인은 파라미터를 굳이 넘길 필요없으니 name attribute를 삭제해준다.
bootstrap의 기능을 활용해서 class disabled를 넣으면 버튼이 비활성화된다.

<div class="mb-3">
    <label for="pwdInput" class="form-label">비밀번호</label>
    <input type="password" id="pwdInput" class="form-control" name="password" />
</div>
<div class="mb-3">
    <label for="pwdInputCheck" class="form-label">비밀번호확인</label>
    <input type="password" id="pwdInputCheck" class="form-control"/>
</div>

<div class="mb-3">
    <input type="submit" class="btn btn-primary disabled" value="가입" />
</div>

입력값을 언제 비교해야하느냐? 키 입력마다 비교해줘야한다.
키업 이벤트 발생시에 발생하도록해주자.

<div id="pwdSuccess" class="form-text text-primary d-none">
    <i class="fa-solid fa-check"></i>
    비밀번호가 일치합니다.
</div>
<div id="pwdFail" class="form-text text-primary d-none">
    <i class="fa-solid fa-triangle-exclamation"></i>
    비밀번호가 일치하지 않습니다.
</div>

<script>
    //패스워드, 패스워드 체크 인풋에 키업 이벤트 발생시
    $("#pwdInput, #pwdInputCheck").keyup(function() {
        // 패스워드에 입력한 값
        const pw1 = $("#pwdInput").val();
        // 패스워드 확인에 입력한 값
        const pw2 = $("#pwdInputCheck").val();
        // 같으면
        if (pw1 === pw2 && pw1 != '') {
            // submit버튼활성화 
            // 패스워드가 같다는 메시지 출력
            $("#signupSubmit").removeClass("disabled");
            $("#pwdSuccess").removeClass("d-none");
            $("#pwdFail").addClass("d-none");
        } else {
            // 그렇지 않으면 비활성화
            $("#signupSubmit").addClass("disabled");
            // 패스워드가 다르다는 메세지 출력
            $("#pwdSuccess").addClass("d-none");
            $("#pwdFail").removeClass("d-none");
        }
    })
</script>

실제 어플리케이션에서는 컨트롤러에서도 확인하는 코드를 넣어줘야한다.

42.2 회원정보수정

회원정보 수정에서도 비밀번호 확인란을 넣어줘서 일치할때만 수정하도록 해주자.
비워둬도 괜찮기 때문에 처음부터 비활성화 할필요는 없다.

<div class="mb-3">
    <label for="pwdInput" class="form-label">비밀번호</label>
    <input type="text" id="pwdInput" class="form-control" name="password" value="" />
    <div class="form-text text-secondary">
        입력하지 않으면 기존 패스워드를 유지합니다.                            
    </div>
</div>
<div class="mb-3">
    <label for="pwdInputCheck" class="form-label">비밀번호확인</label>
    <input type="text" id="pwdInputCheck" class="form-control" value="" />

    <div id="pwdSuccess" class="form-text text-primary d-none">
        <i class="fa-solid fa-check"></i>
        비밀번호가 일치합니다.
    </div>
    <div id="pwdFail" class="form-text text-danger d-none">
        <i class="fa-solid fa-triangle-exclamation"></i>
        비밀번호가 일치하지 않습니다.
    </div>
</div>

<script>
    $("#pwdInput, #pwdInputCheck").keyup(function(){
        const pw1 = $("#pwdInput").val();
        const pw2 = $("#pwdInputCheck").val();

        if (pw1 === pw2 && pw1 != ''){
            $("#updateButton").removeClass("disabled");
            $("#pwdSuccess").removeClass("d-none");
            $("#pwdFail").addClass("d-none");
        } else{
            $("#updateButton").addClass("disabled");
            $("#pwdSuccess").addClass("d-none");
            $("#pwdFail").removeClass("d-none");
        }
    })
</script>

42.3 js코드 파일 분리

js코드를 jsp파일안에 넣어도 괜찮지만 길어지면 지저분해지기때문에 뺄 수 있다.

<script src="/js/member/signup.js"></script>

해당경로로 옮길 것이다.
이 폴더를 어디에 만들것인가 ? webapp 이나 static에 만들어주면된다.

분리하면서 id nickName 비어잇는지 확인하여 버튼 활성화되도록했다.

//패스워드, 패스워드 체크 인풋에 키업 이벤트 발생시
$("#pwdInput, #pwdInputCheck, #idInput, #nickNameInput").keyup(function() {
    // 패스워드에 입력한 값
    const pw1 = $("#pwdInput").val();
    // 패스워드 확인에 입력한 값
    const pw2 = $("#pwdInputCheck").val();
    const id = $("#idInput").val();
    const nickName = $("#nickNameInput").val();

    // 같으면
    if (pw1 === pw2 && pw1 != '') {
        // submit버튼활성화 
        // 패스워드가 같다는 메시지 출력 
        $("#pwdSuccess").removeClass("d-none");
        $("#pwdFail").addClass("d-none");
        //id nickName 비어잇는지 확인
        if (id != '' && nickName != '') {
            $("#signupSubmit").removeClass("disabled");
        } else {
            $("#signupSubmit").addClass("disabled");
        }
    } else {
        // 그렇지 않으면 비활성화
        // 패스워드가 다르다는 메세지 출력
        $("#signupSubmit").addClass("disabled");
        $("#pwdSuccess").addClass("d-none");
        $("#pwdFail").removeClass("d-none");
    }
})

43. AJAX

지금까지 작성한 어플리케이션 링크 클릭하면 페이지 전체가 로딩된다.
소스 전체를 로딩한다.

그런데 어느 기능은 전체코드를 다 받아올 필요가 없는 경우가 있다.
예를들어 회원가입할때 중복확인버튼이 잇을때 클릭하면 있는지 없는지만 받으면되지
html전체를 받을 필요가 없다.

일부정보만 주고받고 싶다. 이때 사용하는 것이 AJAX이다.

페이지 전체를 로딩하지 않고 일부만 로딩한다.
그래서 서버의 데이터를 백그라운드로 보낸다.
일부내용만 받아와서 보여준다.

AJAX는 비동기 자바스크립트와 XML의 줄임말이다.
Asynchronous JavaScript And XML
동기는 클라이언트와 서버가 일을 순서대로 한다.
비동기식은 클라이언트가 서버에게 요청을 보내놓고 일을하고
서버도 일하다가 일끝나면 응답을 해준다.

JS코드로 이런일을 하기 때문에 이런이름이 붙엇다.
XML은 클라이언트와 서버가 주고받는 데이터가 XML형식이엇는데
XML을 주고받기 위한 JS기술이엇다.
시간이 지나며 XML을 안쓰게 되엇고 JSON형식으로 주고받게 되엇다.

아무튼 JSON형식으로 주고받는 JS를 작성해야한다.

43.1 AJAX - 1

JS를 통해서 요청을 보내야한다.
AJAX요청을 보내는 라이브러리가 여러가지가 있다.

초창기에는 XMLHttpRequest()객체를 많이 사용했엇다.
나중에 브라우저 기본 기능인 fetch라는 함수가 추가가 되었다.
jquery의 $.ajax()메소드도 있다.

또는 다른 라이브러리로 axios도 있다.

jquery를 사용중이니 $.ajax()메소드를 사용할것이다.
https://api.jquery.com/category/ajax/
jqyery사이트의 ajax관련 메소드 이벤트 등을 보고 사용하면된다.

jQuery.ajax() == $.ajax()로 사용하면된다.

파라미터를 url, [setting]을 받는다.
url은 url스트링 타입이다.

$.ajax("/sub34/link1");

/sub34/link1에서 일하는 메소드를 불러 온다.
그렇다면 버튼이 클릭되면 ajax가 발생하도록해보자.

<h5>ajax 연습 1</h5>
<div>
    <button id="button1">ajax요청</button>
</div>

$("#button1").click(function () {
    $.ajax("/sub34/link1");
})

개발자도구의 fetch/XHR을 누르면 AJAX요청만 볼 수있다.

method1이 응답 컨텐츠 자체가 되려면 @ResponseBody어노테이션을 붙여주면된다.
응답한 String이 전달되게 된다.

@GetMapping("link1")
@ResponseBody
public String method1() {
    System.out.println("헬로 ajax");
    return "첫번째 응답 데이터";
}

ajax로 댓글 쓰기를 만들 수 있다.

2023.05.10

권한 부여
권한을 하나하나 부여하는 것은 어려움이 잇어보인다.

AJAX의 의미를 알게 되었다.
새로운게 계속해서 등장하니 어렵다.
기능 자체를 사용하는 것은 할 수 있는데 생각해내서 사용하기가 어렵다.