Przeglądaj źródła

1.添加一个提供给 SecurityContextHolderFilter 使用的 SecurityContextRepository 实现
2.WebSecurityConfig 配置 SessionManagementFilter 不使用 ChangeSessionIdAuthenticationStrategy

reghao 3 miesięcy temu
rodzic
commit
95f6f2fa3d

+ 10 - 3
web/src/main/java/cn/reghao/bnt/web/admin/security/WebSecurityConfig.java

@@ -3,6 +3,7 @@ package cn.reghao.bnt.web.admin.security;
 import cn.reghao.bnt.web.admin.security.filter.LoginRedirectFilter;
 import cn.reghao.bnt.web.admin.security.form.AccountAuthFilter;
 import cn.reghao.bnt.web.admin.security.form.AccountAuthProvider;
+import cn.reghao.bnt.web.admin.security.session.MySecurityContextRepository;
 import cn.reghao.bnt.web.admin.security.session.MySessionAuthenticationStrategy;
 import cn.reghao.bnt.web.admin.service.AccountLoginService;
 import cn.reghao.bnt.web.admin.service.MenuService;
@@ -52,11 +53,13 @@ public class WebSecurityConfig {
     private final LogoutSuccessHandler logoutSuccessHandler;
     private final SessionRegistry sessionRegistry;
     private final MenuService menuService;
+    private final MySecurityContextRepository mySecurityContextRepository;
 
     public WebSecurityConfig(AccountAuthProvider userAuthProvider, AccountLoginService accountLoginService,
                              AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler,
                              LogoutHandler logoutHandler, LogoutSuccessHandler logoutSuccessHandler,
-                             SessionRegistry sessionRegistry, MenuService menuService) {
+                             SessionRegistry sessionRegistry, MenuService menuService,
+                             MySecurityContextRepository mySecurityContextRepository) {
         this.userAuthProvider = userAuthProvider;
         this.accountLoginService = accountLoginService;
         this.successHandler = successHandler;
@@ -65,6 +68,7 @@ public class WebSecurityConfig {
         this.logoutSuccessHandler = logoutSuccessHandler;
         this.sessionRegistry = sessionRegistry;
         this.menuService = menuService;
+        this.mySecurityContextRepository = mySecurityContextRepository;
     }
 
     @Bean
@@ -114,6 +118,7 @@ public class WebSecurityConfig {
                     return new AuthorizationDecision(false);
                 }))
                 //.securityContext((context) -> context.securityContextRepository(new HttpSessionSecurityContextRepository()))
+                .securityContext((context) -> context.securityContextRepository(mySecurityContextRepository))
                 .securityContext((securityContext) -> securityContext.requireExplicitSave(true))
                 .addFilterAfter(new LoginRedirectFilter(), SecurityContextHolderFilter.class)
                 .addFilterBefore(accountAuthFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class)
@@ -122,7 +127,9 @@ public class WebSecurityConfig {
                 .and()
                 .exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint())
                 .and()
-                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
+                .sessionManagement()
+                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
+                .sessionFixation().none() // SessionManagementFilter 中禁用 ChangeSessionIdAuthenticationStrategy(会话固定攻击防护)
                 .and()
                 .rememberMe().disable()
                 .cors().disable()
