SpringSecurity入门和架构的学习日志

我是用官方文档学习的

SpringSecurity官方文档

孩子还是大一的小凳,对很多底层理解的不深,博客主要是记录学习日志,可能讲得不够好,望海涵

概述

Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。总之,Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。

 

Servlet的应用使用SpringSecurity

入门

在maven中使用SpringSecurity首先肯定是要引入依赖

<dependencies>
  <!-- ... 其他依赖元素 ... -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>
</dependencies>
由于Spring Boot提供了一个Maven BOM来管理依赖版本,所以你不需要指定一个版本。
如果你想覆盖Spring Security的版本,你可以通过提供一个Maven属性来实现
<properties>
	<!-- ... -->
	<spring-security.version>6.2.0-SNAPSHOT</spring-security.version>
</properties>

如果使用较新的SS的版本,可能springboot也得改成新版本
<properties>
	<!-- ... -->
	<spring.version>6.1.0-M2</spring.version>
</properties>

 

当引入好依赖后,启动springboot项目

如果日志有出现

$ ./mvnw spring-boot:run
...
INFO 23689 --- [  restartedMain] .s.s.UserDetailsServiceAutoConfiguration :

Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336

...

那么说明SpringSecurity启动成功了

 

 

这个时候如果去访问该项目的某个endpoint时,就会401 unauthorized了

如果在浏览器里面访问那就会重定向到一个默认的登录页面,这个登录页面的信息在静态资源那里

就好像

 

这个是现在在做的项目里面的实际的一种情况

 

SpringSecurity在启动时默认会启动以下的一些功能

 

 

Springboot在对SpringSecurity的启动时会有很多自动配置

在我们自己没有去配置SpringSecurtiy的配置类的时候,会启动其内部自定义的SS配置

@EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {
    @Bean
    @ConditionalOnMissingBean(UserDetailsService.class)
    InMemoryUserDetailsManager inMemoryUserDetailsManager() {
        String generatedPassword = // ...;
        return new InMemoryUserDetailsManager(User.withUsername("user")
                                              .password(generatedPassword).roles("ROLE_USER").build());
    }

    @Bean
    @ConditionalOnMissingBean(AuthenticationEventPublisher.class)
    DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher(ApplicationEventPublisher delegate) {
        return new DefaultAuthenticationEventPublisher(delegate);
    }
}

 

实际开发中使用SpringSecurity的代码例子

package com.example.securitydemo.config;

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults; // for httpBasic and formLogin defaults

@Configuration
@EnableWebSecurity // 显式启用 Web 安全性,尽管在 Spring Boot 中定义 SecurityFilterChain bean 时通常会自动应用
public class WebSecurityConfig {

