Sprint Boot에서 Spring Security를 사용하여 로그인, 로그아웃 기능을 이용하는 방법을 알아보고
로그아웃 후에 redirect된 로그인 페이지에서 로그인을 하면 로그인 페이지가 다시 뜨는 문제에 대한 원인과 해결방법을 알아본다.
User Schema
Spring Security 에서 기본으로 제공하는 User Schema는 다음과 같다.

User와 Authority는 일대다 관계로 맵핑하고 Authority의 Primary Key 는 복합키로 username + authority 이다.
User & Authority Model
Spring Hibernate 를 이용하여 구현한 User 와 Authority Model
User
...
@Entity(name = "users")
public class User {
@Id
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, length = 200)
private String password;
@Column(nullable = false, columnDefinition = "TINYINT(1)")
@Type(type = "org.hibernate.type.NumericBooleanType")
private boolean enabled = true;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "username")
private List<Authority> authorities = new ArrayList<>();
@Column
private String name;
...
}
Authority
...
@Entity(name = "authorities")
public class Authority {
public static final String ROLE_USER = "ROLE_USER";
public static final String ROLE_ADMIN = "ROLE_ADMIN";
public static final String ROLE_DBA = "ROLE_DBA";
private @EmbeddedId AuthorityId id;
public Authority(String username, String role) {
id = new AuthorityId(username, role);
}
}
@Embeddable
@AllArgsConstructor
@NoArgsConstructor
class AuthorityId implements Serializable {
String username;
@Column
String authority;
}
Spring Security 설정 클래스 구현
WebSecurityConfigurerAdapter을 상속하여 spring security 관련 설정을 작성한 클래스이다.
...
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final DataSource dataSource;
@Autowired
DevistAuthenticationSuccessHandler devistAuthenticationSuccessHandler;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
public SecurityConfiguration(DataSource dataSource) {
this.dataSource = dataSource;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username,password, enabled from users where username=?")
.authoritiesByUsernameQuery("select username, authority from authorities where username=?")
.passwordEncoder(passwordEncoder())
.withUser("admin").password(passwordEncoder().encode("1234")).roles("ADMIN")
.and()
.withUser("dbadmin").password(passwordEncoder().encode("1234")).roles("ADMIN", "DBA");
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/resources/**", "/webjars/**", "/static/**", "/signup", "/").permitAll()
.antMatchers("/h2-console/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.and()
.logout()
.logoutSuccessUrl("/login")
.and()
.csrf()
.ignoringAntMatchers("/h2-console/**")
.and()
.headers().frameOptions().disable();
}
}
로그인 처리 과정
요청이 들어오면 filter 에 의해 권한이 필요한 요청인지 아닌지를 판단하여 필요하면 /login 페이지를 반환한다.
여authorizeRequests().antMatchers() 를 이용하여 권한이 필요한 URL을 지정한다.
formLogin() 으로 로그인 처리 관련 설정을 하는데 loginPage()에 설정한 URL이 로그인에 사용될 URL이다.
.successHandler() 을 지정하면 로그인 후에 처리할 핸들러를 지정할 수 있다.
기본 설정에 의하여 Redirect 된 로그인 페이지에서 로그인을 하면 로그인 하기 전 요청했던 URL로 redirect 한다.
처음부터 로그인 페이지로 접속하여 로그인을 요청하면 완료 후에 기본 설정인 / 으로 redirect 된다.
로그아웃 처리 과정
Logout 기본 설정된 URL
- 로그아웃 성공 후 반환하는 URL(logoutSuccessUrl) : “/login?logout”, GET
- 로그아웃 요청시 사용하는 URL(logoutUrl) : “/logout”, POST
로그아웃 요청을 하기 위해서는 /logout 을 사용하고 로그아웃 후에는 /login?logout으로 redirect 된다.
로그인 후에 처리되는 SavedRequestAwareAuthenticationSuccessHandler
SavedRequestAwareAuthenticationSuccessHandler는 기본으로 들어가 있는 login success handler 이다.
이 클래스는 RequestCache 라는 객체를 가지고 있는데 /login으로 redirect 되는 어떤 요청이 들어오면 관련 Request정보를 저장해두고 나중에 사용한다.
이렇게 request의 정보를 저장해두기 때문에 이용하여 우리가 로그인 하지 않은 상태로 /todo/add 와 같은 URL에 요청을 하면 /login으로 redirect 되고, 로그인을 하면 다시 /todo/add 로 돌아가는 기능을 사용할 수 있는 것이다.
로그아웃 후 로그인 페이지가 두번 뜨는 현상
spring security 기본 설정으로 구현했을 때, 로그아웃 후에 로그인을 할 때의 상황을 생각해보자. 로그아웃을 하면 로그인 페이지로 redirect 된다. 이 상태에서 로그인을 하면 로그인이 성공하지만 다시 로그인 페이지가 뜬다. redirect된 URL은 /login?logout 으로 표시된다.
로그아웃 후에 로그인 할 때 다음과 같은 과정이 처리된다. 로그 아웃을 하면 logoutSuccessUrl인 /login?logout으로 redirect 한다. 앞에서 설명한 SavedRequestAwareAuthenticationSuccessHandler에 의해 SavedRequest에 /login?logout을 저장하고 로그인 페이지를 띄운다. 로그인 요청해서 성공하면 저장되어 있던 /login?logout으로 redirect를 시도한다. /login?logout 은 /login 요청에 logout이라는 파라미터를 추가한 것이기 때문에 로그인 페이지가 다시 뜨게 된다.
정리해보면,
- 로그아웃 요청 (
/logout) 및 성공 /login?logout로 Redirect- SavedRequest로
/login?logoutURL 저장 - 로그인 페이지 반환
- 로그인 요청 및 성공
- 저장되어 있던 SavedRequest의 URL 인
/login?logout으로 Redirect - 다시 로그인 페이지 반환
해결방법
logout의 successUrl을 /login으로 설정해준다.
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
...
.formLogin()
...
.logout()
.logoutSuccessUrl("/login")
.and()
...
/login으로 요청하여 처리하는 로그인은 SavedRequest 를 따로 저장하지 않기 때문에 로그인이 완료되면 /으로 Redirect 된다.
Spring Security logging 보기 활성화
application.yml
logging:
level:
org.springframework.security: DEBUG