从0到1学Spring Security,Java安全框架不迷路

一、Spring Security 是什么

在 Java 开发的广阔天地里,安全问题始终是重中之重。对于 Web 应用程序而言,保护用户数据、防止非法访问至关重要。Spring Security 作为 Java 领域中最受欢迎的安全框架之一,为我们提供了全面且强大的安全解决方案。

Spring Security 是基于 Spring 框架的安全框架,它就像一位忠诚的卫士,守护着应用程序的安全大门。其核心功能主要包括以下几个方面:

  • 认证(Authentication):简单来说,就是验证用户的身份。比如在登录系统时,输入用户名和密码,系统会验证这些信息是否正确,以确认你是否是合法用户。Spring Security 支持多种身份验证方式,如常见的用户名密码认证、基于记住我(Remember - Me)的认证,甚至还支持像 OAuth、OpenID 等第三方认证方式,满足不同场景下的身份验证需求。
  • 授权(Authorization):在用户通过身份验证后,决定该用户可以访问哪些资源。例如,在一个企业管理系统中,普通员工可能只能查看自己的工作任务,而管理员则拥有更高权限,可以进行系统设置、员工信息管理等操作。Spring Security 通过基于角色(Role)、基于权限(Permission)的授权机制,灵活地控制用户对资源的访问。
  • 攻击防护:帮助我们抵御各种常见的 Web 攻击。像跨站请求伪造(CSRF)攻击,攻击者可能会伪造用户的请求,在用户不知情的情况下执行一些恶意操作。Spring Security 内置了 CSRF 防护机制,有效防止此类攻击。还有会话固定攻击等,Spring Security 都能提供相应的防护措施,让应用程序更加安全可靠。

二、为什么要学习 Spring Security

在当今数字化时代,数据安全和应用程序的安全性至关重要。学习 Spring Security 有着诸多必要性,以下是一些常见的应用场景和原因:

  • 企业级项目:在大型企业级项目中,往往涉及大量敏感数据,如客户信息、财务数据等。这些数据一旦泄露,可能会给企业带来巨大损失。Spring Security 能够通过细致的授权机制,确保只有授权人员才能访问特定的数据。例如,在一个银行系统中,柜员只能查看和处理客户的基本业务信息,而高级管理人员才能访问更敏感的财务报表等数据。通过 Spring Security 的角色和权限管理,能够有效地保护这些敏感数据,防止非法访问。
  • Web 应用程序:对于各类 Web 应用,如电商平台、社交网络等,需要确保用户的登录信息安全,防止账号被盗用。同时,也要保护应用的各种功能接口不被非法调用。以电商平台为例,用户登录后,根据其身份(普通用户、VIP 用户、管理员等),可以访问不同的功能,如普通用户只能查看商品、下单购买,而管理员则可以进行商品管理、订单处理等操作。Spring Security 可以轻松实现这些功能,通过身份验证和授权,保障 Web 应用的安全运行。
  • RESTful 服务:随着微服务架构的流行,RESTful API 被广泛应用。这些 API 需要防止被未授权访问和恶意攻击。Spring Security 可以对 RESTful 服务进行保护,通过配置访问规则,只有经过认证和授权的客户端才能访问相应的 API 接口。比如,一个提供用户数据的 API,只有授权的应用程序才能获取数据,有效保证了数据的安全和完整性。
  • 分布式系统:在分布式系统中,各个微服务之间的通信安全也至关重要。Spring Security 可以在分布式环境中实现统一的安全管理,保护服务之间的通信和数据传输。例如,通过使用 Spring Security 的 OAuth2 支持,可以实现基于令牌的认证和授权,确保不同微服务之间的交互安全可靠。

可以看出,Spring Security 在保障应用程序安全方面发挥着关键作用。无论是保护敏感数据、防止非法访问,还是应对各种安全威胁,它都提供了全面且有效的解决方案。掌握 Spring Security,能够让开发者在构建应用程序时,为其打造坚实的安全防线,提升应用的安全性和可靠性,这对于开发者的职业发展和项目的成功都具有重要意义 。

三、Spring Security 核心概念解析

(一)认证(Authentication)

认证,简单来说,就是验证用户身份的过程。在日常生活中,我们去银行办理业务时,需要出示身份证来证明自己的身份,这就是一种认证方式。在 Spring Security 中,当用户登录时,系统会对用户提供的凭据(如用户名和密码)进行验证,以确认用户是否为其声称的那个人。

