71일차

지금은 회원목록을 누구나 볼 수 있는데 나중엔 권한이 잇어야 볼 수 잇다.

18 회원 삭제

info.jsp에서 수정 혹은 삭제를 해보도록하자.

<input type="hidden" name="id" value="${member.id}" />

18.1 view

삭제버튼을 누르면 포스트방식으로 요청이 일어나도록
안보이는 폼이 눌리도록 해보자.

<button type="submit" form="removeForm">삭제</button>

<div class="d-none">
    <form id="removeForm" action="/member/remove" method="post">
        <input type="text" name="id" value="${member.id }" />
    </form>
</div>

18.2 컨트롤러

@PostMapping("remove")
public void remove(String id) {
    service.remove(id);
}

18.3 서비스

@Override
public void remove(String id) {
    int cnt = mapper.deleteById(id);
}

18.4 mapper

@Delete("""
        DELETE FROM Member WHERE id = #{id}
        """)
int deleteById(String id);

18.5 암호확인

삭제시 바로 삭제되지 않고 암호확인을 받고 삭제를 하도록 하자.
모달을 활용하면된다.
modal-body가 form이되면된다.

<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirmModal">탈퇴</button>

<!-- Modal -->
<div class="modal fade" id="confirmModal" 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="exampleModalLabel">삭제하시겠습니까?</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <form id="removeForm" action="/member/remove" method="post">
                    <input type="hidden" name="id" value="${member.id }" />
                    <label for="passwordInput">암호</label>
                    <input id="passwordInput" class="form-control" type="password" name="password" placeholder="비밀번호를 입력해주세요" />
                </form>
            </div>
            <div class="modal-footer">
                <button form="removeForm" type="submit" class="btn btn-danger">확인</button>
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
            </div>
        </div>
    </div>
</div>

18.6 서비스

서비스에서는 db의 비밀번호와 입력된 비밀번호를 비교한다.

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

18.7 컨트롤러

컨트롤러에서는 비밀번호를 받고 db의 값과 비교를 해야한다.
memberBean으로 두 값을 한번에 받자.
잘되엇으면 홈화면으로 redirect 안되면 회원상세페이지로 돌아가게 해주자.

@PostMapping("remove")
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();
    }
}

19 회원 수정

수정버튼 누를시 수정화면으로 가자.

<a class="btn btn-secondary" href="/member/update?id=${member.id}">수정</a>

19.1 view

<div class="container-lg">
    <div class="row justify-content-center">
        <div class="col-12 col-md-10 col-lg-8">
            <h1>회원수정</h1>
            <form action="/member/update" method="post">
                <div class="mb-3">
                    <label for="idInput" class="form-label">아이디</label>
                    <input type="text" id="idInput" class="form-control" name="id" value="${member.id}" readonly/>
                </div>
                <div class="mb-3">
                    <label for="pwdInput" class="form-label">비밀번호</label>
                    <input type="text" id="pwdInput" class="form-control" name="password" value="${member.password}"/>
                </div>
                <div class="mb-3">
                    <label for="nickNameInput" class="form-label">별명</label>
                    <input type="text" id="nickNameInput" class="form-control" name="nickName" value="${member.nickName}" />
                </div>
                <div class="mb-3">
                    <label for="emailInput" class="form-label">이메일</label>
                    <input type="email" id="emailInput" class="form-control" name="email" value="${member.email}"/>
                </div>
                <div class="mb-3">
                    <input type="submit" class="btn btn-primary" value="수정" />
                </div>
            </form>
        </div>
    </div>
</div>

19.2 컨트롤러

view로 이동시키는 GetMapping과 PostMapping을 만들어줘야한다.
GetMapping은 그 값 자체를 보여주고 수정하는 방식으로 가야하기 때문에
model에 member를 심어준다.

@GetMapping("update")
public void updateForm(String id, Model model) {
    Member member = service.getInfo(id);
    model.addAttribute("member", member);
}

@PostMapping("update")
public String updatePorc(Member member, RedirectAttributes rttr) {
    boolean ok = service.update(member);

    if (ok) {
        rttr.addFlashAttribute("message", "수정되었습니다");
        return "redirect:/member/info?id=" + member.getId();
    } else {
        rttr.addFlashAttribute("message", "수정실패하였습니다.");
        return "redirect:/member/info?id=" + member.getId();
    }
}

19.3 서비스

