로그인 / 로그아웃
1. UserDetailsService
- 데이터베이스에서 회원 정보를 가져오는 인터페이스
- loadUserByUsername() 메소드를 통해 회원정보를 조회 -> UserDetails 인터페이스 반환
2. UserDetails
- 회원 정보를 담는 인터페이스
- 직접 구현하거나 스프링 시큐리티에서 제공하는 User 클래스 사용 (구현체)
3. MemberService 로그인 / 로그아웃 구현
- UserDetailsService 인터페이스를 구현하고 loadUserByUsername() 메소드 오버라이딩
- Builder 패턴을 이용하여 UserDetail 인터페이스를 구현한 User 객체 생성 후 반환
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(email);
if (member == null) {
throw new UsernameNotFoundException(email);
}
return User.builder()
.username(member.getEmail())
.password(member.getPassword())
.roles(member.getRole().toString())
.build();
}
4. SecurityConfig 인증 filter 추가
- configure(HttpSecurity) 메소드를 통해 로그인 및 로그아웃 URL 지정
- http.formLogin() - http 를 통해 들어오는 form 기반 request 를 이용하여 Login 을 처리
- form 태그에서 사용자의 ID 부분은 default 값으로 "username" 필드
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MemberService memberService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/member/login")
.defaultSuccessUrl("/")
.usernameParameter("email")
.failureUrl("/member/login/fail")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
.logoutSuccessUrl("/");
}
}
- AuthenticationManagerBuilder 를 통해 AuthenticationManager 를 생성하여 인증 처리 수행
- UserDetailsService 인터페이스를 구현하고 loadUserByUsername 메소드를 오버라이딩한 memberService 객체를 이용하여 User 객체를 얻어낸 뒤, 지정된 비밀번호 암호화 방식으로 비밀번호가 일치하는지 검증
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/img/**");
}
5. 로그인 페이지
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
<style>
.error {
color: #bd2130;
}
</style>
</th:block>
<div layout:fragment="content">
<form role="form" method="post" action="/members/login">
<div class="form-group">
<label th:for="email">이메일주소</label>
<input type="email" name="email" class="form-control" placeholder="이메일을 입력해주세요">
</div>
<div class="form-group">
<label th:for="password">비밀번호</label>
<input type="password" name="password" id="password" class="form-control" placeholder="비밀번호 입력">
</div>
<p th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"></p>
<button class="btn btn-primary">로그인</button>
<button type="button" class="btn btn-primary" onClick="location.href='/members/new'">회원가입</button>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
</div>
</html>
6. spring-security-test 의존성 주입
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
<version>${spring-security.version}</version>
</dependency>
7. 로그인 테스트
- MemberControllerTest 클래스 생성 및 DI
- @AutoConfigureMockMvc - Controller 를 테스트할 때, 서블릿 컨테이너를 모킹하여 가상의 클라이언트로부터 Request 요청을 보내는 역할 수행
@SpringBootTest
@Transactional
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberControllerTest {
@Autowired
MemberService memberService;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
private MockMvc mockMvc;
public Member createMember(String email, String password) {
JoinFormDto memberFormDto = new JoinFormDto();
memberFormDto.setEmail(email);
memberFormDto.setName("홍길동");
memberFormDto.setAddress("서울시 마포구 합정동");
memberFormDto.setPassword(password);
Member member = Member.createMember(memberFormDto, passwordEncoder);
return memberService.saveMember(member);
}
- 로그인 성공 테스트
- perform() 메소드 - 브라우저에서 서버에 URL 요청을 하는 것처럼 Reqeust 작성
@Test
@DisplayName("로그인 성공 테스트")
public void loginSuccessTest() throws Exception {
String email = "test@email.com";
String password = "1234";
this.createMember(email, password);
mockMvc.perform(formLogin().loginProcessingUrl("/member/login")
.userParameter("email").user(email).password(password))
.andDo(print())
.andExpect(SecurityMockMvcResultMatchers.authenticated());
}
formLogin() - form 태그 기반의 로그인 인증 방식(http.formLogin())을 테스트하기 위해 form 태그 기반 POST request 객체를 생성
loginProcessingUrl() - 요청 URL 설정 (formLogin() 메소드의 매개변수로 설정해도됨)
userParameter("email") - formLogin 방식에서 Request 메시지 Body 부분은 "username"=value 가 default 값이기 때문에 "email"=value 로 변경
user().password() - Requset 메시지 Body Data 설정
- 로그인 실패 테스트
@Test
@DisplayName("로그인 실패 테스트")
public void loginFailTest() throws Exception {
String email = "test@email.com";
String password = "1234";
this.createMember(email, password);
mockMvc.perform(formLogin().loginProcessingUrl("/member/login")
.userParameter("email").user(email).password("12345"))
.andExpect(SecurityMockMvcResultMatchers.unauthenticated());
}
8. thymeleaf-extras-springsecurity5 의존성 추가
- 로그인한 상태에서는 로그아웃만 노출, 상품 등록 메뉴는 관리자로 로그인 했을 때만 노출 되도록 인증 및 권한에 따라 설정 변경을 도와주는 라이브러리
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
9. header Navbar 부분 수정
- xmlns:sec 네임 스페이스 지정
- isAnonymous() - 게스트
- isAuthenticated() - 로그인
- hasAnyAuthority('ROLE_ADMIN') - ADMIN
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<div th:fragment="header">
<nav class="navbar navbar-expand-sm bg-primary navbar-dark">
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarTogglerDemo03" aria-controls="navbarTogglerDemo03"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/">Shop</a>
<div class="collapse navbar-collapse" id="navbarTogglerDemo03">
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
<li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
<a class="nav-link" href="/admin/item/new">상품 등록</a>
</li>
<li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
<a class="nav-link" href="/admin/items">상품 관리</a>
</li>
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" href="/cart">장바구니</a>
</li>
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" href="/orders">구매이력</a>
</li>
<li class="nav-item" sec:authorize="isAnonymous()">
<a class="nav-link" href="/members/login">로그인</a>
</li>
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" href="/members/logout">로그아웃</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0" th:action="@{/}" method="get">
<input name="searchQuery" class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</nav>
</div>
</html>
10. 인증 및 권한에 따른 화면
- Guest 상태
- 로그인 상태
- ADMIN 권한 화면은 권한 설정 참고
'JAVA > SpringBoot Shoppingmall' 카테고리의 다른 글
[VSCODE] SpringBoot 쇼핑몰(MVN) Entity 연관 관계 매핑 (0) | 2022.06.27 |
---|---|
[VSCODE] SpringBoot 쇼핑몰(MVN) 페이지 권한 설정 (0) | 2022.06.27 |
[VSCODE] SpringBoot 쇼핑몰(MVN) 회원가입 기능 구현 (0) | 2022.06.27 |
[VSCODE] SpringBoot 쇼핑몰(MVN) Sring Security (0) | 2022.06.27 |
[VSCODE] SpringBoot 쇼핑몰(MVN) Thymeleaf - 페이지 레이아웃 (0) | 2022.06.24 |