Spring Security 支持多种认证方式,其中最常见的就是用户名密码认证。当用户在登录页面输入用户名和密码后,Spring Security 会将这些信息封装成一个Authentication对象,然后交给AuthenticationManager进行认证。AuthenticationManager会委托给一个或多个AuthenticationProvider来处理具体的认证逻辑。例如,DaoAuthenticationProvider会从用户数据存储(如数据库、内存等)中获取用户信息,并与提交的密码进行比对 。如果认证成功,AuthenticationProvider会返回一个包含用户权限信息的Authentication对象,这个对象会被存储到SecurityContextHolder中,表示用户已成功登录,用户就可以访问受保护的资源了。如果认证失败,就会抛出相应的认证异常,如BadCredentialsException(表示密码错误)等,用户则无法登录。

(二)授权(Authorization)

授权,是在用户通过认证后,决定该用户可以访问哪些资源的过程。就像在一个公司里,不同职位的员工拥有不同的权限,普通员工可能只能查看自己的工作任务,而经理则可以查看和修改整个团队的任务安排。在 Spring Security 中,授权主要通过配置和注解来实现。

Spring Security 提供了多种授权方式,比如基于角色的访问控制(RBAC)。在这种方式下,我们会为用户分配不同的角色,如 “ADMIN”(管理员)、“USER”(普通用户)等,然后根据角色来控制用户对资源的访问。例如,我们可以配置只有拥有 “ADMIN” 角色的用户才能访问系统的管理页面,而拥有 “USER” 角色的用户只能访问普通的业务页面。通过配置HttpSecurity对象,我们可以轻松实现这种基于角色的授权:

http.authorizeRequests()

.antMatchers("/admin/**").hasRole("ADMIN")

.antMatchers("/user/**").hasRole("USER")

.anyRequest().authenticated()

.and()

.formLogin().permitAll()

.and()

.logout().permitAll();

除了基于角色的授权,Spring Security 还支持基于表达式的授权,这种方式更加灵活,可以根据复杂的业务逻辑来决定用户是否有权访问资源。例如,我们可以使用表达式@PreAuthorize("hasRole('ADMIN') or (hasRole('USER') and #id ==
authentication.principal.id)")来表示只有管理员或者当前用户访问自己的资源时才有权限。

(三)核心组件

Spring Security 的功能强大离不开其众多核心组件的协同工作,下面介绍几个重要的核心组件:

  • SecurityContextHolder:这是 Spring Security 的一个关键组件,它提供了对SecurityContext的访问,而SecurityContext持有关于当前用户的详细信息,包括认证对象。简单来说,它就像一个容器,存储着当前用户的安全上下文信息。在一个请求处理过程中,我们可以通过SecurityContextHolder.getContext().getAuthentication()来获取当前用户的认证信息,进而获取用户的权限、用户名等信息。例如,在一个控制器方法中,我们可以这样获取当前登录用户的用户名:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication!= null) {

Object principal = authentication.getPrincipal();

if (principal instanceof UserDetails) {

String username = ((UserDetails) principal).getUsername();

System.out.println("当前登录用户:" + username);

}

}

  • AuthenticationManager:负责验证认证对象,并返回一个已认证的对象。它是认证过程的核心组件,管理着多个AuthenticationProvider。当用户提交登录请求时,AuthenticationManager会依次调用各个AuthenticationProvider的authenticate方法来进行认证。如果有一个AuthenticationProvider认证成功,AuthenticationManager就会返回一个已认证的Authentication对象;如果所有的AuthenticationProvider都认证失败,就会抛出AuthenticationException异常。例如,在自定义认证逻辑时,我们可能会创建一个自定义的AuthenticationProvider,并将其添加到AuthenticationManager中,以实现特定的认证方式 。
  • UserDetailsService:用于根据用户名检索UserDetails对象。UserDetails是 Spring Security 中表示用户详细信息的接口,它包含了用户名、密码、权限等信息。UserDetailsService只有一个方法loadUserByUsername,该方法根据传入的用户名从数据库或其他数据源中加载用户信息。在认证过程中,AuthenticationProvider会通过UserDetailsService获取用户的信息,并与用户提交的登录信息进行比对。例如,我们可以实现一个UserDetailsService接口的实现类,从数据库中查询用户信息:
@Service

public class CustomUserDetailsService implements UserDetailsService {

@Autowired

private UserRepository userRepository;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

User user = userRepository.findByUsername(username);

if (user == null) {

throw new UsernameNotFoundException("用户不存在");

}

// 将User对象转换为UserDetails对象,通常会封装权限等信息

return new org.springframework.security.core.userdetails.User(

user.getUsername(),

user.getPassword(),

AuthorityUtils.createAuthorityList("ROLE_USER"));

}

}

这些核心组件相互协作,共同完成 Spring Security 的认证和授权功能,为应用程序提供了强大的安全保障 。

四、快速上手 Spring Security

(一)创建 Spring Boot 项目

在开始使用 Spring Security 之前,我们需要先创建一个 Spring Boot 项目。如果你使用的是 IntelliJ IDEA,可以按照以下步骤进行创建:

  1. 打开 IntelliJ IDEA,点击 “Create New Project”。
  1. 在弹出的窗口中,选择 “Spring Initializr”,然后点击 “Next”。
  1. 在 “Project Metadata” 页面,填写项目的基本信息,如 Group、Artifact 等,然后点击 “Next”。
  1. 在 “Dependencies” 页面,搜索 “Spring Security”,并将其添加到项目中。此外,为了方便测试,我们还可以添加 “Spring Web” 依赖,这样就可以创建一些简单的 Web 接口来测试 Spring Security 的功能。添加完成后,点击 “Finish”。

等待项目创建完成后,你会看到项目的pom.xml文件中已经添加了 Spring Security 和 Spring Web 的依赖:

org.springframework.boot

spring-boot-starter-security

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-test

test

org.springframework.security

spring-security-test

test

(二)简单配置

接下来,我们对 Spring Security 进行一些基本配置。在 Spring Boot 中,我们可以通过创建一个配置类来配置 Spring Security。在src/main/java目录下,创建一个名为SecurityConfig的类,内容如下:

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

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.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.crypto.password.NoOpPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration

@EnableWebSecurity

public class SecurityConfig extends WebSecurityConfigurerAdapter {

// 配置用户信息,这里使用内存存储用户信息,仅用于演示,实际应用中应从数据库读取

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication()

.withUser("user")

.password("{noop}password")

.roles("USER")

.and()

.withUser("admin")

.password("{noop}admin123")

.roles("ADMIN", "USER");

}

// 配置密码编码器,这里使用NoOpPasswordEncoder表示不进行密码加密,仅用于演示,生产环境应使用更安全的加密方式

@Bean

public PasswordEncoder passwordEncoder() {

return NoOpPasswordEncoder.getInstance();

}

// 配置HTTP安全,定义哪些请求需要认证,哪些请求可以匿名访问等

@Override

protected void configure(HttpSecurity http) throws Exception {

http

.authorizeRequests()

.antMatchers("/", "/home").permitAll()

.antMatchers("/admin/**").hasRole("ADMIN")

.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")

.anyRequest().authenticated()

.and()

.formLogin()

.loginPage("/login")

.permitAll()

.and()

.logout()

.permitAll();

}

}

在上述配置中:

  • configure(AuthenticationManagerBuilder auth)方法配置了两个用户,一个是普通用户user,密码为password,角色为USER;另一个是管理员用户admin,密码为admin123,角色为ADMIN和USER。这里使用{noop}前缀表示密码不进行加密,仅用于演示,实际生产中应使用更安全的密码加密方式,如BCryptPasswordEncoder。
  • passwordEncoder()方法返回一个密码编码器,这里使用NoOpPasswordEncoder表示不进行密码加密,在实际应用中,建议使用如BCryptPasswordEncoder等安全的密码编码器。
  • configure(HttpSecurity http)方法配置了 HTTP 安全规则:
    • antMatchers("/", "/home").permitAll()表示根路径和/home路径可以被所有用户访问,无需认证。
    • antMatchers("/admin/**").hasRole("ADMIN")表示只有具有ADMIN角色的用户才能访问/admin开头的路径。
    • antMatchers("/user/**").hasAnyRole("USER", "ADMIN")表示具有USER或ADMIN角色的用户可以访问/user开头的路径。
    • anyRequest().authenticated()表示其他所有请求都需要用户认证后才能访问。
    • formLogin()配置了表单登录相关的设置,loginPage("/login")指定了登录页面的路径为/login,permitAll()表示允许所有用户访问登录页面。
    • logout()配置了注销登录相关的设置,permitAll()表示允许所有用户进行注销操作。