@Override
public boolean update(Member member) {
    int cnt = 0;
    cnt = mapper.update(member);
    return cnt == 1;
}

19.4 mapper

@Update("""
        UPDATE Member SET 
        password = #{password}, 
        nickName = #{nickName}, 
        email = #{email}
        WHERE id = #{id}
        """)
int update(Member member);

여기서 문제점 action에 지정하지 않앗더니
get요청과post폼데이터가 같이 넘어가게 되어 경로?=쿼리스트링=값,값이 되는 버그가 발생해버렷다.
action에 post요청인 update가 이뤄지도록 정확하게 설정을 해줘야했다.
애초부터 링크가 /member/update/id?=값 이엇기때문에 액션태그가 없으면 같은 링크로 요청하게 된다.
이 링크로 post요청을 하엿으니 파라미터 id,멤버의id로 두개가 되는 것이다.

19.5 view - 2

패스워드가 일치할때만 수정하도록 해주기
form 바깥에 submit과 input박스가 잇으니 다 같은 form으로 가도록 원래 form에 id를 붙이고
각 input과 submit에 form속성을 넣어주엇다.

<!-- 수정 확인 Modal -->
<div class="modal fade" id="confirmModal" 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="exampleModalLabel">수정 확인</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <label for="inputOldPassword" class="form-label">이전 암호</label>
                <input form="modifyForm" id="inputOldPassword" class="form-control" type="text" name="oldPassword" />
            </div>
            <div class="modal-footer">
                <button type="submit" form="modifyForm" class="btn btn-primary">확인</button>
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
            </div>
        </div>
    </div>
</div>

19.6 service

컨트롤러는 form에서 번호를 가져다 사용하면된다.
서비스에서는 db의 비밀번호와 입력받은 비밀번호를 비교해준다.

@Override
public boolean modify(Member member, String oldPassword) {
    int cnt = 0;
    Member dbMember = mapper.selectById(member.getId());
    if (oldPassword.equals(dbMember.getPassword())) {
        // 암호가 같으면?
        cnt = mapper.update(member);
    }
    return cnt == 1;
}

20. 스프링 시큐리티

회원정보로 로그인 글쓰기 권한잇는사람만 목록 보기 등등
그냥 추가하면 session으로 한다.
하지만 스프링을 사용하니 스프링 시큐리티라는 기능을 사용할 수 있다.

spring security라이브러리를 추가해줘야한다.
이제 새 프로젝트를 한다면 롬복, 스프링웹, 데브툴, 마리아db, 시큐리티, 마이바티스를 넣어주자.

시큐리티를 추가하면 관련 filter들을 자동으로 세팅해준다.
모든 경로에 인증을 받아야만 접근 할 수 있게 만들어준다.

공식문서 보기
https://docs.spring.io/spring-security/reference/index.html

서블릿기반을 사용하고 있기때문에 Servlet Applications을 보면된다.
서블릿이 요청을 받고 그 요청을 적절한 컨트롤러에게 요청을 보낸다.
우리는 컨트롤러만 만들엇다. 서블릿이 컨트롤러에게 요청을 보냈기때문이다.
그럼 이 서블릿이 존재하는가? 존재한다.

Initializing Spring DispatcherServlet 'dispatcherServlet'

스프링이 DispatcherServlet이라는 서블릿을 만들어놧는데 얘가 모든 요청을 받고
우리가 만든 컨트롤러에 requestMapping을 보고 컨트롤러 메소드에게 일을 시킨다.

그중 하나가 delegationgFilterProxy이고
그 안에 fitlerchaiporxy가 있고
securityfilterchain을 건드려서 필터링을 해준다.

securityfilterchain도 기본설정을 해두어서 모든 요청을 걸러서
로그인상태가 아니라면 로그인화면으로 redirect하도록 포함되어 있다.
이게 기본 설정이다.

중간에 genderate된 암호가 나온다. 기본 암호이다.
username에 user
password에 이 암호를 넣는다.

로그인은 잘되엇는데 패스워드를 이것을 사용할 것이 아니다.
회원정보를 받아서 아이디와 패스워드를 저장해두었으니
이것을 사용해야한다.

그러기 위해서는 인증과 권한을 알아야한다. 메뉴얼에서 다음과같다.
Authentication 인증
Authorization 권한