@@ -257,7 +264,7 @@ public class WebSecurityConfig {
 
         List<SessionAuthenticationStrategy> authenticationStrategies = new ArrayList<>();
         authenticationStrategies.add(concurrentSessionControlAuthenticationStrategy);
-        // 会话固定护, 认证成功后 session id 会被修改
+        // 会话固定攻击防护, 认证成功后 session id 会被修改
         authenticationStrategies.add(new SessionFixationProtectionStrategy());
         // 认证成功后将认证信息注册到 SessionRegistry
         authenticationStrategies.add(new RegisterSessionAuthenticationStrategy(sessionRegistry));

+ 22 - 1
web/src/main/java/cn/reghao/bnt/web/admin/security/handler/AuthSuccessHandlerImpl.java

@@ -8,6 +8,7 @@ import cn.reghao.bnt.web.admin.service.AccountService;
 import cn.reghao.bnt.web.admin.service.LoginAttemptService;
 import cn.reghao.jutil.jdk.web.result.WebResult;
 import cn.reghao.jutil.web.ServletUtil;
+import jakarta.servlet.http.Cookie;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.savedrequest.SavedRequest;
@@ -51,7 +52,7 @@ public class AuthSuccessHandlerImpl implements AuthenticationSuccessHandler {
         int loginType = accountAuthToken.getLoginType();
         int plat = accountAuthToken.getPlat();
         boolean rememberMe = accountAuthToken.isRememberMe();
-        long timeout = rememberMe ? 3600*24*30 : 0;
+        //long timeout = rememberMe ? 3600*24*30 : 0;
         String uri = ServletUtil.getRequest().getRequestURI();
         if (uri.startsWith("/oauth/redirect")) {
             // 使用第三方帐号的 oauth 方式登入
@@ -60,6 +61,13 @@ public class AuthSuccessHandlerImpl implements AuthenticationSuccessHandler {
             return;*/
         }
 
+        String domain = request.getHeader("host");
+        String cookieName = "USERDATA";
+        String value = sessionId;
+        int sessionTimeout = 3600*24*7;
+        Cookie cookie = generateCookie(domain, cookieName, value, sessionTimeout);
+        //response.addCookie(cookie);
+
         AccountInfo accountInfo = accountService.getAccountInfo();
         AccountToken accountToken = new AccountToken(sessionId);
         AccountLoginRet accountLoginRet = new AccountLoginRet(accountInfo, accountToken, redirectPath);
@@ -91,4 +99,17 @@ public class AuthSuccessHandlerImpl implements AuthenticationSuccessHandler {
         PrintWriter printWriter = response.getWriter();
         printWriter.write(body);
     }
+
+    private Cookie generateCookie(String domain, String name, String value, long timeout) {
+        String path = "/";
+        Cookie cookie = new Cookie(name, value);
+        if (timeout != 0) {
+            cookie.setMaxAge((int) timeout);
+        }
+        cookie.setDomain(domain);
+        //cookie.setSecure(true);
+        cookie.setSecure(false);
+        cookie.setPath(path);
+        return cookie;
+    }
 }

+ 60 - 0
web/src/main/java/cn/reghao/bnt/web/admin/security/session/MyDeferredSecurityContext.java

@@ -0,0 +1,60 @@
+package cn.reghao.bnt.web.admin.security.session;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.core.context.DeferredSecurityContext;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+
+import java.util.function.Supplier;
+
+/**
+ * org.springframework.security.web.context.SupplierDeferredSecurityContext 的复制品,解决其不可见问题
+ *
+ * @author reghao
+ * @date 2025-12-12 10:14:33
+ */
+public class MyDeferredSecurityContext implements DeferredSecurityContext {
+    private static final Log logger = LogFactory.getLog(MyDeferredSecurityContext.class);
+
+    private final Supplier<SecurityContext> supplier;
+
+    private final SecurityContextHolderStrategy strategy;
+
+    private SecurityContext securityContext;
+
+    private boolean missingContext;
+
+    MyDeferredSecurityContext(Supplier<SecurityContext> supplier, SecurityContextHolderStrategy strategy) {
+        this.supplier = supplier;
+        this.strategy = strategy;
+    }
+
+    @Override
+    public SecurityContext get() {
+        init();
+        return this.securityContext;
+    }
+
+    @Override
+    public boolean isGenerated() {
+        init();
+        return this.missingContext;
+    }
+
+    private void init() {
+        if (this.securityContext != null) {
+            return;
+        }
+
+        this.securityContext = this.supplier.get();
+        this.missingContext = (this.securityContext == null);
+        if (this.missingContext) {
+            this.securityContext = this.strategy.createEmptyContext();
+            if (logger.isTraceEnabled()) {
+                logger.trace(LogMessage.format("Created %s", this.securityContext));
+            }
+        }
+    }
+}

+ 92 - 0
web/src/main/java/cn/reghao/bnt/web/admin/security/session/MySecurityContextRepository.java

@@ -0,0 +1,92 @@
+package cn.reghao.bnt.web.admin.security.session;
+
+import cn.reghao.bnt.web.admin.security.form.AccountAuthToken;
+import cn.reghao.jutil.web.ServletUtil;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.security.core.context.DeferredSecurityContext;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.web.context.HttpRequestResponseHolder;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.session.Session;
+import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
+import org.springframework.stereotype.Component;
+
+import java.util.function.Supplier;
+
+/**
+ * 参考 org.springframework.security.web.context.HttpSessionSecurityContextRepository
+ * 提供给 SecurityContextHolderFilter 使用的 SecurityContextRepository 实现
+ *
+ * @author reghao
+ * @date 2025-12-12 10:20:17
+ */
+@Component
+public class MySecurityContextRepository implements SecurityContextRepository {
+    private final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
+    private final String cookieName = "USERDATA";
+    private final JdbcIndexedSessionRepository sessionRepository;
+    private final SecurityContextHolderStrategy securityContextHolderStrategy;
+
+    public MySecurityContextRepository(JdbcIndexedSessionRepository sessionRepository) {
+        this.sessionRepository = sessionRepository;
+        this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
+    }
+
+    // SecurityContextHolderFilter 不会调用这个方法
+    @Override
+    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
+        return SecurityContextHolder.createEmptyContext();
+    }
+
+    @Override
+    public DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
+        Supplier<SecurityContext> supplier = () -> readSecurityContextFromSession(request.getSession(false));
+        return new MyDeferredSecurityContext(supplier, this.securityContextHolderStrategy);
+    }
+
+    private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
+        if (httpSession == null) {
+            return null;
+        }
+
+        /*Cookie cookie = ServletUtil.getCookie1(cookieName, request);
+        if (cookie != null) {
+            String sessionId = cookie.getValue();
+            Session session = sessionRepository.findById(sessionId);
+            if (session != null) {
+                Object object = session.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
+                if (object instanceof SecurityContext securityContext) {
+                    AccountAuthToken authToken = (AccountAuthToken) securityContext.getAuthentication();
+                }
+            }
+        }*/
+
+        // Session exists, so try to obtain a context from it.
+        Object contextFromSession = httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
+        if (contextFromSession == null) {
+            return null;
+        }
+
+        // We now have the security context object from the session.
+        if (!(contextFromSession instanceof SecurityContext)) {
+            return null;
+        }
+
+        // Everything OK. The only non-null return from this method.
+        return (SecurityContext) contextFromSession;
+    }
+
+    @Override
+    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
+    }
+
+    @Override
+    public boolean containsContext(HttpServletRequest request) {
+        return false;
+    }
+}