为了测试配置是否生效,我们可以创建一个简单的控制器类。在src/main/java目录下,创建一个名为HomeController的类,内容如下:

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ResponseBody;

@Controller

public class HomeController {

@GetMapping("/")

public String home() {

return "home";

}

@GetMapping("/home")

@ResponseBody

public String homePage() {

return "Welcome to the home page!";

}

@GetMapping("/user/secure")

@ResponseBody

public String userSecure() {

return "Hello, User! You have access to this user - only page.";

}

@GetMapping("/admin/secure")

@ResponseBody

public String adminSecure() {

return "Hello, Admin! You have access to this admin - only page.";

}

}

在上述控制器类中,我们定义了几个简单的接口:

  • "/"和"/home"接口用于测试不需要认证即可访问的路径。
  • "/user/secure"接口只有具有USER或ADMIN角色的用户可以访问。
  • "/admin/secure"接口只有具有ADMIN角色的用户可以访问。

现在,我们可以运行 Spring Boot 项目了。启动项目后,打开浏览器,访问http://localhost:8080/,你会看到 “Welcome to the home page!” 的提示,这是因为我们配置了根路径和/home路径可以被所有用户访问。

接着,访问
http://localhost:8080/user/secure,由于该路径需要认证,Spring Security 会自动重定向到登录页面
http://localhost:8080/login。在登录页面输入用户名user和密码password,点击登录后,你就可以看到 “Hello, User! You have access to this user - only page.” 的提示,表示你已经成功以普通用户身份访问了受保护的资源。

再尝试访问
http://localhost:8080/admin/secure,使用用户名admin和密码admin123登录后,你可以看到 “Hello, Admin! You have access to this admin - only page.” 的提示,这表明具有ADMIN角色的用户可以访问该路径。

通过以上简单的配置和测试,我们初步体验了 Spring Security 的认证和授权功能。在实际应用中,我们还需要根据具体的业务需求进行更深入的配置和定制 。

五、深入 Spring Security 配置

(一)自定义登录页面

在实际项目中,Spring Security 提供的默认登录页面可能无法满足我们的设计需求,这时就需要自定义登录页面。自定义登录页面主要涉及两个方面:前端页面的创建和后端配置的修改。

首先,创建前端登录页面。在src/main/resources/static目录下创建一个login.html文件(如果使用模板引擎,如 Thymeleaf,可放在相应的模板目录下),内容如下:

用户登录



在这个页面中,我们创建了一个简单的表单,表单的action属性指向/login,这是 Spring Security 默认的登录处理路径,method为post。username和password是 Spring Security 默认的用户名和密码参数名 。

接下来,修改后端配置。在SecurityConfig类中,对configure(HttpSecurity http)方法进行如下修改:

@Override

protected void configure(HttpSecurity http) throws Exception {

http

.authorizeRequests()

.antMatchers("/", "/home").permitAll()

.antMatchers("/admin/**").hasRole("ADMIN")

.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")

.anyRequest().authenticated()

.and()

.formLogin()

.loginPage("/login.html") // 指定自定义登录页面路径

.permitAll()

.and()

.logout()

.permitAll();

}

通过loginPage("/login.html")指定了自定义的登录页面路径为/login.html。这样,当用户访问需要认证的资源时,Spring Security 会将用户重定向到我们自定义的登录页面 。

(二)基于数据库的认证

在前面的示例中,我们使用内存存储用户信息进行认证,这在实际生产环境中是远远不够的。通常,我们需要从数据库中读取用户信息来进行认证。下面以 MySQL 数据库为例,介绍如何实现基于数据库的认证。

首先,添加数据库相关依赖。在pom.xml文件中添加以下依赖:

mysql

mysql-connector-java

org.mybatis.spring.boot

mybatis-spring-boot-starter

com.alibaba

druid-spring-boot-starter

然后,配置数据源。在application.properties文件中添加如下配置:

spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC

spring.datasource.username=root

spring.datasource.password=root

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

请根据实际情况修改数据库名称、用户名和密码 。

接着,创建用户实体类和数据访问层。假设我们有一个User实体类,包含id、username、password等字段,创建User类如下:

public class User {

private Long id;

private String username;

private String password;

// 其他字段及getter、setter方法

}

然后创建UserMapper接口,用于从数据库中查询用户信息:

import org.apache.ibatis.annotations.Mapper;

import org.apache.ibatis.annotations.Select;

@Mapper

public interface UserMapper {

@Select("SELECT * FROM user WHERE username = #{username}")

User findByUsername(String username);

}

接下来,实现UserDetailsService接口。UserDetailsService是 Spring Security 用于加载用户详细信息的接口,我们需要实现其loadUserByUsername方法,从数据库中查询用户信息并封装成UserDetails对象:

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

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.core.userdetails.UsernameNotFoundException;

import org.springframework.stereotype.Service;

import java.util.ArrayList;

import java.util.List;

@Service

public class CustomUserDetailsService implements UserDetailsService {

@Autowired

private UserMapper userMapper;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

User user = userMapper.findByUsername(username);

if (user == null) {

throw new UsernameNotFoundException("用户不存在");

}

List authorities = new ArrayList<>();

authorities.add(new SimpleGrantedAuthority("ROLE_USER"));

// 这里可以根据实际情况从数据库中获取用户的角色信息,并添加到authorities中

return new User(user.getUsername(), user.getPassword(), authorities);

}

}

最后,修改SecurityConfig类,将认证方式改为基于数据库的认证:

@Configuration

@EnableWebSecurity

public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired

private CustomUserDetailsService customUserDetailsService;

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.userDetailsService(customUserDetailsService)

.passwordEncoder(passwordEncoder());

}

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

@Override

protected void configure(HttpSecurity http) throws Exception {

http

.authorizeRequests()

.antMatchers("/", "/home").permitAll()

.antMatchers("/admin/**").hasRole("ADMIN")

.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")

.anyRequest().authenticated()

.and()

.formLogin()

.loginPage("/login.html")

.permitAll()

.and()

.logout()

.permitAll();

}

}

在上述配置中,auth.userDetailsService(customUserDetailsService)指定了使用我们自定义的CustomUserDetailsService来获取用户信息,passwordEncoder()方法配置了密码编码器,这里使用BCryptPasswordEncoder对密码进行加密存储和验证 。

(三)权限控制

权限控制是 Spring Security 的重要功能之一,它可以确保只有授权用户才能访问特定的资源。Spring Security 提供了多种权限控制方式,常见的有基于角色和基于 URL 的访问控制。

基于角色的访问控制(RBAC)是最常用的权限控制方式之一。在前面的示例中,我们已经使用过基于角色的访问控制,例如:

http

.authorizeRequests()

.antMatchers("/admin/**").hasRole("ADMIN")

.antMatchers("/user/**").hasAnyRole("USER", "ADMIN");

这段代码表示只有具有ADMIN角色的用户才能访问/admin开头的路径,具有USER或ADMIN角色的用户可以访问/user开头的路径。

除了基于角色的访问控制,Spring Security 还支持基于 URL 的访问控制。例如,我们可以根据不同的 URL 路径设置不同的访问权限:

http

.authorizeRequests()

.antMatchers("/public/**").permitAll()

.antMatchers("/private/**").authenticated()

.antMatchers("/admin/sensitive").hasRole("ADMIN")

.antMatchers("/user/info").hasAnyRole("USER", "ADMIN");

在这个配置中:

  • "/public/**"路径下的资源允许所有用户访问,无需认证。
  • "/private/**"路径下的资源需要用户认证后才能访问。
  • "/admin/sensitive"路径只有具有ADMIN角色的用户才能访问。
  • "/user/info"路径允许具有USER或ADMIN角色的用户访问 。

Spring Security 还支持基于表达式的访问控制,这种方式更加灵活,可以根据复杂的业务逻辑来决定用户是否有权访问资源。例如:

http

.authorizeRequests()

.antMatchers("/admin/operation").access("hasRole('ADMIN') and @customPermissionService.hasPermission(authentication, 'admin:operation')");

在这个例子中,@
customPermissionService.hasPermission(authentication, 'admin:operation')是一个自定义的权限判断方法,它会根据当前用户的认证信息(authentication)和权限标识(admin:operation)来判断用户是否有权限访问/admin/operation路径。通过这种方式,我们可以实现非常灵活的权限控制逻辑 。

六、常见问题与解决方案

