| |
| implementation 'org.springframework.boot:spring-boot-starter-security:3.0.1' |
- 애플리케이션을 만들기 위해서는 보통 인증/인가 등의 보안이 필요
- 웹에서 인증이란 해당 리소스에 대해서 작업을 수행할 수 있는 주체인지 확인하는 것
- 인가는 인증 과정 이후에 일어나며 리소스에 접근 시 인가된 유저인지 확인(접근 권한 확인)
→ 시큐리티를 추가 후 웹페이지에 접속하면 해당 화면이 뜸
→ user, 화면에 찍히는 비밀번호로 로그인
→ 한 번 로그인하면 서버가 계속 기억을 하고 있기 때문에 로그인이 되는지 확인하려면 http://localhost:8000/logout 로 접속하여 로그아웃을 해줘야 함!
→ 비밀번호가 복잡하기 때문에 application.properties에 코드를 추가하여 user, 1234로 변경
| ########################## |
| # Security 유저 설정 |
| ########################## |
| spring.security.user.name=user |
| spring.security.user.password=1234 |
SecurityConfig 클래스 작성 후 configure 메서드에 설정을 추가하지 않으면 더 이상 요청에 인증을 요구하지 않음
| @Configurable |
| @EnableWebSecurity |
| public class SecurityConfig { |
| |
| @Bean |
| public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
| return http.build(); |
| } |
| } |
- 데이터베이스에서 회원정보를 가져오는 인터페이스
- loadUserByUsername() 메소드를 통해 회원 정보를 조회 → 사용자의 정보와 권한을 갖는 UserDetails 인터페이스를 반환
- 회원 정보를 담는 인터페이스
- 직접 구현하거나 스프링 시큐리티에서 제공하는 User 클래스 사용(구현체)
MemberService.java
| package kr.spring.member.service; |
| import kr.spring.member.entity.Member; |
| import kr.spring.member.repository.MemberRepository; |
| import lombok.RequiredArgsConstructor; |
| import lombok.extern.log4j.Log4j2; |
| import org.springframework.beans.factory.annotation.Autowired; |
| 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 |
| @Transactional |
| @Log4j2 |
| |
| |
| @RequiredArgsConstructor |
| public class MemberService implements UserDetailsService { |
| |
| private final MemberRepository memberRepository; |
| |
| public Member saveMember(Member member) { |
| |
| validateDuplicate(member); |
| |
| return memberRepository.save(member); |
| } |
| |
| private void validateDuplicate(Member member) { |
| |
| Member findMember = memberRepository.findByEmail(member.getEmail()); |
| |
| if(findMember != null) { |
| throw new IllegalStateException("이미 등록된 사용자 입니다."); |
| } |
| |
| } |
| |
| |
| @Override |
| public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { |
| |
| |
| Member member = memberRepository.findByEmail(email); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if(member == null) { |
| |
| throw new UsernameNotFoundException("해당 사용자가 없습니다." + email); |
| } |
| |
| log.info("=================> loadUserByUsername : " + member); |
| |
| return User.builder() |
| .username(member.getEmail()) |
| .password(member.getPassword()) |
| .roles(member.getRole().toString()) |
| .build(); |
| } |
| } |
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(); |
| } |
- configure(HttpSecurity) 메소드를 통해 로그인 및 로그아웃 URL 지정
- http.formLogin() - http를 통해 들어오는 form 기반 request를 이용하여 Login을 처리
- form 태그에서 사용자의 ID 부분은 default 값으로 “username” 필드
| import jakarta.servlet.DispatcherType; |
| 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.crypto.bcrypt.BCryptPasswordEncoder; |
| import org.springframework.security.crypto.password.PasswordEncoder; |
| import org.springframework.security.web.SecurityFilterChain; |
| import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
| |
| @Configuration |
| |
| @EnableWebSecurity |
| public class SecurityConfig { |
| |
| |
| @Bean |
| |
| public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
| |
| http.formLogin() |
| .loginPage("/member/login") |
| .defaultSuccessUrl("/") |
| .usernameParameter("email") |
| .failureUrl("/member/login/error") |
| .and() |
| .logout() |
| .logoutRequestMatcher(new AntPathRequestMatcher("/member/logout")) |
| .logoutSuccessUrl("/"); |
| |
| http.authorizeHttpRequests() |
| |
| .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() |
| .requestMatchers("/css/**", "/js/**").permitAll() |
| .requestMatchers("/", "/member/**", "/item/**", "/images/**").permitAll() |
| .requestMatchers("/admin/**").hasRole("ADMIN") |
| .anyRequest().authenticated(); |
| |
| http.exceptionHandling() |
| .authenticationEntryPoint(new CustomEntryPoint()); |
| |
| return http.build(); |
| } |
| |
| } |
- AuthenticationManagerBuilder를 통해 AuthenticationManager를 생성하여 인증 처리 수행
- UserDetailsService 인터페이스를 구현하고 loadUserByUsername 메소드를 오버라이딩한 memberService 객체를 이용하여 User 객체를 얻어낸 뒤, 지정된 비밀번호 암호화 방식으로 비밀번호가 일치하는지 검증
| @Bean |
| public PasswordEncoder passwordEncoder() { |
| |
| return new BCryptPasswordEncoder(); |
| } |
| |
| <!DOCTYPE html> |
| <html lang="en" |
| xmlns:th="http://www.thymeleaf.org" |
| xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" |
| layout:decorate="~{layouts/layout1}"> |
| |
| <!DOCTYPE html> |
| <html lang="en" xmlns:th="http://www.thymeleaf.org" |
| xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" |
| layout:decorate="~{layouts/layout1}"> |
| |
| |
| <th:block layout:fragment="css"> |
| <style> |
| |
| .error { |
| color: #bd2130; |
| } |
| </style> |
| </th:block> |
| |
| <div layout:fragment="content"> |
| |
| |
| <form role="form" method="post" action="/member/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='/member/new'">회원가입</button> |
| <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> |
| </form> |
| |
| </div> |
| |
| </html> |
- 로그인한 상태에서는 로그아웃만 노출, 상품 등록 메뉴는 관리자로 로그인 했을 때만 노출되도록 인증 및 권한에 따라 설정 변경을 도와주는 라이브러리
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE'
| <!DOCTYPE html> |
| <html lang="en" |
| xmlns:th="http://www.thymeleaf.org" |
| xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> |
| |
| |
| <div th:fragment="header"> |
| <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> |
| <div class="container-fluid"> |
| <a class="navbar-brand" href="/">Woojin's Shop</a> |
| <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> |
| <span class="navbar-toggler-icon"></span> |
| </button> |
| <div class="collapse navbar-collapse" id="navbarSupportedContent"> |
| <ul class="navbar-nav me-auto mb-2 mb-lg-0"> |
| |
| <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')"> |
| <a class="nav-link active" aria-current="page" href="/admin/item/new" >상품등록</a></li> |
| |
| <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')"> |
| <a class="nav-link active" aria-current="page" href="/admin/items">상품관리</a></li> |
| |
| <li class="nav-item" sec:authorize="isAuthenticated()"> |
| <a class="nav-link active" aria-current="page" href="/cart">장바구니</a></li> |
| |
| <li class="nav-item" sec:authorize="isAuthenticated()"> |
| <a class="nav-link active" aria-current="page" href="/orders">구매이력</a></li> |
| |
| <li class="nav-item" sec:authorize="isAuthenticated()"> |
| <a class="nav-link active" aria-current="page" href="/board">게시판</a></li> |
| |
| <li class="nav-item" sec:authorize="isAnonymous()"> |
| <a class="nav-link active" aria-current="page" href="/member/login">로그인</a></li> |
| |
| <li class="nav-item" sec:authorize="isAuthenticated()"> |
| <a class="nav-link active" aria-current="page" href="/member/logout">로그아웃</a></li> |
| </ul> |
| <form class="d-flex" role="search"> |
| <input class="form-control me-2" type="search" placeholder="Search" |
| aria-label="Search"> |
| <button class="btn btn-outline-success" type="submit">Search</button> |
| </form> |
| </div> |
| </div> |
| </nav> |
| </div> |
| |
| </html> |