72일차
회원가입 crud끝낫고 로그인할 수 있는 환경을 만들고 있다.
직접 로그인한다면 세션과 쿠키를 관리하면된다.
스프링 시큐리티가 세션과 쿠키를 관리해준다.
스프링이 제공하는 SecurityFilterChain와 UserDetailsService를 설정해야한다.
필터를 직접 설정하는 것이아닌 HttpSecurity설정으로 하는 것이다.
폼을 통해서 사용자가 로그인하면 username과 password를 매니저로 보내준다.
이 매니저는 daoauthenicationprovier에게 일을하고 이것은 실제 db의 비밀번호와 이름을 얻어낸다.
이 받은 정보를 daoauthenicationprovier가 동일한지 안한지를 판단한다.(매니저가 아님!)
성공하면 authenication manager에게 전달하고 다음의 일을 진행한다.
daoauthenicationprovier가 패스워드가 맞는지 안맞는지를 passwordencoder를 활용해서 비교한다.
이것도 종류가 많지만 BCryptPasswordEncoder를 사용해서 하는 것이다.
25 http.formLogin()
로그인폼을 만들어서 로그인을 해보자.
http.formLogin(Customizer.withDefaults());을 하면 기본 로그인폼이 보인다.
http://localhost:8081/login으로 요청을 보내면 기본 제공하는 로그인 폼이 뜬다.
뭔가를 치면 작성한 폼 username password가 authenication manager에게 전달하고
daoauthenicationprovier에게 일을 하게 하고 예가 UserDetailsService와 비교한것이다.
어제는 UserDetailsService빈을 만들엇는데 가짜 정보 유저정보를 만들어서 제공했다.
이것을 자세하게 만들 필요가 있다.
시큐리티 관련 클래스들은 시큐리티 패키지에 만든다.
UserDetailsService클래스를 직접 만들어줘야한다.
@Component를 붙여주면 자동으로 객체를 만들어준다.
그런데 얘를 만들기만 하면 UserDetailsService인지 모르니 UserDetailsService 인터페이스를 구현하는 구현클래스로 해줘야한다.
loadUserByUsername이라는 메소드가 있게 된다.
폼에 입력한 id를 파라미터로 받는다.
아이디 기준으로 db에서 password를 꺼내서 UserDetails를 만들어서 전달을 하면된다.
MemberMapper로부터 sql문을 활용해서 꺼내오기만 하면된다.
있으면 UserDetails를 만들어서 반환, 없으면 예외를 발생시키면된다.
줘야하는 것은 id, password, 권한인데 권한은 일단 빈 List로제공하자.
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
MemberMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = mapper.selectById(username);
if (member == null) {
throw new UsernameNotFoundException(username + "회원이 없습니다.");
}
UserDetails user = User.builder()
.username(member.getId())
.password(member.getPassword())
.authorities(List.of())
.build();
return user;
}
}26 login form
직접 만든 로그인폼을 사용해보자.
중요한점은 메소드는 post방식으로 input elelmet가 두개있는데 하나는 username 하나는 password로 전송되어야한다.
어제 했던방식으로 파라미터 이름을 바꾸는설정을 하면 바꿀수잇다.
loginPage("경로")메소드를 설정하면 된다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.formLogin()
.loginPage("/member/login");
return http.build();
}@GetMapping("login")
public void loginForm() {
}<form method="post">
아이디 <input type="text" name="username"/> <br />
암호 <input type="text" name="password" /> <br />
<input type="submit" value="로그인" />
</form>http://localhost:8081/login 기존 로그인폼은 작동하지 않고
http://localhost:8081/member/login 커스텀한 로그인 페이지를 요청하면
로그인 페이지가 요청되게 된다.
26.2 view
navvar에 로그인 달아주기
<div class="container-lg">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8">
<h1>로그인</h1>
<form method="post">
<div class="mb-3">
<label for="idInput" class="form-label">아이디</label>
<input type="text" id="idInput" class="form-control" name="username"/>
</div>
<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">
<input type="submit" class="btn btn-primary" value="로그인" />
</div>
</form>
</div>
</div>
</div>27. 로그인 상태 확인
로그인이 되어있는지 아닌지 상태를 확인해보자.
navbar태그에 추가해보자.
스프링 시큐리티 태그를 활용해야한다.
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<div>
<sec:authentication property="principal"/>
</div>anonymousUser로 뜨면 로그인이 안된상태 로그인하면 사용자 정보가 출력된다.
28. 로그아웃
로그아웃경로는 다음과 같이 추가할 수 있다.
http.logout()
.logoutUrl("/member/logout");http://localhost:8081/logout 기존 로그아웃 경로는 작동하지 않고
http://localhost:8081/member/logout 커스텀한 로그아웃 페이지를 요청하면 로그아웃이 되게 된다.
29. 권한부여
특정 링크에 권한이 있는 사람만 오게하려면 authenticated()메소드를 붙여주고
permitAll()메소드를 사용하면 누구든지 올수 있게 할 수 있다.
http.authorizeHttpRequests().requestMatchers("/add").authenticated();
http.authorizeHttpRequests().requestMatchers("/**").permitAll();로그인하지않은 사람 anonymous()
모두거부 denyAll()
모두허용 permitAll() 등등이 있다.
http.authorizeHttpRequests().requestMatchers("/member/signup").anonymous();30. custom
제공된 메소드를 그냥 사용할 수 있는데
메소드를 기본제공하지 않는 경우가 있을때는 직접 만들어줘야한다.
spring security expression
https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html
access="hasRole('admin') and hasIpAddress('192.168.1.0/24') 이런식으로 복잡하게 사용할 수 있다.
access()메소드를 통해서 설정해주면된다.
파라미터로는 과거에는 바로 string이 됫는데 Authorizatior Manger객체를 구현해야한다.
WebExpressionAuthorizationManager인스턴스를 만들어서 넣어주면된다.
이 객체의 생성자가 String을 받고 있다. 이 스트링에 spring security expression을 넣어주면된다.
복잡한것을 넣고 싶으면 String에 and or등등을 넣어주면된다
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.formLogin()
.loginPage("/member/login");
http.logout()
.logoutUrl("/member/logout");
http.authorizeHttpRequests()
.requestMatchers("/add")
.access(new WebExpressionAuthorizationManager("isAuthenticated()"));
http.authorizeHttpRequests().requestMatchers("/member/signup")
.access(new WebExpressionAuthorizationManager("isAnonymous()"));
http.authorizeHttpRequests().requestMatchers("/**")
.access(new WebExpressionAuthorizationManager("permitAll"));
return http.build();
}31. 컨트롤러 제한
다양한 방법으로 접근을 제한할 수 있다.
특정경로가 추가될때마다 config에 와서 하지 않고 컨트롤러에도 제한 할 수 있다.
Config에 @EnableMethodSecurity어노테이션을 작성해주면된다.
특정 메소드에 제한을 두게 할 수 있다.
컨트롤러에서는 @PreAuthorize("expression") 어노테이션을 사용하면된다.
spring security expression를 value값으로 넣어주면된다.
@GetMapping("signup")
@PreAuthorize("isAnonymous()")
public void signupForm() {
}@GetMapping("add")
@PreAuthorize("isAuthenticated()")
public String addForm() {
return "add";
}만약 혼자서 시큐리티이런거 다만들면 어노테이션에 알아서 달고
아니면 config에 작성하면되는 거같다고 생각된다.
지금은 단순하게 햇는데 내 회원정보인지 확인 등을 통해서 복잡한 식이 들어가게 된다.
32 view
권한이 없으면 못들어가는 것뿐만이 아니라 아예 화면에 안보이게 하면된다.
스프링 시큐리티의 태그라이브러리를 사용해야한다.
sec:authorize태그를 사용하면 안쪽 내용물을 보이거나 보이지 않게 할 수있다.
access attribute를 사용하면된다.
spring security expression을 여기에 넣어주면된다.
만약 access가 너무 길다면 var attribute에 담아서 el로 꺼내서 사용할 수 있다.
<div>
<sec:authorize access="isAuthenticated()" var="loggedIn">
로그인한 상태
</sec:authorize>
</div>
<div>
<sec:authorize access="isAnonymous()">
로그아웃한 상태
</sec:authorize>
</div>
<div>
<sec:authorize access="${loggedIn}">
또 로그인한 상태
</sec:authorize>
</div><ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link ${current eq 'list' ? 'active' : '' }" href="/list">목록</a>
</li>
<sec:authorize access="isAuthenticated()">
<li class="nav-item">
<a class="nav-link ${current eq 'add' ? 'active' : ''}" href="/add">글쓰기</a>
</li>
</sec:authorize>
<sec:authorize access="isAnonymous()">
<li class="nav-item">
<a class="nav-link ${current eq 'signup' ? 'active' : ''}" href="/member/signup">회원가입</a>
</li>
</sec:authorize>
<sec:authorize access="isAuthenticated()">
<li class="nav-item">
<a class="nav-link ${current eq 'memberList' ? 'active' : ''}" href="/member/list">회원목록</a>
</li>
</sec:authorize>
<li class="nav-item">
<sec:authorize access="isAnonymous()">
<a class="nav-link ${current eq 'login' ? 'active' : ''}" href="/member/login">로그인</a>
</sec:authorize>
<sec:authorize access="isAuthenticated()">
<a class="nav-link" href="/member/logout">로그아웃</a>
</sec:authorize>
</li>
</ul>백엔드와 뷰단에서 모두 처리를 해주게 되었다.
공격받지 않으려면 모달창들도 안보이게 해줘야한다.
33. view에서 정보 얻기
원래는 로그인 기능을 구현하지 않앗으니 그냥 사용햇엇다.
로그인한 사람의 id가 작성자에 들어가서 알아서 작성되도록해주자.
먼저 연습용 프로젝트에서 적용해보자.
로그인 한사람의 정보를 전달하는 스프링 시큐리티의 태그를 사용하면된다.
authorize로 인증됫냐 안됫냐를 햇다.
authentication을 사용하면된다.
daoauthenicationprovier가 UserDetailsService와 폼에 넣은 것을 비교해서
usernamePasswordAuthenticationToken을 발급한다.
이 정보를 사용하는 것이 authentication 태그이다.
결국 usernamePasswordAuthenticationToken이 SecurityContextHolder에 저장이 된다.
여러 메소드들이 있다. property이 필수 attribute인데 getxxx메소드의 get을 빼고 camelcase로 넣어주면된다.
특정 정보가 반복해서 쓰이면 반복해서 써도되는데 var attribute를 쓰면 페이지 영역에 넣어두고 EL로꺼내서 사용할 수 있다.
<div>
<sec:authentication property="credentials"/>
</div>
<div>
<sec:authentication property="principal"/>
</div>
<div>
<sec:authentication property="authorities"/>
</div>
<div>
<sec:authentication property="details"/>
</div>
<div>
<sec:authentication property="name"/>
</div>
<div>
<sec:authentication property="authenticated"/>
</div>34 백엔드에서 정보 얻기
화면에서는 위처럼 하면되고 백엔드에서 확인하는 방법을 알아보자.
usernamePasswordAuthenticationToken이 Authentication인터페이스를 구현하고 있기때문에
usernamePasswordAuthenticationToken이 Authentication라고 할 수 있다.
따라서 컨트롤러에서 Authentication를 아규먼트로 명시하면 스프링시큐리티가 보고 토큰을 알아서 넣어준다.
Authentication객체의 메소드를 활용해서 유저 정보를 얻어서 사용하면된다.
@GetMapping("viewAtuh")
public void viewAuth(Authentication authentication) {
System.out.println("로그인 정보 확인하기");
System.out.println(authentication);
//이름얻기
System.out.println(authentication.getName());
}컨트롤러에서는 Authentication객체 view에선 Authentication 태그를 사용하는 것이다.
35 프로젝트 적용
view에 직접저장하거나 컨트롤러로부터 받아서 사용할 수 있다.
<div class="mb-3">
<label for="writerInput" class="form-label">작성자</label>
<input type="text" id="writerInput" name="writer" class="form-control" value="<sec:authentication property="name"/>" readonly/>
</div>view에서 변경하면 누군가가 개발자도구로 소스코드를 수정하면 값을 바꿀 수 있다.
그래서 컨트롤러에서 작성할때 작성자 자체를 받을 필요가 없다.
작성자 div를 아예지워버리자.
create하기전에 board의 아이디를 authentication에서 이름을 얻어서 사용해서 넣어주는 것이 좋다.
@PostMapping("add")
@PreAuthorize("isAuthenticated()")
public String addProcess(
@RequestParam("files") MultipartFile[] files,
Board board,
RedirectAttributes rttr,
Authentication authentication) {
try {
board.setWriter(authentication.getName());
boolean ok = service.create(board, files);
if (ok) {
rttr.addFlashAttribute("message", "게시물이 등록되었습니다.");
// return "redirect:/list";
return "redirect:/detail/" + board.getId();
} else {
rttr.addFlashAttribute("message", "게시물 등록에 실패했습니다. 다시입력해주세요");
rttr.addFlashAttribute("board", board);
}
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/add";
}36. update 수정
자기가 쓴글만 수정하거나 삭제하도록 하고 싶다.
화면에서 수정버튼 자체를 없애주고 컨트롤러에서도 막아주자.
연습폴더에서 먼저 연습을 해보자.
로그인한 사용자정보는 authentication의 name에서 얻으면되는데
글의 작성자는 외부 메소드를 통해서 얻어와야한다.
외부 작성자를 사용하려면 @beanName.methodname()을 WebExpressionAuthorizationManager의 생성자 파라미터로 넣어주면된다.
http.authorizeHttpRequests()
.requestMatchers("/sub33/customCheck")
.access(new WebExpressionAuthorizationManager("@securityUtil.checkBoardWriter()"));이게 원래 되야하는데 안된다. 그래서 메소드 자체에 제한을 거는 것을 해보자.
이것을 왜사용하느냐? 복잡한 검증이 필요하기 때문이다.
@GetMapping("customCheck")
@PreAuthorize("@securityUtil.checkBoardWriter()")
public void customCheck() {
System.out.println("customCheck 메소드 실행 중");
}checkBoardWriter에서 검증 이후 같다면 true를 리턴하고 다르다면 false를 리턴한다.
true를 리턴하면 컨트롤러가 서블릿을 보여주고 false를 리턴하면 권한이 필요해서 로그인폼이 오게 된다.
누가 로그인했는지 정보를 얻으려면 Authentication객체를 통해얻으면된다.
호출할때 담아서 보내줘야한다.
컨트롤러의 파라미터를 받아서 #파라미터로 건네주면 비교를 할수 있다.
@GetMapping("customCheck")
@PreAuthorize("@securityUtil.checkBoardWriter(authentication, #id)")
public void customCheck(String id) {
System.out.println("customCheck 메소드 실행 중");
}@Component
public class SecurityUtil {
public boolean checkBoardWriter(Authentication authentication, String id) {
System.out.println("게시물 작성자 확인 메소드");
System.out.println(id);
System.out.println(authentication.getName());
return id.equals(authentication.getName());
}
}36.2 프로젝트 적용
@PostMapping("/update/{id}")
@PreAuthorize("isAuthenticated() and @customSecurityChecker.checkBoardWriter(authentication, #board.id)")
//수정하려는 게시물 id : board.id를 전달하던지 board를 전달해주기
public String updateProcess(Board board,
@RequestParam(value = "removeFiles", required = false) List<String> removeFileNames,
@RequestParam(value = "files", required = false) MultipartFile[] files,
RedirectAttributes rttr) {
try {
boolean ok = service.update(board, removeFileNames, files);
if (ok) {
// 해당게시물 보기로 리디렉션
rttr.addFlashAttribute("message", board.getId() + "번 게시물이 수정되었습니다.");
return "redirect:/detail/" + board.getId();
} else {
// 수정폼으로 리디렉션
rttr.addFlashAttribute("message", "게시물이 수정되지 않았습니다.");
}
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/update/" + board.getId();
}@Component
public class CustomSecurityChecker{
@Autowired
private BoardMapper mapper;
public boolean checkBoardWriter (Authentication authentication, Integer boardId) {
Board board = mapper.selectById(boardId);
String username = authentication.getName();
String writer = board.getWriter();
return username.equals(writer);
}
}36.3 VIEW수정
view를 수정해서 남의글의 수정버튼 자체를 막아보자.
sec:authentication 에서 값을 꺼내고 jstl if태그에서 비교를 해주면된다.
여러방법이 있다.
<sec:authorize access="isAuthenticated()">
<sec:authentication property="name" var="userId"/>
<c:if test="${userId eq board.writer }">
<a class="btn btn-secondary" href="/update/${board.id}">수정</a>
<button id="removeButton" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteConfirmModal" >삭제</button>
</c:if>
</sec:authorize>access에 #파라미터.프로퍼티해서 값을 넣을 수도 있다는 것을 발견했다.
메소드 실행도 된다.
<sec:authorize access="isAuthenticated() and @customSecurityChecker.checkBoardWriter(authentication, #board.id)">
<a class="btn btn-secondary" href="/update/${board.id}">수정</a>
<button id="removeButton" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteConfirmModal" >삭제</button>
</sec:authorize>
<sec:authorize access="isAuthenticated() and authentication.name eq #board.writer">
<a class="btn btn-secondary" href="/update/${board.id}">수정</a>
<button id="removeButton" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteConfirmModal" >삭제</button>
</sec:authorize>2023.05.09
놀라운것
작성자 자체를 넘겨주지 않고 세팅해버릴수 있다.
싱글톤객체임을 활용해서 다 같은 객체임을 활용하는 것이다.
'국비 > Project - 1 게시판' 카테고리의 다른 글
| 2023.05.11 74일차 Project (0) | 2023.05.11 |
|---|---|
| 2023.05.10 73일차 Project (0) | 2023.05.11 |
| 2023.05.08 71일차 Project (0) | 2023.05.09 |
| 2023.05.04 70일차 Project (0) | 2023.05.04 |
| 2023.05.03 69일차 Project (0) | 2023.05.03 |