(一)跨域问题

在前后端分离的开发模式中,跨域问题是一个常见的挑战。跨域问题产生的原因是浏览器的同源策略,该策略要求浏览器在执行脚本时,限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这里的 “源” 由协议、域名和端口号组成,只要这三者中的任何一个不同,就会产生跨域问题 。

例如,前端应用运行在http://localhost:8080,而后端服务部署在http://localhost:8081,当前端向后端发送请求时,就会触发跨域问题。因为它们的端口号不同,属于不同的源。

在 Spring Security 中,配置解决跨域的方法主要有以下几种:

  1. 开启 Cors 方法:在SecurityConfig配置类中,通过调用cors()方法开启 Spring Security 对 CORS 的支持。例如:
@Configuration

@EnableWebSecurity

public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override

protected void configure(HttpSecurity http) throws Exception {

http

.authorizeRequests()

.anyRequest().authenticated()

.and()

.formLogin()

.permitAll()

.and()

.httpBasic()

.and()

.cors() // 开启CORS支持

.and()

.csrf().disable();

}

}

这种方式会使用默认的 CORS 配置,允许所有的跨域请求。在实际应用中,可能需要更细粒度的控制 。

2. 进行全局配置:通过自定义CorsConfigurationSource来实现更详细的跨域配置。首先创建一个CorsConfigurationSource的 Bean,然后在HttpSecurity配置中引用它。例如:

@Configuration

@EnableWebSecurity

public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override

protected void configure(HttpSecurity http) throws Exception {

http

.authorizeRequests()

.anyRequest().authenticated()

.and()

.formLogin()

.permitAll()

.and()

.httpBasic()

.and()

.cors().configurationSource(corsConfigurationSource()) // 引用自定义的CorsConfigurationSource

.and()

.csrf().disable();

}

@Bean

public CorsConfigurationSource corsConfigurationSource() {

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowCredentials(true);


configuration.setAllowedOrigins(Collections.singletonList("*")); // 允许所有源,可根据实际情况修改


configuration.setAllowedMethods(Collections.singletonList("*")); // 允许所有方法


configuration.setAllowedHeaders(Collections.singletonList("*")); // 允许所有头信息

configuration.setMaxAge(Duration.ofHours(1)); // 预检请求的有效期为1小时


source.registerCorsConfiguration("/**", configuration); // 对所有路径应用配置

return source;

}

}

在上述配置中,通过CorsConfiguration对象详细配置了跨域的相关参数,如允许的源、方法、头信息以及预检请求的有效期等。然后通过
UrlBasedCorsConfigurationSource将这些配置应用到所有路径上。这样就实现了对跨域请求的全局配置 。

(二)CSRF 防御

CSRF(Cross - Site Request Forgery),即跨站请求伪造,是一种常见的网络攻击方式。它的攻击原理是利用用户已经登录的信任状态,诱骗用户在不知情的情况下执行恶意请求。攻击者并不需要直接获取用户的凭证(如密码),而是利用用户已有的会话令牌(通常是 Cookie)等来伪造请求 。

例如,用户在登录了一个银行网站后,在未退出登录的情况下,访问了一个包含恶意链接的网页。该链接可能会伪装成一个普通的图片链接,但实际上它会向银行网站发送一个转账请求,由于用户的浏览器会自动携带已登录银行网站的 Cookie,银行网站无法区分这个请求是用户的真实操作还是攻击者的恶意伪造,从而执行了转账操作,导致用户资金受损。

Spring Security 提供了强大的 CSRF 防御机制,默认情况下是开启的。其主要防御机制基于 “同步令牌模式”(Synchronizer Token Pattern),工作机制如下:

  1. 生成令牌:服务端为每个用户会话生成一个唯一的 CSRF 令牌(token)。这个令牌会随着每一个表单或者需要受保护的请求一起发送到客户端。
  1. 提交令牌:当用户提交表单或者发送请求时,必须携带这个令牌。
  1. 验证令牌:服务端接收到请求后会验证令牌是否匹配当前用户会话中存储的令牌。如果匹配,则请求有效;否则,认为是 CSRF 攻击,请求被拒绝。

在 Spring Security 中,相关配置如下:

@Configuration

@EnableWebSecurity

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override

protected void configure(HttpSecurity http) throws Exception {

http

.csrf()

.csrfTokenRepository(
CookieCsrfTokenRepository.withHttpOnlyFalse()); // 使用Cookie存储CSRF令牌

}

}