인증은 로그인 권한은 로그인한 이후의 특정 경로의 자원의 접근할 권한이 있냐없냐
인증은 회사건물에 들어올수 잇느냐 없느냐 권한은 들어왓는데 몇층에 갈 수 잇냐 아니냐

21. Authentication 인증

건물에 들어올 수 있냐 없느냐가 어떻게 할 수 잇느냐가 세분화되어잇다.
우리가 사용할 것은 Username/Password을 받아서 검증하는 것이다.
이것에도 방식이 여러가지가 있는데
Form / Basic / Digest가 있는데 우리는 일단 폼형식을 사용할 것이다.

Location/login경로로 가고 안되면 rediret
로그인 폼이 보이게 된다.

만약 username과 password를 submit해서 로그인에 성공하게 되면
UsernamePasswordAuthenticationFilter가 Authentication 객체로 UsernamePasswordAuthenticationToken로 만들어서 Authentication manager로 담아두게 된다.
UsernamePasswordAuthentication가 submitToken한 username과 패스워드를 가지고 있다.
어떻게 되잇느냐에 따라서 인증을 해준다.

이 Authentication manager도 많은데 이녀석이 호출하는 DaoAuthenticationProvider을 사용할 것이다.

DaoAuthenticationProvider가 userDetailsSercive와 passwordEncoder를 가지고 일을 한다.

userDetailsSercive는 DaoAuthenticationProvider에의해서 userName과 password를 얻는데 사용이 된다.
userDetailsSercive로 실제 id와 password를 얻어낸다.
이걸 다시 Authentication manager에게 건네주고 폼으로부터 받은것 실제 정보를 두가지 가지게 된다.
이것을 비교하는 것이다. 맞으면 인증이 되는 것이고 틑리면 실패한 처리가 된다.

즉 Authentication manager가 폼으로부터 받은것과 userDetailsSercive의 db정보를 가지고 비교하게 되는 것이다.
즉 userDetailsSercive을 만들어야한다.

최종적으로 securityfilterChain 설정, userDetailsSercive 작성 두가지를 해야한다.

config패키지에서 설정을 해줘야한다.
-> 처음 공부할때 서블릿마다 설정을 나누엇듯이 시큐리티 용 config인듯 하다.

만들어둔 filter들을 모아놓은게 SecurityFilterChain이다.
securityFilterChain Bean을 만들어야하는데 http객체를 이용해서 만들수 있다.
필터를 직접 다 할필요가 없다.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.build();
}

http://localhost:8082/login
http://localhost:8082/logout
이 처리 되지 않는다.

만들지 않앗으면 기본설정이 되는데 우리가 만들엇으니 필터체인을 그대로 사용하는 것이다.
여기에 로그인 필터 로그아웃필터 등등을 추가해주면된다.
이게 기본설정을 덮어씌운 것이다.

22. SecurityFilterChain

HttpSecurity의 문서를 보면서 사용해보자.
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/HttpSecurity.html

직접필터를 넣고 싶다. addFilter등 이있는데 직접할일이 거의없다.
로그인설정 formlogin() 로그아웃 logout()
oauth2Client()설정이 하고싶다 그런데 우리는 폼만할것임.

SecurityFilterChain만들자마자 기본설정이 바뀌었다.
http.formLogin();하면 로그인 필터가 설정이된다.

http.authorizeHttpRequests().requestMatchers("/abc").authenticated();
하면 abc경로에는 인증된 사용자만 로그인을 해야만 갈 수 있다.
로그인을 하기전에는 필터가 걸리고 로그인에 성공하면 첫요청을 넣었던곳으로 갈 수 있게 해준다.

처음부터 로그인으로 가면 루트로 보내진다.
이게 싫다면 formLogin()메소드의 defaultSuccessUrl();메소드를 설정해주면된다.

http.formLogin().defaultSuccessUrl("/list");

외우지말고 api보면서 해라,,, 여러 설정이 있다는 느낌만 알아라?

defaultSuccessUrl에 추가 파라미터로 true를 주면 abc로 요청하고 login하면 abc가 아니라 설정한 루트로 요청하게 된다.

http.formLogin().defaultSuccessUrl("/list", true);

스프링이 만든 기본 페이지가 아니라 내가 만든 로그인페이지로 가고싶다면 loginPage()메소드를 설정해주면된다.

http.formLogin()
    .loginPage("/mylogin")
    .defaultSuccessUrl("/list", true);

컨트롤러에서 mylogin에서 일을 하겠다는 것을 매핑해줘야한다.

