Open TokiNoviceProgrammer opened 2 weeks ago
ユーザーがページを離れる前に /logout エンドポイントにログアウトリクエストを送信
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Logout on Tab Close</title>
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<script>
window.addEventListener("beforeunload", function (event) {
var csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
var csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
var headers = {};
headers[csrfHeader] = csrfToken;
var formData = new FormData();
formData.append(csrfHeader, csrfToken);
navigator.sendBeacon("/logout", formData);
});
</script>
</head>
<body>
<h1>Welcome to the Application</h1>
</body>
</html>
window.addEventListener("beforeunload", function (event) {
var csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
var csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
var xhr = new XMLHttpRequest();
xhr.open("POST", "/logout", false); // 同期リクエスト
xhr.setRequestHeader(csrfHeader, csrfToken);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send();
});
window.addEventListener("beforeunload", function (event) {
var csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
var csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
fetch('/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: encodeURIComponent(csrfHeader) + '=' + encodeURIComponent(csrfToken)
}).then(response => {
if (response.ok) {
console.log('Logged out successfully');
} else {
console.error('Logout failed');
}
}).catch(error => {
console.error('Error:', error);
});
});
window.addEventListener("beforeunload", function (event) {
var csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
var csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
var headers = {};
headers[csrfHeader] = csrfToken;
var data = new URLSearchParams();
data.append(csrfHeader, csrfToken);
var blob = new Blob([data.toString()], { type: 'application/x-www-form-urlencoded' });
navigator.sendBeacon("/test/logout", blob);
});
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.session.SessionRegistry; import org.springframework.security.web.session.SessionRegistryImpl;
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.maximumSessions(1)
.sessionRegistry(sessionRegistry());
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
window.onload = function() { if (localStorage.getItem('tabOpened')) { alert('既にこのサイトの他のタブが開かれています。'); window.location.href = 'about:blank'; } else { localStorage.setItem('tabOpened', 'true'); }
window.onbeforeunload = function() {
localStorage.removeItem('tabOpened');
}
};
import org.springframework.security.web.csrf.CsrfToken; import org.springframework.stereotype.Component; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.IOException;
@Component public class CsrfTokenFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpSession session = httpServletRequest.getSession(false);
if (session != null) {
CsrfToken csrfToken = (CsrfToken) httpServletRequest.getAttribute(CsrfToken.class.getName());
if (csrfToken != null) {
String currentToken = csrfToken.getToken();
String previousToken = (String) session.getAttribute("previousCsrfToken");
// セッションに保存されたトークンと一致しない場合はエラーをスロー
if (previousToken != null && !previousToken.equals(currentToken)) {
throw new ServletException("Invalid CSRF token. Possible multiple tab usage detected.");
}
// セッションに現在のトークンを保存
session.setAttribute("previousCsrfToken", currentToken);
}
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.csrf.CsrfFilter;
@Configuration @Order(1) public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CsrfTokenFilter csrfTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterAfter(csrfTokenFilter, CsrfFilter.class);
}
}
import java.util.UUID;
public class TokenGenerator { public static String generateToken() { return UUID.randomUUID().toString(); }
public static void main(String[] args) {
String token = generateToken();
System.out.println("Generated Token: " + token);
}
}
import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
@Component public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 画面からトークンを取得
String requestToken = request.getParameter("token");
// ここで、ユーザーの認証情報からトークンを取得する(例:セッション、DB、等)
String userToken = getUserTokenFromSessionOrDatabase();
// トークンが一致するか確認
if (requestToken == null || !requestToken.equals(userToken)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return false;
}
return true;
}
private String getUserTokenFromSessionOrDatabase() {
// 実際の実装では、セッションやデータベースからトークンを取得します
// ここでは仮のトークンを返す
return "expectedToken";
}
}
import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession;
@Component public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// トークンを生成しセッションに保存
HttpSession session = request.getSession();
String token = TokenUtil.generateToken();
session.setAttribute("token", token);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// モデルにトークンを追加
if (modelAndView != null) {
HttpSession session = request.getSession();
String token = (String) session.getAttribute("token");
modelAndView.addObject("token", token);
}
}
}
import java.util.UUID;
public class TokenUtil { public static String generateToken() { return UUID.randomUUID().toString(); } }
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam;
@Controller public class MyController {
@GetMapping("/showForm")
public String showForm(Model model) {
model.addAttribute("message", "Hello, Thymeleaf!");
return "form"; // form.html というテンプレートを返す
}
@PostMapping("/submitForm")
public String submitForm(@RequestParam String name, Model model) {
// ModelをModelMapにキャスト
if (model instanceof ModelMap) {
ModelMap modelMap = (ModelMap) model;
// ModelMapから値を取得
String message = (String) modelMap.get("message");
// 値を使って何か処理を行う(例としてコンソールに出力)
System.out.println("Message: " + message);
System.out.println("Name: " + name);
// 処理結果を再度Modelに追加してビューに渡す
model.addAttribute("result", "Form submitted successfully!");
}
return "result"; // result.html というテンプレートを返す
}
}
https://spring.pleiades.io/spring-security/reference/servlet/authentication/session-management.html
maximumSessions(1)
の場合、同一ユーザーが複数回ログインすることが防止される。2 回目のログインにより、最初のログインが無効になる。maximumSessions(1).maxSessionsPreventsLogin(true)
の場合、2 回目のログインは拒否される。