在上述配置中,csrfTokenRepository(
CookieCsrfTokenRepository.withHttpOnlyFalse())指定了使用 Cookie 来存储 CSRF 令牌,并且设置HttpOnly为false,这样前端 JavaScript 代码可以读取到 Cookie 中的令牌,以便在请求中携带。

在前端页面中,如果使用表单提交,Spring Security 会自动为表单添加 CSRF 令牌。例如:

在这个表单中,${_csrf.parameterName}和${_csrf.token}会被 Spring Security 替换为实际的 CSRF 令牌参数名和令牌值,确保在表单提交时携带正确的 CSRF 令牌 。

如果是使用 Ajax 请求,也需要在请求头中添加 CSRF 令牌。可以通过在页面中添加如下元数据来获取 CSRF 令牌信息:

然后在 JavaScript 代码中,通过如下方式在 Ajax 请求头中添加 CSRF 令牌:

$(function() {

var token = $('meta[name="_csrf"]').attr('content');

var header = $('meta[name="_csrf_header"]').attr('content');

$(document).ajaxSend(function(e, xhr, options) {

xhr.setRequestHeader(header, token);

});

});

这样,在每次 Ajax 请求时,都会自动在请求头中添加 CSRF 令牌,保证请求的安全性 。

七、总结与展望

通过对 Spring Security 的学习,我们深入了解了其在保障 Java 应用程序安全方面的强大功能和重要性。从基础的认证和授权机制,到核心概念与组件的解析,再到实际的配置与应用,以及常见问题的解决,Spring Security 为我们提供了一套全面且灵活的安全解决方案 。

在学习过程中,我们掌握了如何实现基于内存、数据库的认证,以及如何进行权限控制和自定义登录页面等关键技能。同时,也了解了 Spring Security 在应对跨域问题和 CSRF 攻击等安全威胁时的有效策略 。

然而,Spring Security 的学习是一个不断深入的过程。随着技术的发展和应用场景的多样化,后续我们还可以进一步探索其更多高级功能,如与 OAuth2 集成实现第三方认证、在分布式系统中的应用,以及结合 JWT(JSON Web Token)实现无状态的认证机制等 。

希望读者通过本文的学习,能够对 Spring Security 有一个全面的认识,并在实际项目中灵活运用,为应用程序构建起坚实的安全防线。不断学习和实践,才能在安全领域不断进步,应对日益复杂的安全挑战 。

相关文章

做Python开发时遇到需求实现,必须调用Java方法,可以这么做

之前在公司做框架及全自动化测试工具开发时,需要测试结束后,回传结果及日志到测试平台与云存储平台。但是云存储平台没有相关Python的服务接口开放,而且构造参数时及其复杂,经沟通之前其他类似需求业务是通...

java程序员面试时经常被问到的10个问题

java程序员,尤其是做web开发的,面试时,面试官最喜欢问到以下10个问题,掌握面试的动态和技巧,有利于提高我们面试的成功率,了解以下10个问题,有利于java程序员的面试。1、简单描述一下Log4...

从“线程小白”到“池主”:Java线程与线程池的修炼秘籍

线程:并发世界的基础在 Java 的编程宇宙中,线程是一个不可或缺的重要概念。它就像是并发编程的 “超级英雄”,赋予程序同时执行多个任务的超能力,极大地提升了程序的效率和响应性。想象一下,你去一家餐厅...

记一次CPU使用率低负载高的排查过程

一、背景历史原因,当前有一个服务专门用于处理mq消息,mq使用的阿里云rocketmq,sdk版本1.2.6(2016年)。随着业务的发展,该应用上的consumer越来越多,接近200+,导致该应用...

面试官:核心线程数为0时,线程池如何执行?

线程池是 Java 中用于提升程序执行效率的主要手段,也是并发编程中的核心实现技术,并且它也被广泛的应用在日常项目的开发之中。那问题来了,如果把线程池中的核心线程数设置为 0 时,线程池是如何执行的?...

从IO到NIO:Java数据传输的进阶之路

引言:Java IO 的进化在 Java 编程的世界里,I/O(Input/Output)操作就像是程序与外部世界沟通的桥梁。无论是读取文件、网络通信,还是写入数据,I/O 操作无处不在。早期的 Ja...