-
< Spring Boot > SecuritySpring Boot 2023. 10. 16. 11:25
이번 포스팅에서는 Security 에 대해 작성해보겠습니다.
이번 프로젝트를 하며 시큐리티에 처음 접해보았습니다.
시큐리티를 이용하면 로그인 인증 한 번만을 통해 세션이동 간에 인증을 유지하며 로그아웃 하면 세션을 날리는 것과 동시에 인증을 없앨 수 있습니다.
처음 접할 때는 상당히 골치 아팠지만 공부하여 사용해 보니 이 만큼 편한게 없다고 생각이 듭니다.
하지만 아직도 하나하나 구성 하라하면 머리가 아프지만 그래도 제가 공부하여 이해한 만큼 작성해보겠습니다.
시큐리티를 이용하기 위해선 config 클래스를 작성하여 bean 을 등록하여야 합니다
config 클래스 코드를 보며 설명해드리겠습니다.
import jakarta.servlet.DispatcherType; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import com.fullstack2.website.oauth2.OAuth2Service; @Configuration @EnableMethodSecurity @RequiredArgsConstructor public class SpringSecurityConfig { @Autowired private final OAuth2Service oAuth2UserService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeHttpRequests(request -> request .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() .requestMatchers("/**").permitAll() .requestMatchers("/view/admin").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(login -> login .loginPage("/view/login") .loginProcessingUrl("/login-process") .usernameParameter("email") .passwordParameter("password") //.defaultSuccessUrl("/view/mainLog", true) .defaultSuccessUrl("/", false) // 기본적으로 모든 사용자가 이동할 페이지 .successHandler((request, response, authentication) -> { // 사용자 역할을 확인하여 조건부로 리다이렉트 if (authentication != null && authentication.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) { response.sendRedirect("/view/admin"); // admin 역할을 가진 사용자 } else { response.sendRedirect("/view/mainLog"); // 그 외의 사용자 } }) ) .logout() .logoutUrl("/logout") .invalidateHttpSession(true); return http.build(); } }
전체 코드는 위와 같습니다.
이제 코드 하나하나 분리하여 설명 드리겠습니다.
@Configuration @EnableMethodSecurity @RequiredArgsConstructor public class SpringSecurityConfig { @Autowired private final OAuth2Service oAuth2UserService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeHttpRequests(request -> request .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() .requestMatchers("/**").permitAll() .requestMatchers("/view/admin").hasRole("ADMIN") .anyRequest().authenticated() ) .logout() .logoutUrl("/logout") .invalidateHttpSession(true); return http.build(); }
@Configuration 을 이용하여 config 클래스로 등록하고 enable Method Secrity 를 이용하여 시큐리티 메서드를 이용가능하게 만든다.
본인은 시큐리티에 포함 된 PasswordEncoder 를 이용해 회원가입 시 암호화된 비밀번호를 db에 저장하게 하였습니다.
spring boot 3.x 버전 부터는 filterChain 을 이용하여 시큐리티 빈을 생성하여 주어야 합니다.
http.csrf().disable()
웹 애플리케이션의 Cross-Site Request Forgery (CSRF) 보호를 비활성화합니다. CSRF는 악의적인 웹사이트가 사용자의 동의 없이 사용자를 대신해서 요청을 만드는 것을 방지하는 보안 기능입니다. 상태를 가지지 않거나 공개 API를 사용하는 경우 CSRF 보호를 비활성화해야 할 수 있습니다.
authorizeHttpRequests(request -> request ...: )
이 메서드 호출은 들어오는 HTTP 요청을 어떻게 승인할지 구성하는 데 사용됩니다. request 매개변수를 사용하여 다양 한 조건을 기반으로 규칙을 지정할 수 있습니다.
dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() .
"FORWARD" 디스패처 유형을 가진 모든 요청을 허용합니다. 디스패처 유형은 일반적으로 서블릿 컨테이너에서 요청이 어떻게 디스패치되는지를 나타냅니다.
requestMatchers("/status", "/view/join", "/auth/join", "/my/", "/my/main").permitAll()
지정된 URL에 일치하는 모든 요청을 인증이나 승인 확인없이 허용합니다. 이 경우 "/" 로 시작하는 모든 경로에 대한 요청이 인증이나 승인 검사없이 허용됩니다.
.anyRequest().authenticated()
Spring Security 구성에서 중요한 부분을 나타냅니다. 이 부분은 모든 요청에 대한 권한이 있어야 한다는 것을 의미합니다
- .anyRequest() : 이 부분은 모든 HTTP 요청을 나타냅니다. 어떤 URL이나 경로를 요청하든지 상관없이 해당 구성이 적용됩니다. 다시 말해, 이 부분은 모든 요청에 대한 보안 규칙을 정의하겠다는 것을 의미합니다.
- .authenticated(): 이 부분은 해당 모든 요청에 대해 인증이 필요하다고 명시합니다. 즉, 모든 요청은 사용자가 로그인하고 성공적으로 인증되어야만 액세스할 수 있습니다. 만약 사용자가 로그인하지 않은 상태로 요청을 보내면 해당 요청은 거부될 것입니다.
이번에는 시큐리티에 로그인에 대해 알아보겠습니다.
.formLogin(login -> login .loginPage("/view/login") .loginProcessingUrl("/login-process") .usernameParameter("email") .passwordParameter("password") //.defaultSuccessUrl("/view/mainLog", true) .defaultSuccessUrl("/", false) // 기본적으로 모든 사용자가 이동할 페이지 .successHandler((request, response, authentication) -> { // 사용자 역할을 확인하여 조건부로 리다이렉트 if (authentication != null && authentication.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) { response.sendRedirect("/view/admin"); // admin 역할을 가진 사용자 } else { response.sendRedirect("/view/mainLog"); // 그 외의 사용자 } }) )
우선 제가 설정한 formLogin 입니다.
현재 보여드린 코드는 커스텀으로 본인이 만든 뷰어에서 로그인하여 그 뷰에서 입력받은 아이디, 비밀번호가 디비에 존재하면 인증을 완료하여 defaultSuccessUrl로 이동하게 설정합니다.
successHandler 을 이용해 로그인 후 처리할 로직을 정의합니다.
여기서 저는 role 에 따라 로그인 후 이동하는 페이지를 다르게 설정했습니다.
Spring Security의 formLogin을 사용하면 로그인 페이지와 로그인 인증 과정을 쉽게 설정할 수 있으며, 사용자가 로그인 페이지로 이동하고 로그인 정보를 제출하면 Spring Security가 사용자를 인증하고 설정된 리다이렉션 및 처리 핸들러를 실행합니다. 이것은 보안 인증 및 사용자 관리를 구현하는 데 매우 유용한 도구입니다.
아래는 formLogin 에 대한 개념입니다.
.formLogin()
formLogin 메서드는 로그인 폼을 설정하고 로그인 요청을 처리하는 방법을 구성합니다. 일반적으로 다음과 같은 설정을 지정합니다
- .loginPage("/login") : 로그인 폼이 표시될 경로를 지정합니다. "/login" 경로를 요청하면 로그인 폼이 표시됩니다.
- .defaultSuccessUrl("/home"): 로그인 성공 후 리다이렉트될 경로를 지정합니다. 예를 들어, 사용자가 로그인에 성공하면 "/home"으로 리다이렉트됩니다.
- .failureUrl("/login?error"): 로그인 실패 시 리다이렉트될 경로를 지정합니다. 보통 로그인 실패 메시지와 함께 다시 로그인 페이지로 리다이렉트합니다.
- .permitAll(): 로그인 페이지에 대한 액세스를 모든 사용자에게 허용하도록 설정합니다.
.usernameParameter("username") 및 .passwordParameter("password")
로그인 폼에서 사용자 이름 및 비밀번호 필드의 이름을 지정합니다. 이러한 필드 이름은 로그인 요청에서 사용됩니다.
.loginProcessingUrl("/authenticate")
실제 로그인 처리가 이뤄질 URL을 지정합니다. 이 URL은 사용자가 로그인 요청을 보낼 때 사용됩니다.
.successHandler() 및 .failureHandler()
로그인 성공 및 실패 시 실행할 사용자 지정 로직을 지정하는 핸들러를 설정할 수 있습니다.
이번에는 로그아웃을 통해 인증을 삭제하는 법을 설명하겠습니다.
.logout() .logoutUrl("/logout") .invalidateHttpSession(true);
.logout()은 Spring Security에서 로그아웃 관련 설정을 구성하는 데 사용되는 메서드이며, .logoutUrl() 및 .invalidateHttpSession()은 로그아웃 동작에 대한 설정을 지정하는 데 도움이 됩니다. 이것들의 역할을 설명해보겠습니다
.logout()
이 메서드는 로그아웃 관련 설정을 시작하는 지점을 나타냅니다. 로그아웃은 사용자가 세션에서 로그아웃하고, 보통 로그아웃 후에 어떤 동작을 수행해야 하는지를 구성할 때 사용됩니다.
.logoutUrl("/logout")
이 부분은 로그아웃을 트리거할 로그아웃 URL을 지정합니다. 사용자가 "/logout" 경로로 GET 또는 POST 요청을 보내면 Spring Security는 해당 요청을 로그아웃으로 처리하고 사용자를 세션에서 로그아웃시킵니다.
.invalidateHttpSession(true)
이 설정은 사용자 세션의 무효화 여부를 결정합니다. .invalidateHttpSession(true)는 사용자의 세션을 로그아웃 후에 무효화하도록 지정합니다. 이것은 사용자가 로그아웃한 후에 새로운 세션을 시작하고 다시 로그인해야 함을 의미합니다. 일반적으로 로그아웃 시 세션의 무효화는 보안상 좋은 방법입니다.
이러한 설정을 사용하면 로그아웃 프로세스를 사용자 지정하고 보안을 강화할 수 있습니다. 사용자가 로그아웃 URL로 이동하거나 요청을 보내면 Spring Security는 사용자를 세션에서 로그아웃하고 세션을 무효화할지 여부를 결정합니다. 로그아웃 후의 동작 및 추가 설정은 애플리케이션의 요구에 따라 다양하게 구성할 수 있습니다.
config 를 정의 하였으니 이제 시큐리티 이용해 필요한
UserDetaile 를 implement 한 서비스 Component 와 여기 사용될 서비스인 MemberServuce 를 하나 생성해주겠습니다.
package com.fullstack2.website.config; 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.Component; import com.fullstack2.website.entity.Member; import com.fullstack2.website.service.MemberService; import java.util.Optional; @Component public class MyUserDetailService implements UserDetailsService { private final MemberService memberService; @Autowired public MyUserDetailService(MemberService memberService) { this.memberService = memberService; } @Override public UserDetails loadUserByUsername(String insertedUserId) throws UsernameNotFoundException { Optional<Member> findOne = memberService.findOne(insertedUserId); Member member = findOne.orElseThrow(() -> new UsernameNotFoundException("없는 회원입니다 ㅠ")); return User.builder() .username(member.getEmail()) .password(member.getPassword()) .roles(member.getRole()) .build(); } }
MyUserDetailService 클래스는 UserDetailsService 인터페이스를 구현하고 Spring 컴포넌트로 표시(@Component)합니다. 이것은 Spring 컨테이너에서 이 클래스의 인스턴스를 관리하도록 지정합니다.
생성자(MyUserDetailService)에서 MemberService를 주입받습니다.
(MemberService는 사용자 정보를 검색하고 관리하는 서비스 클래스입니다.)
loadUserByUsername
UserDetailsService 인터페이스의 요구 사항에 따라 구현되었습니다.
이 메서드는 사용자 이름(사용자 아이디)을 입력받아 해당 사용자의 정보를 로드하고 UserDetails 객체로 반환해야 합니다.
loadUserByUsername 메서드 내부에서는 입력받은 사용자 아이디를 사용하여 MemberService의 정의한 메서드인 finOne 을 통해 해당 사용자 정보를 조회합니다.
사용자가 존재하지 않을 경우 UsernameNotFoundException 예외를 던지고 해당 사용자가 없다는 메시지를 포함하여 처리합니다.
사용자 정보가 존재하는 경우, 이 정보를 기반으로 User 객체를 생성하고 반환합니다.
User는 Spring Security에서 제공하는 사용자 정보를 나타내는 클래스로, 사용자 이름, 비밀번호 및
역할(권한)을 설정합니다.
반환된 User 객체는 사용자의 인증을 나타내며,
Spring Security는 이 정보를 사용하여 사용자를 인증하고 요청된 작업에 대한 권한을 확인합니다.
요약하면, MyUserDetailService 클래스는 사용자의 인증 정보를 데이터베이스 또는 다른 데이터 소스에서 검색하고 Spring Security에게 제공하는 역할을 합니다. 이를 통해 사용자의 로그인 및 권한 관리를 구현할 수 있으며, loadUserByUsername 메서드를 통해 사용자 정보를 반환하여 Spring Security가 인증 및 권한 확인을 수행할 수 있게 합니다.
< MemberService>
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fullstack2.website.entity.Member; import com.fullstack2.website.repository.MemberRepository; import java.util.Optional; @Service public class MemberService { private final MemberRepository repository; public Optional<Member> findOne(String userId) { return repository.findByEmail(userId); } }
@Service
이 주석은 클래스가 Spring 애플리케이션 컨텍스트에서 서비스로 관리되어야 함을 나타냅니다. 이렇게 하면 Spring이 MemberService 클래스의 인스턴스를 생성하고 관리합니다.
@Autowired
repository 필드에 대한 주입을 나타냅니다. 이 필드에 주입된 MemberRepository는 데이터베이스와의 상호작용을 담당합니다. @Autowired를 통해 Spring은 해당 필드에 자동으로 MemberRepository의 인스턴스를 주입하게 됩니다.
findOne(String userId)
이 메서드는 주어진 사용자 아이디(userId)를 기반으로 회원 정보를 검색하는 데 사용됩니다.
메서드 내부에서는 repository를 통해 findByEmail 메서드를 호출하여 사용자를 찾고,
결과를 Optional<Member> 형식으로 반환합니다.
Optional은 검색 결과가 없을 수 있음을 나타내며,
null을 대체하는 안전한 방법으로 사용됩니다.
MemberService는 주로 사용자 정보를 검색하고 반환하는 역할을 수행합니다.
이것은 Spring 컨트롤러에서 사용자 인증 및 다른 회원 관련 작업에 사용될 수 있으며,
MemberRepository를 통해 데이터베이스와 통신하여 사용자 정보를 가져옵니다.
<Member (entity) >
package com.fullstack2.website.entity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; @AllArgsConstructor @NoArgsConstructor @Data @Entity @Builder public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true) private String email; private String password; private String name; // 주소 private String postalCode; private String addressBasic; private String addressRest; private String phone; private String mobile; private String birth; private String role; private String provider; // 회원가입 시 목록 추가할 곳 public static Member createMember( String email, String password, String name, String postalCode, String addressBasic, String addressRest, String phone, String mobile, String birth, PasswordEncoder passwordEncoder) { Member member = new Member(); member.setEmail(email); member.setPassword(passwordEncoder.encode(password)); member.setName(name); member.setPostalCode(postalCode); member.setAddressBasic(addressBasic); member.setAddressRest(addressRest); member.setPhone(phone); member.setMobile(mobile); member.setBirth(birth); member.setRole("USER"); member.setProvider("일반회원"); return member; } // 사용자의 이름이나 이메일을 업데이트하는 메소드 public Member updateUser(String name, String email, String birth, String mobile) { this.name = name; this.email = email; this.birth = birth; this.mobile = mobile; return this; } }
updateUser 메서드
사용자의 이름, 이메일, 생년월일, 휴대전화 번호를 업데이트할 때 사용됩니다. 이 메서드를 호출하면 해당 필드가 업데이트되고 업데이트된 멤버 객체가 반환됩니다.
createMember 메서드
createMember 메서드는 회원을 생성할 때 사용되며, 주요 정보와 PasswordEncoder를 입력으로 받아 회원 객체를 생성합니다. 비밀번호는 passwordEncoder를 사용하여 암호화되어 저장됩니다.
<MemberRepository>
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.fullstack2.website.entity.Member; import java.util.Optional; @Repository public interface MemberRepository extends JpaRepository<Member, Long> { Optional<Member> findByEmail(String email); }
이 코드는 Spring Data JPA를 사용하여 Member 엔티티와 상호작용하는 데 사용되는 MemberRepository 인터페이스를 정의합니다. MemberRepository는 데이터베이스에 저장된 회원 정보를 검색하고 조작하는 데 사용됩니다.
여기에서 주요 구성과 기능을 설명하겠습니다
JpaRepository<Member, Long>
MemberRepository는 Spring Data JPA의 JpaRepository 인터페이스를 확장하고 있습니다. 이것은 기본적인 CRUD (Create, Read, Update, Delete) 작업을 제공합니다. Member 엔티티를 관리하며, Long은 엔티티의 주요 키(Primary Key) 데이터 유형을 나타냅니다.
@Repository
이 주석은 Spring에서 MemberRepository가 빈으로 관리되는 컴포넌트임을 나타냅니다. 이것은 Spring에서 이 리포지토리 인터페이스의 구현체를 자동으로 생성하고 관리하여 데이터베이스 작업을 쉽게 수행할 수 있도록 해줍니다.
Optional<Member> findByEmail(String email)
이 메서드는 사용자의 이메일 주소에 해당하는 회원을 찾는데 사용됩니다. Spring Data JPA는 메서드 이름을 통해 자동으로 쿼리를 생성합니다. 여기서 findByEmail은 email 필드를 기반으로 검색하는 메서드를 정의하는 것입니다. Optional은 검색 결과가 없을 수 있으므로 null 대신 Optional을 사용하여 결과를 반환합니다.
이로써 security 설정에 대한 포스팅을 끝내겠습니다.
'Spring Boot' 카테고리의 다른 글
< Spring Boot > RESTful (0) 2023.11.02 <Spring Boot> 간단한 스프링부트를 이용한 회원가입, 로그인 (0) 2023.09.05 < Spring Boot > ThymLeaf (0) 2023.08.30 <Spring Boot> queryAnnoation (0) 2023.08.25 <Spring Boot> queryMethod (0) 2023.08.25