Spring Security

Spring Security#

1.概述#

1.1 简介#

官网的说明

Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first class support for securing both imperative and reactive applications, it is the de-facto standard for securing Spring-based applications.

直译:

Spring Security 是一个提供身份验证授权防止常见攻击的框架。凭借对保护命令式反应式应用程序的一流支持,它是保护基于 Spring 的应用程序的事实标准。

1.2 获取 SpringSecurity#

<!-- SpringBoot 版本-->
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-security</artifactId>
	</dependency>
</dependencies>
<!-- Spring 版本-->
<dependencies>
	<dependency>
		<groupId>org.springframework.security</groupId>
		<artifactId>spring-security-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.security</groupId>
		<artifactId>spring-security-config</artifactId>
	</dependency>
</dependencies>

1.3 相关组件#

SpringSecurity 由许多的组件组成,权限模块,密码加密,认证模块,各种各样的 Filter…

1.3.1 passwordEncoder

密码加密器,常用的 4 种实现,其实只是 3 种。DelegatingPasswordEncoder 是 SpringSecurity 为了兼容以前老版本的编码方式来实现的一个类似于 PasswordEncoder 容器的东西,用来存放一些 PasswordEncoder,在不同的场景下直接通过 DelegatingPasswordEncoder 来使用

/*
DelegatingPasswordEncoder 加密容器
BCryptPasswordEncoder 常用的,三种前缀模式,自定义加密次数,4-31 越高越慢
Pbkdf2PasswordEncoder 使用 PBKDF2 的PasswordEncoder实现,具有可配置的迭代次数和随机的 8 字节随机盐值。
SCryptPasswordEncoder Scrypt 基于 Salsa20,它在 Java 中表现不佳(与 AES 相当),但在支持 SIMD 的平台上表现出色(快 4-5 倍)
*/
// 接口
public interface PasswordEncoder {
    // 加密的方法
	String encode(CharSequence rawPassword);
    // 密码匹配
	boolean matches(CharSequence rawPassword, String encodedPassword);
    // 如果为了更好的安全性应该再次对编码的密码进行编码,则返回 true
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}
// 使用
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance()); // 已经弃用了,这个和原密码相当
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder()); // 已经弃用了可以看看 Pbkdf2PasswordEncoder
// 根据 Key 获取一个 BCryptPasswordEncoder 加密的 Encoder
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
1.3.2 ExceptionTranslationFilter

当过滤器链执行到这个地方的时候,发现拥有 AuthenticationException 异常,于是开始执行认证相关的流程

  1. SecurityHolder 被清除
  2. Request 保存在 RequestCache 中,当认证成功就设置回去
  3. AuthenticationEntryPoint 用于从客户端请求认证凭据,可能会重定向到登陆页面或者是携带一些其他的请求标头

如果是 AccessDeniedException 异常,直接拒绝掉

如果不是这两种异常或者他们的子异常,那么该过滤器没有任何的操作直接将异常抛出就没了

认证相关组件

1.3.3 SecurityContextHolder

SecurityContextHolder 有三种存放 SecurityContext 的策略(其实相当于存放用户身份信息的策略),

  1. MODE_THREADLOCAL: 每个线程独有一份,在 ThreadLocal 里面

    1. 补充一下,ThreadLocal 是每个线程独享的,子线程是拿不到当前线程的 ThreadLocal 的
  2. MODE_INHERITABLETHREADLOCAL: 可以被子类拿到的 ThreadLocal 中

    1. 这个相关的实现可以去看看 InheritableThreadLocal 这块的设计思路
  3. MODE_GLOBAL: 全局的

其他的都是一些对 SecurityContext 和存放策略的 getter 和 setter 相关方法以及避免内存泄漏的清除方法

1.3.4 SecurityContext

对 Authentication(用户身份信息) 的 getter 和 setter

1.3.5 Authentication

内置一些获取用户身份信息的方法(这个类来自于 SecurityContext):

1. Collection<? extends GrantedAuthority> getAuthorities(); // 用户权限
2. Object getCredentials(); // 用户身份信息通常是 password 
3. Object getDetails(); // 用户的凭据,可能是一个 token 证书 IP 等信息
4. Object getPrincipal(); // 用户名
5. boolean isAuthenticated(); // 是否认证了
6. void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException // 变更认证状态
	
