Merge pull request #9069 from kirill-vlasov/master

BAEL-3969 Spring Security - Custom Logout Handler
This commit is contained in:
Jonathan Cook 2020-04-26 11:18:09 +02:00 committed by GitHub
commit 5eb21c54bb
12 changed files with 356 additions and 0 deletions

View File

@ -0,0 +1,13 @@
package com.baeldung.customlogouthandler;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CustomLogoutApplication {
public static void main(String[] args) {
SpringApplication.run(CustomLogoutApplication.class, args);
}
}

View File

@ -0,0 +1,55 @@
package com.baeldung.customlogouthandler;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import com.baeldung.customlogouthandler.web.CustomLogoutHandler;
@Configuration
@EnableWebSecurity
public class MvcConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private CustomLogoutHandler logoutHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/user/**")
.hasRole("USER")
.and()
.logout()
.logoutUrl("/user/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
.permitAll()
.and()
.csrf()
.disable()
.formLogin()
.disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select login, password, true from users where login=?")
.authoritiesByUsernameQuery("select login, role from users where login=?");
}
}

View File

@ -0,0 +1,35 @@
package com.baeldung.customlogouthandler.services;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import com.baeldung.customlogouthandler.user.User;
@Service
public class UserCache {
@PersistenceContext
private EntityManager entityManager;
private final ConcurrentMap<String, User> store = new ConcurrentHashMap<>(256);
public User getByUserName(String userName) {
return store.computeIfAbsent(userName, k -> entityManager.createQuery("from User where login=:login", User.class)
.setParameter("login", k)
.getSingleResult());
}
public void evictUser(String userName) {
store.remove(userName);
}
public int size() {
return store.size();
}
}

View File

@ -0,0 +1,61 @@
package com.baeldung.customlogouthandler.user;
import javax.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(unique = true)
private String login;
private String password;
private String role;
private String language;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
}

View File

@ -0,0 +1,14 @@
package com.baeldung.customlogouthandler.user;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class UserUtils {
public static String getAuthenticatedUserName() {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
return auth != null ? ((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername() : null;
}
}

View File

@ -0,0 +1,28 @@
package com.baeldung.customlogouthandler.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Service;
import com.baeldung.customlogouthandler.services.UserCache;
import com.baeldung.customlogouthandler.user.UserUtils;
@Service
public class CustomLogoutHandler implements LogoutHandler {
private final UserCache userCache;
public CustomLogoutHandler(UserCache userCache) {
this.userCache = userCache;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String userName = UserUtils.getAuthenticatedUserName();
userCache.evictUser(userName);
}
}

View File

@ -0,0 +1,30 @@
package com.baeldung.customlogouthandler.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.baeldung.customlogouthandler.services.UserCache;
import com.baeldung.customlogouthandler.user.User;
import com.baeldung.customlogouthandler.user.UserUtils;
@Controller
@RequestMapping(path = "/user")
public class UserController {
private final UserCache userCache;
public UserController(UserCache userCache) {
this.userCache = userCache;
}
@GetMapping(path = "/language")
@ResponseBody
public String getLanguage() {
String userName = UserUtils.getAuthenticatedUserName();
User user = userCache.getByUserName(userName);
return user.getLanguage();
}
}

View File

@ -0,0 +1,5 @@
spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=test
spring.datasource.password=test
spring.jpa.hibernate.ddl-auto=create

View File

@ -0,0 +1,108 @@
package com.baeldung.customlogouthandler;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlGroup;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.baeldung.customlogouthandler.services.UserCache;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { CustomLogoutApplication.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@SqlGroup({ @Sql(value = "classpath:customlogouthandler/before.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), @Sql(value = "classpath:customlogouthandler/after.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) })
@TestPropertySource(locations="classpath:customlogouthandler/application.properties")
class CustomLogoutHandlerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserCache userCache;
@LocalServerPort
private int port;
@Test
public void whenLogin_thenUseUserCache() {
// User cache should be empty on start
assertThat(userCache.size()).isEqualTo(0);
// Request using first login
ResponseEntity<String> response = restTemplate.withBasicAuth("user", "pass")
.getForEntity(getLanguageUrl(), String.class);
assertThat(response.getBody()).contains("english");
// User cache must contain the user
assertThat(userCache.size()).isEqualTo(1);
// Getting the session cookie
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Cookie", response.getHeaders()
.getFirst(HttpHeaders.SET_COOKIE));
// Request with the session cookie
response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET, new HttpEntity<String>(requestHeaders), String.class);
assertThat(response.getBody()).contains("english");
// Logging out using the session cookies
response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET, new HttpEntity<String>(requestHeaders), String.class);
assertThat(response.getStatusCode()
.value()).isEqualTo(200);
}
@Test
public void whenLogout_thenCacheIsEmpty() {
// User cache should be empty on start
assertThat(userCache.size()).isEqualTo(0);
// Request using first login
ResponseEntity<String> response = restTemplate.withBasicAuth("user", "pass")
.getForEntity(getLanguageUrl(), String.class);
assertThat(response.getBody()).contains("english");
// User cache must contain the user
assertThat(userCache.size()).isEqualTo(1);
// Getting the session cookie
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Cookie", response.getHeaders()
.getFirst(HttpHeaders.SET_COOKIE));
// Logging out using the session cookies
response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET, new HttpEntity<String>(requestHeaders), String.class);
assertThat(response.getStatusCode()
.value()).isEqualTo(200);
// User cache must be empty now
// this is the reaction on custom logout filter execution
assertThat(userCache.size()).isEqualTo(0);
// Assert unauthorized request
response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET, new HttpEntity<String>(requestHeaders), String.class);
assertThat(response.getStatusCode()
.value()).isEqualTo(401);
}
private String getLanguageUrl() {
return "http://localhost:" + port + "/user/language";
}
private String getLogoutUrl() {
return "http://localhost:" + port + "/user/logout";
}
}

View File

@ -0,0 +1,5 @@
spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=test
spring.datasource.password=test
spring.jpa.hibernate.ddl-auto=create

View File

@ -0,0 +1 @@
insert into users (login, password, role, language) values ('user', '{noop}pass', 'ROLE_USER', 'english');