+ 23 - 0
web/src/main/java/cn/reghao/bnt/web/admin/security/session/SecuritySessionConfig.java

@@ -7,6 +7,8 @@ import org.springframework.security.web.session.ConcurrentSessionFilter;
 import org.springframework.security.web.session.SessionInformationExpiredStrategy;
 import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
 import org.springframework.session.security.SpringSessionBackedSessionRegistry;
+import org.springframework.session.web.http.CookieSerializer;
+import org.springframework.session.web.http.DefaultCookieSerializer;
 
 /**
  * spring-security 中涉及 session 处理的相关配置
@@ -53,4 +55,25 @@ public class SecuritySessionConfig {
                                                            SessionInformationExpiredStrategy sessionExpiredStrategy){
         return new ConcurrentSessionFilter(sessionRegistry, sessionExpiredStrategy);
     }
+
+    /**
+     * 自定义 spring session 的 cookie 名字, 默认名是 SESSION
+     * DefaultCookieSerializer 中会对 sessionId 进行 base64 编码
+     *
+     * @param
+     * @return
+     * @date 2023-08-02 09:08:08
+     */
+    @Bean
+    public CookieSerializer cookieSerializer() {
+        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
+        serializer.setCookieName("SESSDATA");
+        serializer.setCookiePath("/");
+        // domain 设置为一级域名
+        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
+        //serializer.setSameSite(null);
+        // 设置 cookie 有效期为 7 天
+        serializer.setCookieMaxAge(3600*24*7);
+        return serializer;
+    }
 }