@GetMapping("mylogin")
public void loginForm() {
}

<h1>내가 만든 로그인 폼</h1>
<form method="post">
    아이디 <input type="text" name=""/> <br />
    암호 <input type="password" name=""/> <br />
    <input type="submit" value="로그인" />
</form>

그냥 두면 무한루프요청이되서 뜨지 않는다. 다음을 붙이면

http.authorizeHttpRequests().anyRequest().permitAll();

.authenticated()한 사이트가 아니라면 그냥 들어가지는데 이게 설정되어잇으면 인증을 해야들어가진다.

csrf토큰을 보내줘야 로그인 처리르를 할 수 있다.

csrf 사이트간 요청위조
선량한사용자가 해커가 보낸 이메일을 보는데 버튼을 누르면
login이란 사이트에 요청을 보내면 로그인 한 상태니까 공통 목적사이트의 키를 가지고 있는 사람이다.
특정버튼을 누르면 로그인한 상태의 요청이 잘가고 기능이 잘된다.
클라이언트만 알수잇는 증표가 필요한데 그것이 csrf토큰이다.
그래서 클라이언트에게 공격을 유도하는 버튼을 만들 수 없다.

form 에 요청할때 넣어줘야한다.
csrf태그를 사용하면된다. 이걸 사용하려면 taglib을 추가해줘야한다.
dependency를 추가해주고 사용해야한다.

<!-- security tag library -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
</dependency>

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

sec:csrfInput/을 추가하면 hidden으로 csrf가 넘어가는것을 볼 수 있다.
같이 id값과 password값을 전달하려면 각각 usernameParameter() passwordParameter()메소드로 설정해주고
폼에서의 name값을 같게 해주면된다.
이과정을 통해서 로그인 처리를 할 수 있다.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.formLogin()
            .loginPage("/sub33/mylogin")
            .usernameParameter("id")
            .passwordParameter("pw")
            .defaultSuccessUrl("/list", true);
    http.logout();

    http.authorizeHttpRequests().requestMatchers("/abc").authenticated();
    http.authorizeHttpRequests().anyRequest().permitAll();

    return http.build();
}

<h1>내가 만든 로그인 폼</h1>
<form method="post">
    아이디 <input type="text" name="id"/> <br />
    암호 <input type="password" name="pw"/> <br />
    <input type="submit" value="로그인" />
    <sec:csrfInput/>
</form>

우리가 보낸 csrf토큰 을 disable()하면 sec:csrfInput/을 안써도된다
이런 여러가지 설정으로

http.csrf().disable();

아까는 logout이 안됫는데 csrf가 inalbe를 햇엇느네 post요청을 햇어야햇다.
post방식 로그아웃을 햇어야햇엇는데 포스트방식은 csrf토큰을 가지고 갓어야햇기때문이다.
disable을 하면 사용하지 않으니 그래서 그냥 로그아웃된다.

HttpSecurity의 필터링을 활용해서 처리한다.

23. UserDetailsService

여러 설정을 하고 나면 pw와id가 로그인폼을 사용해서 SecurityFilterChain으로 보내진다.
이제 얘를 UsernamePasswordAuthenticationToken에담아서
Authentication manager에 보내게 된다.

Authentication manager는 dao~를 거쳐서 UserDetailsService를 통해서 실제가진 아이디와 비밀번호를 얻어낸다.
비교해서 맞냐틀리냐를 성공 실패하게 된다.

이전에 PasswordEncoder를 설정해줘야한다.
PasswordEncoder는 기본 password가 유출되면 안되니 암호화해주는 것이다
BCryptPasswordEncoder객체를 활용해서 암호화할 것이다.

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

이걸활용해서 사용자 정보를 UserDetailsService에서 사용자정보를 설정할 수 있다.
user1, user2사용자에게 pw1, pw2를 각각 부여한것이다.
authorities(List.of())로 인증과 권한에 대해서 설정도 해줘야한다.

바로 String으로 비밀번호를 넣지않고 변환해서 넣어야한다.
PasswordEncoder encoder = passwordEncoder();를 활용해서 변환해서 넣을 수 있다.
PasswordEncoder의 encode메소드를 사용하면된다.

출력해보면 pw1이라는 값이 어려운 문자로 변환되어 들어있다.
이상태로db에 저장되어잇어야한다.

