https://magicmk.tistory.com/29
이전 편에 패스워드 암호화를 진행한 뒤 바로 로그인 구현을 위해 삽질을 했는데 진짜 별거 아닌 것 같았지만
수많은 시행착오를 겪느라 구현이 늦어버렸다. spring security config에 WebSecurityConfigureAdapter가
deprecated 되면서 다른 방안을 모색하느라 시간을 많이 허비한 것 같다...
앞으로는 전체 코드를 같이 볼 수 있게 github도 같이 올리면 좋을 것 같다. 그래서 맨 하단에 github 주소도
넣도록 하겠다.
✅ security config 설정하기
이번에 로그인을 구현하면서 이전 시간에 했던 security config와는 모습이 많이 바뀌었다.
package com.practice.board.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// 정적 리소스들이 보안필터를 거치지 않게끔
return (web) -> web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/font/**");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().disable() // cors 방지
.csrf().disable() // csrf 방지
.headers().frameOptions().disable(); // x frame 방어 해제
http.authorizeRequests()
// 페이지 권한 설정
.antMatchers("/member/**").hasRole("USER")
.antMatchers("/board/**").hasRole("USER")
.anyRequest().permitAll();
http.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error=true")
.usernameParameter("email")
.passwordParameter("password")
.and()
.logout()
.logoutSuccessUrl("/?logout=true")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
// status code 핸들링
http.exceptionHandling().accessDeniedPage("/denied");
return http.build();
}
}
formLogin에 관한 내용들은 간단하게 살펴보자면 다음과 같다.
⏹️ login 관련
메소드(?) | 내용 |
loginPage() | 해당 메소드가 없다면 그림1 과 같은 페이지가 나오고 파라미터로 html 파일 경로를 적어주면 해당 파일이 출력된다. |
loginProcessingUrl() | 말 그대로 login프로세스가 실행될 url을 적어주면 된다. 필자는 /login이라고 적었지만 /helloJava 이렇게 적어도 된다. 다만 login form에서 action을 동일하게 적어줘야 한다. |
defaultSuccessUrl() | 로그인이 성공했을 때 돌아갈 주소를 적어준다. |
failureUrl() | 로그인이 실패했을 때 돌아가는 주소 필자처럼 param값을 넘겨줄 수 있다. |
usernameParameter() | 해당 메소드에 적은 name을 login form에 적힌 name과 일치 시켜준다. |
passwordParameter() | 해당 메소드에 적은 name을 login form에 적힌 name과 일치 시켜준다. |
⏹️ logout 관련
메소드 | 내용 |
logoutSuccessUrl() | 로그아웃을 성공했을 때 돌아가는 주소 이것 또한 param을 같이 넘길 수 있다. |
invalidateHttpSession() | 로그아웃 시 session을 전부 날린다. |
deleteCookies() | 인자로 적은 값의 cookies를 죽인다. |
⏹️ 무쓸모 삽질...
WebSecurityConfigureAdapter가 *deprecated 되었는데 인터넷에 떠돌아 다니는 수많은 예제가 전부 해당 내용으로
작성되어 있다보니 config를 변경하는데 많은 삽질을 했다. 특히 이전 버전의 코드에서는
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
해당 내용이 있었는데 이것을 어떻게 변경해야 하는가 삽질을 많이 한 것 같다.
결론은 없어도 되는 것 같다.... 단 이후에 UserDetailsService를 override 해야 하는 과정은 필요하다.
Deprecated
사용해도 괜찮으나 언제 없어질 지 모르고 무슨 일이 벌어질지 모르니 권고하지 않는다.
라는 내용이다.
✅ UserDetailsService 구현
package com.practice.board.service.Impl;
import com.practice.board.domain.Member;
import com.practice.board.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("이메일이 존재하지 않습니다."));
return toUserDetails(member);
}
private UserDetails toUserDetails(Member member) {
return User.builder()
.username(member.getEmail())
.password(member.getPassword())
.authorities(new SimpleGrantedAuthority(member.getRole().toString()))
.build();
}
}
위에서 얘기했다시피 UserDetailsService는 구현해줘야 한다. member 객체를 찾아서 User로 build 시켜주면
Spring security가 알아서 확인하는 것 같다. 자세한 사항은 더 알아봐야 할 것 같다.
✅ 테스트
이렇게만 해주면 일단 끝이다. 진짜 이렇게 간단한 내용을 며칠을 삽질을 했는지 모르겠다.
(혹시 모른다 끝이 아닐지도...)
⏹️ 로그인 실패 테스트
잘못된 이메일을 입력하고 로그인을 클릭하면 위에 config에서 적용한 것처럼 param으로 error를 보내게 되고 thymeleaf에서 param을 잡아 해당 메세지를 출력해준다.
⏹️ 로그인 성공 테스트
로그인을 성공하면 홈 화면으로 리다이렉트 되고 추후 설명할 내용이지만 thymeleaf에서 session에 name을 받아와
화면에 출력해준다.
⏹️ 로그아웃 테스트
로그아웃 클릭 시 config에서 정의한 내용처럼 logout param을 넘겨주고 thymeleaf는 해당 param을 받아 메세지를 화면에 출력한다.
✅ Authorize 적용
해당 회원이 로그인 즉, 인증을 했는지 안했는지를 판단해야 한다. 위에서 보여준 security config에서
http.authorizeRequests()
// 페이지 권한 설정
.antMatchers("/member/**").hasRole("USER")
.antMatchers("/board/**").hasRole("USER")
.anyRequest().permitAll();
위와 같은 내용이 있는데 사실 필자처럼 role이 여러 개가 아니라 user 하나만 있다면 authenticated()를 사용해도 된다.
- authenticated() - 인증된 사용자의 접근을 허용
- permitAll() - 무조건 허용
- denyAll() - 무조건 차단
- anonymous() - 익명 사용자 허용
- hasRole(String) - 사용자가 주어진 역할이 있다면 접근을 허용
- hasAnyRole(String, ...) - 사용자가 주어진 역할 중 하나라도 있다면 허용
등등 많은 내용이 있다. 하지만 혹시 모를 admin 페이지를 생각해서 저렇게 적용해 놓았다.
⏹️ Role
package com.practice.board.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum Role {
ROLE_USER("user");
private String value;
}
role을 그냥 USER라고 해두면 security에서 인식을 하지 못하기 때문에 꼭 "ROLE_" 을 포함시켜줘야 한다.
이것 때문에도 한참을 삽질했다...
role을 생성했다면 member entity에도 role을 추가해줘야 한다.
해당 내용은 생략.
⏹️ thymeleaf authorize
마지막으로 view page에서 권한에 따른 페이지 변화시킬 수 있다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}">
<title>Home</title>
</head>
<body>
<div class="container">
<div sec:authorize="hasRole('USER')">
<h2 sec:authentication="name">사용자</h2>
<a href="/member/list">회원목록</a>
<a href="/logout">로그아웃</a>
</div>
<div sec:authorize="!hasRole('USER')">
<div th:if="${param.logout}">
로그아웃 하셨습니다.
</div>
<a href="/login">로그인</a>
</div>
</div>
</body>
</html>
간단하게 Home 화면을 예로 들자면 sec:authorize를 통해 인증된 사용자와 아닌 사용자를 구분하고
param으로 다른 메세지를 전달할 수도 있다. 자세한 내용은 thymeleaf 문법을 찾아보길 바란다.
https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#introducing-thymeleaf
✅ 끝
작성하고 보니 정리하면 20분도 안 걸리는 내용을 며칠 동안 팠다는 게.... 우울하다.
다른 사람들은 필자처럼 고생 안했으면 좋겠다 끝으로 Github.
https://github.com/Kimmingki/board
'Java > Spring Boot 게시판' 카테고리의 다른 글
spring boot 게시판 - 6 <bootstrap 적용하기> (0) | 2022.11.22 |
---|---|
spring boot 게시판 - 5 <thymeleaf layout 적용> (0) | 2022.11.21 |
spring boot 게시판 - 3 <패스워드 암호화, 회원 목록 조회> (0) | 2022.11.11 |
spring boot 게시판 - 2 <회원 중복 체크 및 유효성 검사> (0) | 2022.11.11 |
Spring boot 게시판 - 1 < 간단 회원 가입 구현> (8) | 2022.11.10 |