1.3.6 GrantedAuthority

权限相关类,其实就是一个 String 的权限信息,一般对应数据库的 Role 表里面的数据

1.3.7 AuthenticationManager && AuthenticationProvider

内置一个身份认证的方法

Authentication authenticate(Authentication authentication) throws AuthenticationException;

有一个默认的实现 ProviderManager 它实现的 authenticate 方法内会遍历所有的 AuthenticationProvider 调用他们的 support 方法去检验当前 Authentication 是否能够被支持,如果能就执行 AuthenticationProvider 的 authenticate 方法。如果都不能就会尝试让其他的 AuthenticationManager 去尝试,如果他们也不行就会报错 ProviderNotFoundException 里面的异常抛出流程因为还会去调用其他的 AuthenticationManager 来尝试认证,还挺复杂的大伙儿可以去看看

1.3.8 AuthenticationEntryPoint

用于发送从客户端请求凭据的 HTTP 响应

未认证 或者 认证失败 的时候会 AuthenticationEntryPoint 会设置一个 WWW-Authenticate 请求标头到响应中去,当客户端拿到了这个响应头就知道当前认证失败了,于是就重定向到登录页面去

当然除了这种方式其内有一种实现是基于 重定向 URILoginUrlAuthenticationEntryPoint,它会去拼接一个 URL 作为认证失败的响应重定向 URL

1.3.9 AbstractAuthenticationProcessingFilter

认证的 Filter,认证的流程就是从它开始的。主要实现通过客户端传递的相关信息,生成一个 Authentication ,如果认证失败走 unsuccessfulAuthentication 方法,成功走 successfulAuthentication 方法

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    // 包装一下 请求和响应
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;
    // 判断当前请求是否需要认证
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);

        return;
    }

    if (logger.isDebugEnabled()) {
        logger.debug("Request is to process authentication");
    }

    Authentication authResult;

    try {
        // 当前类是一个抽象类,并没有定义 attemptAuthentication 方法,而是交给子类去实现
        // 这个地方可以去看 UsernamePasswordAuthenticationFilter 实现的逻辑,反正最后生成一个 Authentication
        // 如果认证失败,抛出异常,开始执行 unsuccessfulAuthentication
        // 认证成功前如果还有一些逻辑还会执行过滤器链
        authResult = attemptAuthentication(request, response); 
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    catch (InternalAuthenticationServiceException failed) {
        logger.error("An internal error occurred while trying to authenticate the user.",failed);
        unsuccessfulAuthentication(request, response, failed);
        return;
    }
    catch (AuthenticationException failed) {
        // 可以自定义实现这个方法实现认证失败的逻辑
        unsuccessfulAuthentication(request, response, failed);
        return;
    }

    // Authentication success
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }
    /**
    认证成功的相关流程,当前类的实现方式是将这个 Authentication 直接放进 SecurityContext 中,方便后续使用
    后续还回去执行 rememberMe 相关的逻辑 设置 Session 什么的 懂吧... 这块做过登陆的应该都知道
    */
    successfulAuthentication(request, response, chain, authResult);
    
}
// 我还是贴一下 UsernamePasswordAuthenticationFilter 的实现吧,等会儿说我送佛送不到西
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    // 这个地方是可以配置的! postOnly 默认是 true
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
    }
	// 从请求参数里面拿到 username 和 password 默认的参数名称就是这两个变量的 String
    String username = obtainUsername(request);
    String password = obtainPassword(request);

    if (username == null) {
        username = "";
    }

    if (password == null) {
        password = "";
    }
	// 去掉两端的空字符
    username = username.trim();
	// 创建一个 UsernamePasswordAuthenticationToken 这个是 Authentication 的实现
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);
 
    /** 这个地方的实现看这个 WebAuthenticationDetails 类
    public WebAuthenticationDetails(HttpServletRequest request) {
		this.remoteAddress = request.getRemoteAddr(); 设置 IP
		HttpSession session = request.getSession(false); 有 Session 就设置 Session 没有就 Null
		this.sessionId = (session != null) ? session.getId() : null;
	}
    */
    setDetails(request, authRequest);
	// 最后获取 AuthenticationManager 去认证
    return this.getAuthenticationManager().authenticate(authRequest);
}

最后成功了就会执行 AuthenticationSuccessHandler 的 onAuthenticationSuccess 方法