입력을 pw1로 하더라도 변환된것이 db에저장이 되어 잇어야한다.

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

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

    UserDetails user1 = User.builder()
            .username("user1")
            .password(pw1)
            .authorities(List.of())
            .build();

    UserDetails user2 = User.builder()
            .username("user2")
            .password(pw2)
            .authorities(List.of())
            .build();

    return new InMemoryUserDetailsManager(user1, user2);
}

Authentication manager는 UserDetailsService에서 받은 username과 password를 비교해서 성공실패 처리를 해주게 된다.
가짜 id와 가짜 password를 이용해서 로그인한 것이다.
입력한 것을 확인해서 잘 적용햇는지를 보여준 것이다.

24 프로젝트 적용

배운내용을 프로젝트에 적용해보자.
선행작업이 필요하다. 패스워드를 저장할때 평문으로 저장햇엇는데
패스워드 인코더로 저장할 것이다.

앞으로 저장되는 것들을 암호화해서 저장해보자.

MemberService에서 signUp메소드에서 설정을 해야한다.

PasswordEncoder는 스프링 시큐리티가 제공하니 제공받은것으로 사용하면된다.

@Service
@Transactional(rollbackFor = Exception.class)
public class MemberServiceImpl implements MemberService {

    @Autowired
    MemberMapper mapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public boolean signUp(Member member) {
        //암호암호화
        String plain = member.getPassword();
        member.setPassword(passwordEncoder.encode(plain));

        int cnt = mapper.createMember(member);

        return cnt == 1;
    }
    //...
}

이러면 저장할때 새로운 회원은 암호화되어서 저장하게 된다.

그러면 회원정보를 crud할때 이 긴 패스워드를 입력해야하나
아니다 평문으로 입력해도 알아서 비교하도록 설정을 해줘야한다.
dbMember.getPassword().equals(member.getPassword()
현재 암호화된 번호와 평문을 비교하고 있다.
passwordEncoder의 matches메소드를 사용해야한다.

passwordEncoder.matches(평문, 암호화된것)

수정 부분에서 값을 가져오고 있는데 암호환된것이 input박스에 들어가잇으니
수정버튼을 누르면 암호화된게 또 암호회되어서 보내지게 된다.
그래서 처음부터 패스워드를 보여주지 말자.
입력하면 기존의 패스워드가 넘어가고 아니면 기존 패스워드가 넘어가게 해주자.

@Override
public boolean modify(Member member, String inputPassword) {
    int cnt = 0;

    //패스워드를 바꾸기 입력했다면
    if (!member.getPassword().isBlank()) {
        //입력된 패스워드를 암호화
        String plain = member.getPassword();
        member.setPassword(passwordEncoder.encode(plain));
    }

    Member dbMember = mapper.selectById(member.getId());
    if (passwordEncoder.matches(inputPassword, dbMember.getPassword())) {
        // 암호가 같으면?
        cnt = mapper.update(member);
    }
    return cnt == 1;
}

password가 빈스트링이거나 null이아니라면 변경 아니면 변경안하게 해준다.

@Update("""
        <script>
        UPDATE Member SET
        <if test="password neq null and password neq ''">
        password = #{password},
        </if>
        nickName = #{nickName},
        email = #{email}
        WHERE id = #{id}
        </script>
        """)
int update(Member member);

2023.05.08

여기서 문제점 action에 지정하지 않앗더니
get요청과post폼데이터가 같이 넘어가게 되어 경로?=쿼리스트링=값,값이 되는 버그가 발생해버렷다.
action에 post요청인 update가 이뤄지도록 정확하게 설정을 해줘야했다.

스프링 시큐리티 진짜 처음 배우는것이다.
세션과의 차이점이 무엇인지. 많은 사람을 어떻게 처리해주는지
진짜 진짜 모르는게 튀어나오기 시작한다.
그전까지는 어떻게든 해볼만햇는데 매일 매일 정진 하는 수 밖에 없다.
한걸음씩 나아가자 먼저 두려워하지 말자.

'국비 > Project - 1 게시판' 카테고리의 다른 글

2023.05.10 73일차 Project  (0) 2023.05.11
2023.05.09 72일차 Project  (0) 2023.05.09
2023.05.04 70일차 Project  (0) 2023.05.04
2023.05.03 69일차 Project  (0) 2023.05.03
2023.05.02 68일차 Project  (0) 2023.05.02

+ Recent posts