4.1 理解授权
欢迎进入授权的迷人世界,这里我们将学习如何在 Spring Security 中精细控制谁可以做什么。想象一下,我们的应用是一个大型的音乐节,授权就是确保每个人都在正确的舞台前摇摆!
4.1.1 基础知识详解
授权的核心
授权,简而言之,就是确定一个已认证的用户(即已经通过登录过程的用户)是否拥有执行某项操作的权限。这涉及到两个层面的判断:角色(Role)和权限(Authority)。
- 角色 (Role): 角色通常表示用户的分组,每个角色拥有一组权限。例如,"管理员"角色可能有权限修改所有用户的数据,而"普通用户"角色可能只能修改自己的数据。
- 权限 (Authority): 权限是更细粒度的访问控制,它定义了用户可以执行的具体操作,如"读取文件"、"写入数据"等。
授权策略
Spring Security 提供了多种授权策略,允许开发者根据具体需求来定制访问控制规则:
- 基于 URL 的授权: 通过配置特定的 URL 模式与所需的角色或权限相关联,来控制对这些 URL 的访问。
- 方法级的授权: 使用注解(如
@PreAuthorize
、@Secured
)直接在业务方法上定义访问控制规则,为细粒度的控制提供了便利。
方法级安全
Spring Security 的方法级安全功能是通过 AOP(面向切面编程)实现的,它允许你在不修改业务逻辑代码的情况下,添加额外的安全检查。这种方式非常灵活,可以应用于任何 Spring 管理的 Bean 的方法上。
- 启用方法级安全: 通过在配置类上使用
@EnableGlobalMethodSecurity
注解并设置相应的属性(如prePostEnabled
、securedEnabled
)来启用。 - 安全注解:
@PreAuthorize
、@PostAuthorize
、@Secured
和@RolesAllowed
等注解提供了丰富的选项,用于定义方法的访问控制规则。
动态权限检查
在某些情况下,静态定义的角色或权限可能不足以满足复杂的业务需求。Spring Security 支持动态权限检查,允许在运行时根据具体情况决定是否授权。
- 表达式驱动的访问控制:
@PreAuthorize
和@PostAuthorize
注解支持 SpEL(Spring 表达式语言),使得可以在表达式中引用方法参数、调用方法等,实现动态的权限判断。
通过精细地配置和使用 Spring Security 的授权机制,开发者可以为应用构建起一道坚固的安全防线,确保只有拥有适当权限的用户才能访问敏感资源或执行特定操作。这不仅提高了应用的安全性,也为维护良好的用户体验提供了支持。
4.1.2 主要案例:基于角色的页面访问控制
在这个案例中,我们将通过一个实际的示例来演示如何在 Spring Security 中实现基于角色的页面访问控制。这将确保只有具备特定角色的用户能访问相应的页面或执行特定的操作,从而提高应用的安全性。
案例 Demo
假设我们的应用有三个主要区域:首页(公开访问)、用户仪表板(仅限登录用户访问)、管理员控制台(仅限管理员访问)。
步骤 1: 定义角色
在这个场景中,我们定义两个角色:ROLE_USER
和 ROLE_ADMIN
。
步骤 2: 配置 Spring Security
创建一个名为 WebSecurityConfig
的安全配置类,继承 WebSecurityConfigurerAdapter
并重写相应的方法来配置安全策略。
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 示例中禁用CSRF保护,实际应用中应根据需要启用 .authorizeRequests() .antMatchers("/").permitAll() // 首页允许所有人访问 .antMatchers("/user/**").hasRole("USER") // 用户仪表板仅限拥有 ROLE_USER 的用户访问 .antMatchers("/admin/**").hasRole("ADMIN") // 管理员控制台仅限拥有 ROLE_ADMIN 的用户访问 .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() // 提供自定义登录页面 .and() .logout() .permitAll(); // 允许所有用户登出 } @Bean @Override public UserDetailsService userDetailsService() { // 在内存中配置一些用户 InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("user").password(passwordEncoder().encode("password")).roles("USER").build()); manager.createUser(User.withUsername("admin").password(passwordEncoder().encode("password")).roles("ADMIN").build()); return manager; } @Bean public PasswordEncoder passwordEncoder() { // 使用 BCryptPasswordEncoder 对密码进行编码 return new BCryptPasswordEncoder(); } }
步骤 3: 测试访问控制
启动应用,并尝试访问不同区域:
- 访问
/
应该对所有人开放。 - 访问
/user/dashboard
时,如果未登录或不具备ROLE_USER
角色,应重定向到登录页面。 - 访问
/admin/control
时,仅当用户拥有ROLE_ADMIN
角色时才能访问,否则应拒绝访问。
4.1.3 拓展案例 1:自定义投票策略
在更复杂的安全需求中,简单的角色检查可能不足以满足需求,这时可以通过自定义投票策略来进行细粒度的控制。Spring Security 提供了一个灵活的访问决策管理器(AccessDecisionManager
),它可以根据多个投票器(AccessDecisionVoter
)的投票结果来决定是否授予访问权限。
案例 Demo
假设我们有一个需求,只允许在特定时间段内访问某些资源。我们可以通过实现一个自定义的投票器来实现这一需求。
步骤 1: 创建自定义投票器
首先,我们创建一个自定义投票器 TimeBasedAccessDecisionVoter
,它将根据当前时间和预定义的时间段来决定是否投票赞成访问。
import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.core.Authentication; import org.springframework.security.web.FilterInvocation; import java.time.LocalTime; import java.util.Collection; public class TimeBasedAccessDecisionVoter implements AccessDecisionVoter<FilterInvocation> { private final LocalTime allowedStartTime; private final LocalTime allowedEndTime; public TimeBasedAccessDecisionVoter(String startTime, String endTime) { this.allowedStartTime = LocalTime.parse(startTime); this.allowedEndTime = LocalTime.parse(endTime); } @Override public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) { LocalTime now = LocalTime.now(); if (now.isAfter(allowedStartTime) && now.isBefore(allowedEndTime)) { return ACCESS_GRANTED; } else { return ACCESS_DENIED; } } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
步骤 2: 配置访问决策管理器
接下来,在安全配置类中配置自定义的访问决策管理器,将我们的自定义投票器添加到其中。
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.access.vote.AffirmativeBased; import org.springframework.security.access.vote.ConsensusBased; import org.springframework.security.access.vote.UnanimousBased; import java.util.List; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .accessDecisionManager(accessDecisionManager()); } public AccessDecisionManager accessDecisionManager() { List<AccessDecisionVoter<?>> decisionVoters = List.of( new TimeBasedAccessDecisionVoter("09:00", "17:00") ); return new AffirmativeBased(decisionVoters); // 使用肯定性决策策略 } }
这里,我们使用 AffirmativeBased
访问决策管理器,并添加了我们的 TimeBasedAccessDecisionVoter
作为投票器。这意味着,如果自定义投票器投票赞成,则允许访问;否则,拒绝访问。
测试自定义投票策略
启动应用并尝试在不同时间访问受保护的资源。你会发现,只有在定义的时间段内(例如上午 9:00 至下午 5:00),请求才会被允许;其他时间则会被拒绝。
通过实现这个案例,你就学会了如何使用 Spring Security 的高级特性来满足特定的安全需求,提供了一种灵活而强大的方法来根据应用的具体需求定制访问控制策略。
4.1.4 拓展案例 2:使用方法级安全进行细粒度控制
方法级安全是 Spring Security 提供的一个强大功能,它允许开发者在单个方法上应用安全注解,从而实现细粒度的访问控制。这种方式非常适用于那些需要根据不同业务逻辑对访问权限进行精细管理的应用。
案例 Demo
假设我们正在开发一个博客系统,其中包含一个文章服务,我们希望只有文章的作者或者管理员才能编辑文章。
步骤 1: 启用方法级安全
首先,在你的安全配置类上添加 @EnableGlobalMethodSecurity
注解,并设置 prePostEnabled
为 true
,以启用方法级安全。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class MethodSecurityConfig extends WebSecurityConfigurerAdapter { // 安全配置内容 }
步骤 2: 创建文章服务
接下来,创建一个文章服务 ArticleService
,并在编辑文章的方法上使用 @PreAuthorize
注解来定义访问控制规则。
@Service public class ArticleService { @PreAuthorize("hasRole('ADMIN') or #article.author.username == authentication.principal.username") public void editArticle(Article article) { // 编辑文章的逻辑 } }
在这个例子中,#article.author.username
引用了方法参数 article
中作者用户名的属性,而 authentication.principal.username
引用了当前认证用户的用户名。这条规则的含义是:“只有当当前用户是管理员,或者文章的作者时,才允许编辑文章。”
动态权限验证
假设系统中还有一个需求,即用户可以对文章进行评论,但我们希望在用户被禁言时禁止其评论。
步骤 1: 实现用户服务
假设我们有一个 UserService
,它提供了方法来检查用户是否被禁言。
@Service public class UserService { public boolean isUserBanned(String username) { // 检查用户是否被禁言的逻辑 return true; // 假设返回结果 } }
步骤 2: 使用 SpEL 进行动态验证
在评论服务的添加评论方法上使用 @PreAuthorize
,结合 SpEL 表达式和自定义方法来实现动态权限验证。
@Service public class CommentService { @Autowired private UserService userService; @PreAuthorize("!@userService.isUserBanned(authentication.principal.username)") public void addComment(Comment comment) { // 添加评论的逻辑 } }
这里,@userService.isUserBanned(authentication.principal.username)
调用了 UserService
的 isUserBanned
方法来检查当前认证用户是否被禁言,从而动态决定是否允许用户添加评论。
通过在方法上应用安全注解,你可以根据业务逻辑的需要实现复杂的访问控制策略。这种灵活性是 Spring Security 方法级安全特别强大的地方,使得开发者可以轻松应对各种复杂的安全需求。
4.2 角色与权限的配置
在 Spring Security 中,角色和权限的概念是实现细粒度访问控制的关键。通过合理配置角色和权限,你可以精确地控制谁可以访问应用中的哪些资源。
4.2.1 基础知识详解
- 角色 (Roles):
- 角色通常用来表示用户的分组,它是一种高级别的权限集合,可以简化权限管理。
- 在 Spring Security 中,角色通常以
ROLE_
前缀命名,例如ROLE_ADMIN
,ROLE_USER
等。 - 角色使得可以通过一次检查来控制对多个资源的访问权限。
- 权限 (Authorities):
- 权限代表对特定行为的访问控制,它是更细粒度的访问权限。
- 权限不一定以
ROLE_
前缀命名,可以是任何字符串,如READ_PRIVILEGES
,WRITE_PRIVILEGES
等。 - 权限允许对用户可以执行的操作进行精确控制。
- 角色与权限的关系:
- 一个角色可以包含多个权限,而一个用户可以拥有多个角色。
- 角色和权限共同工作,提供了一种灵活而强大的方式来定义安全策略,确保只有合适的用户能访问应用中的资源。
- 配置方式:
- 内存中的配置:适用于简单应用或开发测试阶段。通过
AuthenticationManagerBuilder
直接在安全配置中指定角色和用户。 - 数据库配置:适用于需要动态管理用户、角色和权限的生产环境。通常结合
UserDetailsService
和JdbcUserDetailsManager
或自定义实现来完成。
- 方法级安全:
- Spring Security 支持在方法级别进行安全控制,允许在业务逻辑方法上直接声明访问控制规则。
- 使用
@PreAuthorize
、@PostAuthorize
、@Secured
和@RolesAllowed
等注解,可以基于表达式或直接基于角色进行方法访问控制。 - 方法级安全提供了比URL级安全更细粒度的控制,特别适用于复杂业务逻辑的安全需求。
通过掌握角色和权限的基础知识以及它们在 Spring Security 中的应用方式,开发者可以为应用构建起一套既强大又灵活的安全策略,有效地保护应用资源,确保只有授权用户才能访问敏感信息或执行关键操作。
4.2.2 主要案例:配置内存中的用户、角色和权限
在这个案例中,我们将通过一个实际的示例来演示如何在 Spring Security 中使用内存配置来定义用户、角色和权限。这种方式非常适合于简单的应用或是在开发和测试阶段快速搭建安全框架。
案例 Demo
假设我们正在开发一个小型的博客系统,其中包含三类用户:管理员(ADMIN
)、作者(AUTHOR
)和访客(VISITOR
)。每种用户类型都有不同的权限集合。
步骤 1: 创建安全配置类
首先,创建一个名为 WebSecurityConfig
的安全配置类,继承 WebSecurityConfigurerAdapter
并重写 configure(AuthenticationManagerBuilder auth)
方法来配置用户、角色和权限。
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .passwordEncoder(NoOpPasswordEncoder.getInstance()) .withUser("admin").password("adminPass").roles("ADMIN") .and() .withUser("author").password("authorPass").authorities("ROLE_AUTHOR", "CREATE_POST", "EDIT_POST") .and() .withUser("visitor").password("visitorPass").roles("VISITOR"); } }
在这个配置中,我们使用了 NoOpPasswordEncoder
为了简化示例,实际应用中推荐使用 BCryptPasswordEncoder
。
步骤 2: 定义访问控制规则
继续在 WebSecurityConfig
中,重写 configure(HttpSecurity http)
方法来定义各种URL路径的访问控制规则。
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").permitAll() // 首页允许所有人访问 .antMatchers("/admin/**").hasRole("ADMIN") // 管理员界面仅管理员可访问 .antMatchers("/blog/new", "/blog/edit/*").hasAuthority("CREATE_POST") // 博客创建和编辑仅作者可访问 .anyRequest().authenticated() // 其他所有请求都需要认证 .and() .formLogin() // 启用默认登录页面 .and() .logout(); // 启用默认登出功能 }
动态角色和权限的数据库配置
在生产环境中,通常需要从数据库动态加载用户、角色和权限信息,以便于管理。你可以通过实现 UserDetailsService
接口并使用 JPA 或 MyBatis 等ORM框架从数据库加载用户信息。
基于表达式的访问控制
Spring Security 支持基于表达式的访问控制(SpEL),这为定义复杂的访问控制逻辑提供了强大的灵活性。
http .authorizeRequests() .antMatchers("/blog/**").access("hasRole('ROLE_AUTHOR') or hasRole('ROLE_ADMIN')") .anyRequest().authenticated();
通过实现这个案例,你可以看到如何在 Spring Security 中配置内存中的用户、角色和权限。这种方法虽然简单,但能够为你的应用提供一个快速而安全的认证和授权机制。在实际生产环境中,你可能会需要使用数据库来存储这些信息,以支持动态的权限管理和更细粒度的访问控制。
4.2.3 拓展案例 1:动态角色和权限的数据库配置
在实际生产环境中,角色和权限的配置通常需要更加灵活和动态,以便于管理和扩展。通过数据库配置用户、角色和权限信息,可以实现这一目标。以下是如何在 Spring Security 中实现动态角色和权限配置的示例。
案例 Demo
假设我们有一个博客系统,其中用户信息、角色和权限都存储在数据库中。
步骤 1: 创建数据库模型
首先,我们需要在数据库中创建用户(User
)、角色(Role
)和权限(Permission
)的模型。以下是一个简化的模型示例:
CREATE TABLE `users` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(100) NOT NULL, `enabled` BOOLEAN NOT NULL ); CREATE TABLE `roles` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(50) NOT NULL ); CREATE TABLE `user_roles` ( `user_id` BIGINT NOT NULL, `role_id` BIGINT NOT NULL, PRIMARY KEY (`user_id`, `role_id`), FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ); CREATE TABLE `permissions` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(50) NOT NULL ); CREATE TABLE `role_permissions` ( `role_id` BIGINT NOT NULL, `permission_id` BIGINT NOT NULL, PRIMARY KEY (`role_id`, `permission_id`), FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`), FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) );
步骤 2: 实现 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) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); Set<GrantedAuthority> authorities = new HashSet<>(); user.getRoles().forEach(role -> { authorities.add(new SimpleGrantedAuthority(role.getName())); role.getPermissions().forEach(permission -> authorities.add(new SimpleGrantedAuthority(permission.getName()))); }); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); } }
步骤 3: 配置 WebSecurityConfig
在安全配置类中使用自定义的 UserDetailsService
和合适的密码编码器。
@Configuration @EnableWebSecurity public class WebSecurityConfig 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(); } // 其他配置... }
步骤 4: 测试动态角色和权限
启动应用,并测试不同角色的用户访问受限资源,验证动态角色和权限配置是否生效。
通过实现这个案例,你就可以在 Spring Security 中实现动态的角色和权限管理。这种方式为应用提供了更大的灵活性和扩展性,使得管理用户访问权限变得更加简单和高效。
第4章 Spring Security 的授权与角色管理(2024 最新版)(下)+https://developer.aliyun.com/article/1487148