失败了就会执行 AuthenticationFailureHandler 的 onAuthenticationFailure 方法

1.4 Spring Boot 的集成#

startGetting
  1. Spring Boot 的集成简化了 Spring Security 的开发,建立一个 Servlet 级别的 过滤器链 springSecurityFilterChain 来实现 Spring Security 相关的安全业务逻辑(保护应用程序 URL、验证提交的用户名和密码、重定向到登录表单等)
  2. 任意一个用户登录都会拥有一个对应的 UserDetailsService 可以获取用户的相关信息

默认的配置

  1. 想要进行正常的使用,需要对身份进行认证,Spring Security 自己自带了一个简单的表单认证服务

  2. 启动的时候在终端会显示默认的用户名密码,也可以在 application.yml 里面进行相关配置

  3. 使用 BCrypt 作为默认的加密器

  4. 拥有 Logout 相关功能

  5. 防止 CSRF 攻击

  6. Session 相关的保护策略

    1. Spring Security 会修改请求头和响应头,里面携带 SpringSecurity 相关的标记
    2. 也有关于 XSS 的集成
  7. Servlet 相关的 API 集成,用于获取用户的相关权限和登录状态

Architecture

Filter 相关的知识就当作大家都知道了。

SpringSecurity 是一条 Filter Chain,单个 Request 在进入 SpringMVC 提供的 DispatcherServlet 之前,就会执行到 Filter 相关的逻辑,每个 Filter 可能会对当前的请求进行包装,然后传递给下游的 Filter 或者是 Servlet 所以 Filter 的顺序十分重要

Spring 提供了一个 Bean 级别的 Filter 代理 —– DelegatingFilterProxy 它存在 Servlet 的生命周期中,也拥有 Spring 提供的 ApplicationContext。它内部可以定义一条 Filter chain 来对某一个 Bean 的行为做过滤 内部的方法叫做

invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)

上文提及的 Spring Security 就是在这个地方,对 Spring 的 ApplicationContext 对象进行包装,放入一些能够拿到当前登录用户的个人信息的 SecurityContext 对象。随后在过过滤器链回来的时候,完美解决了 内存泄露问题(在此将 SecurityContext 对象给释放掉)

同样的 Spring Security 就是定义了一个叫做 FilterChainProxy 的 Bean 来实现和 DelegatingFilterProxy 相同的功能

这样做有很多的好处,上面已经提及了一种可以包装一下 ApplicationContext 对象加入一些全局信息。

其二,当认证流程出现了问题,从这个 Bean 定义的执行链开头开始排查是很方便的

SpringSecurity 对于 URI 的匹配有很多个模式,也是用过这个 Filter 进行实现的

// SpringSecurity 全部的 Filter 挑点重点的来说
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter // SecurityContext 持久化相关
HeaderWriterFilter // SpringSecurity 对请求响应头拓展
CorsFilter
CsrfFilter
LogoutFilter // 登出
OAuth2AuthorizationRequestRedirectFilter // OAuth2 重定向相关
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter // *** 用户登录过滤器
OpenIDAuthenticationFilter // 看上去挺像那啥 唯一 ID 过滤器(WeChat???)
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter // 通常设置 Token 的信息都是这个玩意儿 Bearer
BasicAuthenticationFilter // 认证相关
RequestCacheAwareFilter // 请求缓存
SecurityContextHolderAwareRequestFilter // 这个地方设置 SecurityContextHolder
JaasApiIntegrationFilter
RememberMeAuthenticationFilter // 实现记住我功能好像是通过 Session 的认证
AnonymousAuthenticationFilter 
OAuth2AuthorizationCodeGrantFilter // OAuth2 权限
SessionManagementFilter
ExceptionTranslationFilter // ** 这个东西处理抛出来的认证或者权限相关异常
FilterSecurityInterceptor
SwitchUserFilter

2. 应用#

2.1 认证方案#

共计三种形式,Form LoginBasic AuthenticationDigest Authentication

内存2.1.1 Form Login

进入 FilterSecurityInterceptor 抛出 AccessDeniedException 进入 ExceptionTranslationFilter 进行认证流程,大多数都是进入 LoginUrlAuthenticationEntryPoint 重定向到认证界面