    // 3.1 定义密码编码器 (非常重要!)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 3.2 使用内存中的用户进行认证 (用于演示)
    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user = User.builder()
                .username("user")
                .password(passwordEncoder.encode("password")) // 密码必须加密
                .roles("USER")
                .build();
        UserDetails admin = User.builder()
                .username("admin")
                .password(passwordEncoder.encode("adminpassword"))
                .roles("ADMIN", "USER") // admin 拥有 ADMIN 和 USER 角色
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }
    //UserDetails是spring security用来对用户进行身份验证和授权的接口,
    //可以在里面定义用户授权验证的相关信息,也可以从数据库里面获取用户信息封装在这里面
    //为后续的验证授权功能服务
    // 3.3 配置 HTTP 安全规则
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/home", "/css/**", "/js/**", "/images/**", "/public/**").permitAll() // 3.3.1 允许公共访问的路径
                .requestMatchers("/admin/**").hasRole("ADMIN") // 3.3.2 只有 ADMIN 角色的用户才能访问 /admin/**
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // USER 或 ADMIN 角色可以访问 /user/**
                .anyRequest().authenticated() // 3.3.3 其他所有请求都需要认证
            )
            .formLogin(formLogin -> formLogin // 3.3.4 配置表单登录
                .loginPage("/login") // 3.3.5 指定自定义登录页面的路径 (如果需要)
                .permitAll() // 登录页面本身需要允许所有用户访问
                .defaultSuccessUrl("/hello", true) // 登录成功后的默认跳转页面
            )
            .logout(logout -> logout // 3.3.6 配置登出
                .logoutUrl("/perform_logout") // 自定义登出URL
                .logoutSuccessUrl("/login?logout") // 登出成功后跳转的页面
                .permitAll()
            )
            .httpBasic(withDefaults()); // 启用 HTTP Basic 认证,使用默认配置

        return http.build();
    }
}

 

实际开发中肯定是从数据库查询数据来进行认证的

架构

从web请求到servlet之前通常需要经过一系列的filter的处理

而Servlet和Filter是Web容器如Tomcat直接管理的对象,他们不直接感知Spring容器的存在。而他们的生命周期是通过Web容器控制的。

如果希望在Filter里面使用Spring提供的服务,如使用bean,那就需要借助DelegatingFilterProxy来解决了。

当请求符合DelegatingFilterProxy时,它本身不执行过滤逻辑,相反地它会从Spring的WebApplicationContext中寻找DelegatingFilterProxy声明的filter-name对应的Filter Bean,然后执行do Filter操作

 

 

FilterChainProxy简单说就是Security所有Filter的链总入口,同时作为一个Bean Filter存在

而SecurityFilterChain是实现SpringSecurity一系列功能的链,FilterChainProxy会把任务委托给一个特定的SecurityFilterChain

 

 

进一步的,SecurityFilterChain被FilterChainProxy用来确定当前请求应该调用哪些SecurityFilter实例

 

 

 

 

如果单单使用DelegatingFilterProxy来调用SecurityFilter,那么可以要有很多的DelegatingFilterProxy来满足需求,所有可以用FilterChainProxy来放多个SecurityFilterChain,以FilterChainProxy为入口,并且通过它来选择用哪个SecurityFilterChain

 

 

Security Filter是通过SecurityFilterChain的API来插入到FilterChainProxy中间的

而这些Filter可以有多种不同的功能要求,如认证授权漏洞保护什么的,可以按照特定的顺序执行

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults())//CSRFFIlter
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )//UsernamePasswordAUthenticationFilter
            .httpBasic(Customizer.withDefaults())//BasicAuthenticationFilter
            .formLogin(Customizer.withDefaults());//AuthorizationFilter
        return http.build();
    }

}

 

 

 

那么如何添加自定义的Filter到FilterChain呢

在官方文档里面说默认的securityFilter足以满足应用程序的安全,但是也可以自定义一个filter去实现如

想添加一个 Filter,获得一个租户 id header 并检查当前用户是否有访问该租户的权限。

前面的描述已经给了我们一个添加 filter 的线索,因为我们需要知道当前的用户,所以我们需要在认证 filter 之后添加它

 

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;
//也可也从OncePerRequestFilter中继承
public class TenantFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String tenantId = request.getHeader("X-Tenant-Id");
        boolean hasAccess = isUserAllowed(tenantId);
        if (hasAccess) {
            filterChain.doFilter(request, response);
            return;
        }
        throw new AccessDeniedException("Access denied");
    }

}


//接下来需要将其添加到securityfilterchain中
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
    return http.build();
}

进一步地,由于把filter声明为bean,并且在springsecurity中也有调用,难免会有一些问题,可以通过依赖注入避免重复调用

@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
    FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}

这样就可以避免springboot向容器注册它导致重复调用了

 

处理Security异常

ExceptionTranslationFilter可以将AccessDeniedException和AuthenticationException翻译衬托Http响应

并且这个FIlter作为SecurityFilter之一插入到FilterChainProxy之中

 

当进入ExceptionTranslationFilter,会调用FilterChain.doFIlter调用其他部分,看看是Authentication还是Access Denied

如果是前者,就会把SecurityContextHolder清理掉,如何把HttpServletRequest缓存起来,认证成功时使用

AuthenticationEntryPoint用于请求客户的凭证,可以重定向到一个登录页面或者发送一个WWW-Authenticate头

如果时AccessDenied就会调用AccessDeniedHandler拒绝访问

 

如果没有抛出这俩个错误就不会执行ExceptionTranslationFilter

 

RequestCache

HttpServletRequest被保存在RequestCache里面,当用户成功认证后从中拿取请求

 

RequestCacheAwareFilter就是用RequestCache来保存HttpServletRequest的

 

如何定制RequestCache的实现呢

@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
	HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
	requestCache.setMatchingRequestParameterName("continue");
	http
		// ...
		.requestCache((cache) -> cache
			.requestCache(requestCache)
		);
	return http.build();
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值