[Springboot/Spring-Security]Spring Security를 이용하여 특정 권한을 가진 유저의 Login, Logout을 처리해보자

Juseong Han·2022년 9월 20일
0

궁금증

Spring security를 이용하여 특정 권한을 갖고있는 사용자를 안전하게 로그인 및 로그아웃 시키는 방법이 무엇일까?

해결법

Spring security에 설정을 해주면 된다.
thymeleaf, security, web, jpa를 활용하였다.

1. 기본 설정(Gradle, thymeleaf)

// spring-security
impementation("org.springframework.boot:spring-boot-starter-security")
impementation("org.springframework.boot:spring-boot-starter-data-jpa")
impementation("org.springframework.boot:spring-boot-starter-web")
impementation("org.springframework.boot:spring-boot-starter-thymeleaf")

build.gradle

<form th:action="@{/login}" method="POST">
      
        <h1 class="h3 mb-3 fw-normal">로그인 페이지</h1>

        <div class="form-floating">
            <input type="text" class="form-control" name="username" id="username" placeholder="아이디 입력...">
            <label for="username">아이디</label>
        </div>
        <div class="form-floating">
            <input type="password" class="form-control" name="password" id="password" placeholder="Password">
            <label for="password">비밀번호</label>
        </div>

        <div class="checkbox mb-3">
            <label>
                <input type="checkbox" id="remember-me" name="remember-me"> 아이디 저장
            </label>
        </div>
        <button id="btn-login" class="w-100 btn btn-lg btn-primary" type="submit">로그인</button>
    </form>

login.html

기본적으로 필요한 두 개의 file이다, 여기서 중요한 점은 thymeleaf에서 login processing을 하는 form이 submit을 통해 POST method로 서버로 넘어가야한다는 점이다.

참고로 checkbox의 remember-me는 다음 포스트에서 활용 방법을 설명하겠다.

2. Spring Security 설정

security 설정은 o.s.s.c.a.w.c.WebSecurityConfigurerAdapter 추상클래스를 상속받아 구현 가능하다. 일정 버전 이상의 Springboot에서는 새로운 방법으로 시큐리티 설정이 가능한데 그 방법은 따로 서술하지 않고 여기에서 확인 가능하다.

@RequiredArgsConstructor
@SpringBootConfiguration
@EnableWebSecurity
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {
	private final UserService;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable().headers().frameOptions().disable();
        
        // login
        http.formLogin()
                .loginPage("/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/login/error")
                .permitAll();

        // logout
        http.logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true);

        http.userDetailsService(userService);
    }
    
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }
}

CustomWebSecurityConfig.java

일단 하나씩 파헤쳐보자.

1. http.csrf().disable()

csrf란 Cross Site Request Forgery로 크로스 사이트 공격을 방어하는 것이다. 이는 spring security에서 default로 protection이 적용중이지만 spring security 공식 가이드에 따르면 non-browser clients 만을 위한 서비스라면 csrf를 disable 하여도 좋다고 한다.

rest api를 이용한 서버라면, session 기반 인증과는 다르게 stateless하기 때문에 서버에 인증정보를 보관하지 않는다. rest api에서 client는 권한이 필요한 요청을 하기 위해서는 요청에 필요한 인증 정보를(OAuth2, jwt토큰 등)을 포함시켜야 한다. 따라서 서버에 인증정보를 저장하지 않기 때문에 굳이 불필요한 csrf 코드들을 작성할 필요가 없다.

2. http.header().frameOption().disable()

iframe태그를 사용할 때 목적 도메인의 프레임옵션인 X-Frame-Option을 설정하는 것으로 disable()하면 보안상 이슈가 발생할 수 있다.

3. http.formLogin()

로그인 처리를 하는 부분이다. formLogin()으로 시작이 되어야 한다.
loginPage()
로그인 페이지로 보내주는 url이다. 여기서의 /login은 GET 요청을 뜻한다.

usernameParameter()
default로 "username"이 들어간다.(username인 경우 설정불필요) 이는 html의 form의 input 요소 중 어떤 name을 가진 요소가 ID에 해당하는 것인지를 알려주는 부분이다.

passwordParameter()
이 역시 위와 동일하게 password가 default로 들어가며 password에 해당하는 input의 name이 무엇인지 알려주는 부분이다.

loginProcessingUrl()
로그인 처리를 담당할 url을 설정하는 부분이다. 위의 html코드에서 봤듯이 /login POST 요청을 보낸다. 여기서의 /login은 POST 요청을 뜻한다.

defaultSuccessUrl()
로그인에 성공했을 때 어떤 Url로 리디렉션할지 정하는 부분이다. 보통 index로 이동을 많이하여 /로 설정했다.

failureUrl()
위와 마찬가지로 로그인 실패시 리디렉션할 url이다

permitAll()
모든 사용자에게 요청을 허용한다는 뜻이다.(인증되지 않았더라도)

4. http.formLogout()

로그아웃을 담당하는 부분이다.

logoutRequestMatcher()
로그아웃을 요청하는 url이 무엇인지 설정하는 부분이다 logoutUrl()로 대체 가능하다.

logoutSuccessUrl()
로그아웃에 성공했을 때 보내질 url이다

invalidateHttpSession()
로그아웃에 성공했을 때 Session을 끊을것인지 여부를 결정한다.

5. http.userDetailsService()

Spring security에서는 UserDetails라는 인터페이스가 사용자의 정보를 담는 역할을 한다. 이를 반환하는 Service를 지정해주는 부분이다. 밑에 추가로 설명하겠다.

5. auth.userDetailService().passwordEncoder()

인증정보를 담기 위해 사용될 UserDetails를 만들어주는 UserDetailService와 패스워드 인코딩을 도와줄 passwordEncoder를 지정한다.

3. UserDetailsService란?

앞서 간단히 설명한대로 UserDetails 인터페이스는 Spring security에서 사용자의 정보를 담는 역할을 한다. UserDetailsService 인터페이스에는 loadByUsername()메소드가 있는데 이는 username, 즉 id를 통해 찾은 유저 정보를 UserDetails형태로 return 하는 메소드이다. 우리는 이 인터페이스를 직접 구현한다.

@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {
	private final UserRepository userRepository;
    private final HttpSession httpSession;
    
    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username).orElse(null);
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(user.getUserRole().getRole()));
        httpSession.setAttribute("user", user);
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
    }
}

UserService.java

UserDetails는 다음과 같은 필드를 가지고 있다.
String username : 유저 이름
String password : 패스워드
Collection<? extends GrantedAuthority> authorities : 인증정보들

그 외에도 여러 필드가 있지만 나머지는 default값이 있기 때문에 굳이 수정할 필요가 없다. 위 코드는

authorities.add(new SimpleGrantedAuthority(user.getUserRole().getRole()));

바로 여기에서 UserRole을 설정한다. 그 뒤

httpSession.setAttribute("user", user);

session에 현재 사용자를 저장한다(실제론 민감정보때문에 user자체를 보내선 안된다.)

return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);

username, password, authority를 담은 UserDetails를 return한다.

다음과 같이 설정하면 Controller에서 따로 /login POST 요청에 대한 코드를 작성하지않아도 /login 요청이 들어오면 Security가 알아서 프로세스를 처리하게된다.

다음 포스트는 사용자를 쿠키에 저장하는 remember-me 기능에 대해 소개하겠다.

profile
개발이 하고 싶어요💻 개발이 너무 재밌는 Juseong입니다.🖐

0개의 댓글