用户此次登录携带信息进入 UsernamePasswordAuthenticationFilter 验证相关信息,这个相关的流程上面已经阐述过了,不明白的再看看

开启表单登录 –> .formLogin(withDefaults()) 可配置 LoginPage()

2.1.2 Basic Authentication

相同的步骤,抛出 AccessDeniedException 从 ExceptionTranslationFilter 进入 BasicAuthenticationEntryPoint 里面写入 WWW-Authenticate 到响应头,客户端解析到这个响应头进行重定向 这个默认是执行的 AbstractAuthenticationProcessingFilter 里面的逻辑,就只是简单的设置一个 请求头而已

开启 Basic Authentication —> .httpBasic(withDefaults())

2.1.3 Digest Authentication

这个东西官方是不推荐的,为什么?你想你这个摘要的认证就相当于你只有一个密码,而且你还要每次请求都携带,虽然加了密,但是也不安全嘛… 这不纯纯的那啥嘛… 但是还是讲一下吧,我反正没这种应用场景,说不定大伙儿可以遇到呢

他上面的流程其实一样,返回 WWW-Authenticate 的时候会设置一串编码,有个过期时间,默认五分钟。然后加到响应头里面(这个地方可以自行实现设置一些 key realname 参数,realName 会添加到这个串里面,key 不会),通过 WWW-Authenticate 为键来拿里面就有一些信息,大胆的明文登录了属于是…

然后再次进来的时候最好是自定义一个 DigestAuthenticationFilter 里面去拿到 Key 去做判断…

2.2 密码储存方案#

共计四种形式,In-Memory, Basic Authentication, UserDetailsService, LDAP

2.2.1 In-Memory

InMemoryUserDetailsManager 可以实现将用户的 UserDetails 存入 HashMap 中

public InMemoryUserDetailsManager(Properties users) {// Properties 是 HashTable 的子类	
   Enumeration<?> names = users.propertyNames();
    // 专门用于内存储存方式中的一个编辑器 可以操作用户信息 用户的CRUD,密码的更改...
   UserAttributeEditor editor = new UserAttributeEditor(); 

   while (names.hasMoreElements()) {
      String name = (String) names.nextElement();
      editor.setAsText(users.getProperty(name));
      UserAttribute attr = (UserAttribute) editor.getValue();
      UserDetails user = new User(name, attr.getPassword(), attr.isEnabled(), true,
            true, true, attr.getAuthorities());
      createUser(user);
   }
}
2.2.2 JDBC

Spring Security 也有直接对接 JDBC 的,不同于我们自己逻辑,它内置了一些写死了的变量相关实现可以查看

JdbcDaoImpl 我还是觉得自己去实现相关逻辑要好点,可能 Spring Security 像比如 Shiro 就重在这儿吧

里面涉及和权限表,用户表,会自动帮你去加载一些信息省去了写 SQL 的时间

2.2.3 UserDetailsService

这个是我们常用的方式,SpringSecurity 内置了这块的逻辑放入了一个叫 DaoAuthenticationProvider 里面,我们之前说过在 AbstractAuthenticationProcessingFilter 的子类 UsernamePasswordAuthenticationFilter 的 attemptAuthentication 方法,里面通过 AuthenticationManager 的 authenticate 方法去认证,而它的实现就是 ProviderManager,ProviderManager 的 authenticate 方法会寻找所有的 AuthenticationProvider 调用他们的 supports 方法,来查看是否支持当前的 Authentication,而 AuthenticationProvider 的实现有很多个其中我们要说的就是 DaoAuthenticationProvider。它的 Supports 方法是定义在父类 AbstractUserDetailsAuthenticationProvider 里面的,是可以支持 UsernamePasswordAuthenticationToken 这个 Authentication 的。整体的逻辑就是这样的

再来说 DaoAuthenticationProvider 内的实现,additionalAuthenticationChecks 是里面的实现方法,就是通过设置的 PasswordEncoder 的 match 方法进行密码匹配。但是逻辑过于简单拿,并且抛出异常对用户体验也不好,所以我们可以仿照着自定义一个

2.2.4 LDAP(Light Directory Access Portocol)

这个我也不太明白,就不说了后续如果想起来了可以添加一下

作者:donphds

出处:https://www.cnblogs.com/donphds/p/15853453.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   无力挽回  阅读(111)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示