SpringBoot Security

关于 SpringBoot Security 安全框架,以及在项目中如何使用(未完成)

Spring Security

什么是 Spring Security

一个安全框架

主要负责实现两大功能:认证与授权

事实上就是一系列具有特定功能的过滤器组合成为的一个过滤链(Filler Chain)

过滤器链解析

**ChannelProcessingFilter**:确保请求通过正确的通道(HTTP 或 HTTPS)传输。

**WebAsyncManagerIntegrationFilter**:集成 Spring Security 与 Spring Web 的异步请求处理。

**SecurityContextPersistenceFilter**:负责从 HttpSession 中加载 SecurityContext,并在请求处理完成后将其存储回 HttpSession

**HeaderWriterFilter**:用于添加安全相关的 HTTP 头,例如 X-Content-Type-OptionsX-Frame-OptionsX-XSS-Protection 等。

**CsrfFilter**:防止跨站请求伪造(CSRF)攻击。

**LogoutFilter**:处理用户注销请求。

UsernamePasswordAuthenticationFilter

负责处理我们在登录界面填写了用户名和密码后的登陆请求。负责认证工作

处理基于用户名和密码的身份验证请求。

**DefaultLoginPageGeneratingFilter**:生成默认的登录页面(如果未提供自定义登录页面)。

**DefaultLogoutPageGeneratingFilter**:生成默认的注销页面(如果未提供自定义注销页面)。

**BasicAuthenticationFilter**:处理 HTTP Basic 认证请求。

**RequestCacheAwareFilter**:确保用户在登录后重定向到他们最初请求的 URL。

**SecurityContextHolderAwareRequestFilter**:将 SecurityContext 中的信息添加到 HttpServletRequest 中。

**AnonymousAuthenticationFilter**:为未认证的用户提供匿名身份。

**SessionManagementFilter**:管理用户会话,包括并发会话控制和会话固定攻击防护。

ExceptionTranslationFilter

处理认证和授权过程中抛出的异常,并将用户重定向到适当的错误页面。

处理 AccessDeniedException AuthenticationException

FilterSecurityInterceptor

负责权限校验的过滤器,负责授权

执行访问控制决策,确定当前用户是否有权访问请求的资源。

认证流程

Authentication

Authentication 类是一个核心接口,表示用户的认证信息,有如下的方法

  • **getAuthorities()**:返回用户的权限(角色)。
  • **getCredentials()**:返回用户的凭证(如密码)。
  • **getDetails()**:返回与认证请求相关的附加信息。
  • **getPrincipal()**:返回用户的主体信息(如用户名)。
  • **isAuthenticated()**:返回用户是否已认证。
  • **setAuthenticated(boolean isAuthenticated)**:设置用户的认证状态。

SecurityContextHolder

SecurityContextHolder 是 Spring Security 中的一个核心类,用于存储和获取当前应用程序的安全上下文(SecurityContext 包含了当前用户的认证信息 Authentication 对象)

主要作用为:

  1. 存储认证信息SecurityContextHolder 存储当前用户的 SecurityContext,其中包含了用户的认证信息和权限。
  2. 获取认证信息:可以通过 SecurityContextHolder 获取当前用户的 SecurityContext,从而获取用户的认证信息和权限。
  3. 线程安全SecurityContextHolder 提供了多种策略来确保在多线程环境中安全地存储和访问 SecurityContext

项目配置

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

添加项目配置后重新运行项目

产生一个 security password

image-20240808203852328

image-20240808203835326

如果此时直接访问这个应用会出现 401 错误

image-20240808204018434

基于表单的登陆和登出

Security 提供了两个界面用于登陆和登出,分别位于 /login 和 /logout 路径下

只有通过这个进行验证后才能访问服务的其他端点

image-20240808213223113

禁用 Spring Security

1
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class })

CSRF

验证码验证

Security + JWT 实现用户权限控制

登陆

  1. 自定义登陆接口
    1. 调用 ProviderManger 的方法进行验证 如果验证通过生成 JWT
    2. 将用户信息存入 Redis 中(减少存储压力)
  2. 自定义 UserDetailService
    1. 实现查询数据库获得数据

校验

定义 JWT 认证过滤器

  1. 获取 token
  2. 解析 token 获取其中的 UserId
  3. 从 redis 中获取用户信息
  4. 存入 SecurityContextHolder

无法自动装配。找不到 ‘HttpSecurity’ 类型的 Bean。

1
2
3
//启动类添加注解
@SpringBootApplication
@EnableWebSecurity

java: java.lang.NoSuchFieldError

1
2
3
4
5
Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field 'com.sun.tools.javac.tree.JCTree qualid'


// 升级lombok版本到 1.18.30
// 参考 https://stackoverflow.com/questions/77297895/how-to-fix-nosuchfielderror-com-sun-tools-javac-tree-jctree

数据库校验用户

实现 UserDetailsService 接口

image-20240821161209809

实现 UserDetails 接口

密码加密存储

在实际项目中不会将密码等重要数据以明文的形式存入数据库中。

默认的 PasswordEncoder 要求数据库中的密码格式为:{id} password。会自动根据 id 的值去判断数据库中密码的加密方式。但是我们不会使用这种形式进行存储。所以需要替换这个 PasswordEncoder。

可以使用 SpringScurity 提供的 BCryptPasswordEncoder。需要将 BCryptPasswordEncoder 对象注入 Spring 容器中,SpringScurity 就会使用该种形式的 PasswordEncoder 进行密码校验。

定义一个 SpringScurity 的配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.wcx.blog.BlogBackend.config;

@Configuration
public class JwtSecurityConfig {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}


}

接口放行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// 开始配置HTTP请求的授权
.authorizeHttpRequests(auth -> auth
// 对于匹配"/swagger-ui/**"和"/v3/api-docs/**"路径的请求,允许所有用户访问
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**","/user/login","/authenticate").permitAll()
// 对于其他所有请求,只有经过身份验证的用户才能访问
.anyRequest().authenticated())
// 启用HTTP Basic认证
.httpBasic(Customizer.withDefaults())
// 配置会话管理,设置会话创建策略为无状态,即Spring Security不会创建或使用HTTP会话
// 不通过session来获取SecurityContext,因为JWT是无状态的
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 禁用CSRF(跨站请求伪造)保护,因为使用JWT时,通常不需要CSRF保护
.csrf(AbstractHttpConfigurer::disable)
// 配置OAuth2资源服务器,使用默认的JWT配置
// .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
// 构建并返回SecurityFilterChain对象
.build();
}

装载 AuthenticationManager

暴露出 AuthenticationManager 用于验证

1
2
3
4
5
// JwtSecurityConfig   
@Bean
public AuthenticationManager authenticationManager() throws Exception{
return authenticationConfiguration.getAuthenticationManager();
}

实现登陆验证逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  public String login(String phone,String password){
//AuthenticationManger authenticate user
//创建用于验证的 Authentication对象,下面的是其一种实现
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone, password);
//进行验证
//验证过程中会使用到 UserDetails 接口 以及 UserDetailsService 接口 需自行实现
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//验证返回结果为null则验证失败
if (Objects.isNull(authenticate)){
throw new RuntimeException("验证错误,登陆失败");
}
// 从authenticate中获取用户信息
UserDetailsImpl userDetails = (UserDetailsImpl) authenticate.getPrincipal();
// 使用用户id而不是用户的手机号生成jwt
return tokenUtil.createToken(userDetails.getUser().getId().toString());

// todo:如果认证通过,使用userid生成jwt,返回jwt
// todo: 把完整的用户信息存入redis,userid为key

}
}

JWT 认证过滤器

OncePerRequestFilter 是 Spring Framework 提供的一个抽象类,它用于确保一个请求只被过滤一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 如果token为空,直接放行
filterChain.doFilter(request, response);
return;
}

// 解析token
try{
String userId = tokenUtil.parseToken(token);
String redisKey = "token:" + userId;
UserDetailsImpl userDetails = (UserDetailsImpl) redisUtil.get(redisKey);
if (Objects.isNull(userDetails)) {
throw new RuntimeException("用户未登陆");
}

// 将用户信息存入SecurityContext中,方便后续获取用户信息
// todo: 获取权限信息 封装进入 UserDetailsImpl
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, null);
// 将authentication存入SecurityContext中
SecurityContextHolder.getContext().setAuthentication(authentication);
// 放行
filterChain.doFilter(request, response);

}catch (Exception e){
throw new RuntimeException("Token验证失败");
}
}

配置 设置过滤器的位置

1
2
3
4
// JwtSecurityConfig  securityFilterChain
// 加上一行

.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)

Cannot invoke “org.apache.commons.logging.Log.isDebugEnabled ()” because “this.logger” is null 问题解决

参考链接 StackoverFlow

出现的原因,使用 AOP + logger 时将一些不正确的类也包括了进去,导致报错。具体来说是因为 GenericFilterBean 是 Spring 提供的一个基础类,用于创建过滤器。它在初始化时会设置 logger 对象。如果在 logger 对象被设置之前就调用了需要使用 logger 的方法,就会出现这个问题。 不一定是直接使用这个类,可能是继承了这个类或者其子类

1
2
@Around("execution(* com.wcx.blog.BlogBackend..*(..))

修改后

1
2
@Around("execution(* com.wcx.blog.BlogBackend..*(..)) && !execution(* com.wcx.blog.BlogBackend.config.JwtAuthenticationTokenFilter.*(..))")

  1. 添加 Spring Security 和 JWT 相关的依赖。
  2. 创建一个 JwtTokenProvider 类,用于生成和验证 JWT。
  3. 创建一个 JwtAuthenticationFilter 类,用于在每个请求中获取 JWT,并进行验证。
  4. 在 Spring Security 的配置类中,配置 JwtAuthenticationFilter。
  5. 创建一个 UserDetailsService 实现类,用于加载用户信息。
  6. 在 Spring Security 的配置类中,配置 UserDetailsService 和密码编码器。
1
2
3
4
5
6
7
/* 
注解用于从 Spring Boot 应用程序的配置文件(如 application.properties 或 application.yml)中注入配置属性值。
具体来说,它会将配置文件中 app.jwtSecret 属性的值注入到被注解的字段、方法参数或构造函数参数中。
*/
@Value("${app.jwtSecret}")
private String jwtSecret;

第三方登录

Todo

  1. 使用 Security 和 JWT 实现用户权限限制
  2. 实现 OAuth2 实现使用 github 账号登陆