首先,我们通过一个具体的案例来分析 Spring Security 认证和授权的使用。
案例场景
假设我们有一个简单的在线书店应用,该应用包含以下功能:
- 用户可以查看书籍列表。
- 只有已注册用户可以查看特定书籍的详细信息。
- 只有管理员可以添加、修改或删除书籍。
实现步骤
1. 引入依赖
首先,我们需要在 pom.xml 中引入 Spring Security 相关的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 配置安全性
创建一个配置类来配置 Spring Security:
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.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/books").permitAll() // 允许所有人访问书籍列表
.antMatchers("/book/**").hasAnyRole("USER", "ADMIN") // 只有用户和管理员能访问书籍详情
.antMatchers("/admin/**").hasRole("ADMIN") // 只有管理员能进行管理操作
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.logout().permitAll();
}
@Override
@Bean
protected UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build());
manager.createUser(User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN")
.build());
return manager;
}
}
3. 创建控制器
定义控制器来处理不同的请求:
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class BookController {
@GetMapping("/books")
public String listBooks(Model model) {
// 添加书籍列表到模型(模拟书籍数据)
model.addAttribute("books", List.of("Book1", "Book2", "Book3"));
return "books";
}
@GetMapping("/book/{id}")
public String viewBook(@PathVariable("id") int id, Model model) {
// 根据ID查询书籍详细信息(模拟书籍数据)
model.addAttribute("book", "Book" + id);
return "bookDetail";
}
}
@Controller
public class AdminController {
@GetMapping("/admin")
public String adminPage() {
return "admin";
}
// 管理员可以添加、修改或删除书籍的逻辑实现
}
4. 创建视图
假设我们使用 Thymeleaf 作为视图层模板引擎,需要创建一些 HTML 文件:
books.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Books</title>
</head>
<body>
<h2>Book List</h2>
<ul>
<li th:each="book : ${books}" th:text="${book}"></li>
</ul>
<a href="/logout">Logout</a>
</body>
</html>
bookDetail.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Book Detail</title>
</head>
<body>
<h2>Book Detail</h2>
<p th:text="${book}"></p>
<a href="/books">Back to list</a>
<a href="/logout">Logout</a>
</body>
</html>
admin.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Admin Page</title>
</head>
<body>
<h2>Admin Page</h2>
<p>Welcome, Admin!</p>
<!-- 这里可以添加一些管理员特定的操作链接,如添加、删除书籍等 -->
<a href="/logout">Logout</a>
</body>
</html>
案例总结
通过上述步骤,我们实现了一个简单的在线书店应用安全配置,并完成了以下功能:
- 公开页面:书籍列表 (
/books) 对所有用户开放,无需认证。 - 认证访问:书籍详细信息页面 (
/book/{id}) 需要用户或管理员登录后才能访问。 - 管理员权限:管理页面 (
/admin) 只有管理员角色能访问。
这种配置方式使用了内存中的用户和密码管理,但在实际应用中,你通常会结合数据库来管理用户数据。你可以实现 UserDetailsService 接口,从数据库中加载用户信息。此外,通过使用表达式和方法级别的注解(如 @PreAuthorize 和 @Secured),可以进一步细化授权控制。
底层源码分析
底层实现主要由几个核心组件和流程组成。下面是对其认证和授权机制的简要说明:
1. 认证(Authentication)
认证是指验证用户身份的过程。Spring Security 的认证过程通常包括以下几个步骤:
1.1 用户提交凭证
用户通过登录页面或其他方式提交其凭证(如用户名和密码)。
1.2 过滤器链 (Filter Chain)
Spring Security 使用一系列的过滤器来处理请求。其中,UsernamePasswordAuthenticationFilter 是负责处理基于表单登录的过滤器。
1.3 AuthenticationManager
过滤器会将用户的凭证传递给 AuthenticationManager。这是一个接口,其常见实现类是 ProviderManager。
1.4 AuthenticationProvider
AuthenticationManager 会委托一个或多个 AuthenticationProvider 来进行实际的认证工作。每个 AuthenticationProvider 都知道如何验证特定类型的凭证。最常用的实现是 DaoAuthenticationProvider,它使用 UserDetailsService 来加载用户数据并进行验证。
1.5 UserDetailsService
UserDetailsService 是一个接口,定义了从数据源加载用户详细信息的方法。开发者需要提供一个实现类,以从数据库或其他存储中获取用户信息。
1.6 UserDetails 和 GrantedAuthority
UserDetails 包含了用户的基本信息(如用户名、密码、是否启用等)。GrantedAuthority 表示授予用户的权限(角色)。
1.7 认证成功或失败
如果认证成功,AuthenticationManager 返回一个完整填充的 Authentication 对象,表示已认证的用户。如果失败,则抛出异常。
2. 授权(Authorization)
授权是指确定用户可以访问哪些资源或执行哪些操作。
2.1 SecurityContextHolder
SecurityContextHolder 持有当前经过认证的 Authentication 对象。在应用的整个生命周期中,这个对象是可访问的,并且包含当前用户的认证信息。
2.2 AccessDecisionManager
授权决策是由 AccessDecisionManager 做出的。AccessDecisionManager 接受当前的 Authentication 对象、请求的资源以及相应的 ConfigAttributes,然后决定是否允许访问。
2.3 Voter
AccessDecisionManager 通常依赖于一组 Voter 来做出决策。每个 Voter 对具体的权限检查提供意见(批准、拒绝或放弃)。
2.4 表达式和注解
Spring Security 支持使用表达式和注解来简化授权配置。例如,可以使用 @PreAuthorize 注解在方法级别上进行权限检查。
@PreAuthorize("hasRole('ROLE_ADMIN')")
public void someAdminMethod() {
// 方法体
}
下面,我们深入分析 Spring Security 在认证与授权方面的一些核心源码。
1. 认证(Authentication)流程
1.1 用户提交凭证
用户通过登录页面或其他方式提交其凭证,例如用户名和密码。在 Spring Security 中,这通常由 UsernamePasswordAuthenticationFilter 处理。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 省略部分代码...
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
1.2 AuthenticationManager 和 AuthenticationProvider
UsernamePasswordAuthenticationFilter 将 UsernamePasswordAuthenticationToken 传递给 AuthenticationManager 进行认证。最常用的 AuthenticationManager 实现是 ProviderManager。
public class ProviderManager implements AuthenticationManager {
private List<AuthenticationProvider> providers = Collections.emptyList();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
for (AuthenticationProvider provider : providers) {
if (provider.supports(authentication.getClass())) {
return provider.authenticate(authentication);
}
}
throw new ProviderNotFoundException("No authentication provider found");
}
}
ProviderManager 会遍历所有注册的 AuthenticationProvider,找到一个支持当前认证类型的提供者,并调用其 authenticate 方法。例如,DaoAuthenticationProvider 是一个常见的实现,它使用 UserDetailsService 加载用户数据并进行验证。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
throw new BadCredentialsException("Bad credentials");
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
throw new BadCredentialsException("Bad credentials");
}
}
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
return this.userDetailsService.loadUserByUsername(username);
}
}
1.3 UserDetailsService
UserDetailsService 是一个接口,定义了从数据源加载用户详细信息的方法。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
开发者需要提供一个实现类,以从数据库或其他存储中获取用户信息。
2. 授权(Authorization)流程
2.1 SecurityContextHolder
SecurityContextHolder 持有当前经过认证的 Authentication 对象,可以在应用的任意地方访问它。
public class SecurityContextHolder {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
public static SecurityContext getContext() {
return contextHolder.get();
}
public static void setContext(SecurityContext context) {
contextHolder.set(context);
}
public static void clearContext() {
contextHolder.remove();
}
}
2.2 AccessDecisionManager 和 Voter
每个 AccessDecisionVoter 都会对当前请求进行投票,决定是否允许访问。例如,RoleVoter 是一个常见的实现,它根据用户的角色来投票。
public class RoleVoter implements AccessDecisionVoter<Object> {
public static final String ROLE_PREFIX = "ROLE_";
@Override
public boolean supports(ConfigAttribute attribute) {
return (attribute != null) && this.supports(attribute.getAttribute());
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
int result = ACCESS_ABSTAIN;
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
// Attempt to find a matching granted authority
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
private boolean supports(String attribute) {
return (attribute != null) && attribute.startsWith(ROLE_PREFIX);
}
}
3. 扩展与自定义
3.1 自定义 UserDetailsService
通常,我们需要从数据库中加载用户数据,而不是硬编码在配置文件中。我们可以实现 UserDetailsService 接口,编写自定义的用户服务。
首先,我们需要定义用于存储用户信息和角色信息的数据库表。通常情况下,会使用两个表:
- 存储用户基本信息(如用户名和密码),
- 存储用户角色信息(一条数据存储一个角色)
表 1: users
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | BIGINT | 主键,自增 |
| username | VARCHAR(50) | 用户名,唯一 |
| password | VARCHAR(100) | 密码 |
| enabled | BOOLEAN | 用户是否启用 |
表 2: roles
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | BIGINT | 主键,自增 |
| user_id | BIGINT | 外键,引用用户表的主键 |
| role | VARCHAR(50) | 角色名称 |
2. 实体类
接下来,我们定义相应的实体类来映射这些数据库表。
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private boolean enabled;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Role> roles;
// getters and setters
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String role;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
// getters and setters
}
实现 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 not found");
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthorities(user));
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
}
然后,在配置中使用这个自定义的 UserDetailsService:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.2 使用表达式和注解进行授权
你可以使用 @PreAuthorize 注解来保护服务层方法。例如:
@Service
public class BookService {
@PreAuthorize("hasRole('ROLE_ADMIN')")
public void addBook(Book book) {
// 添加书籍逻辑
}
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
public Book getBookById(Long id) {
// 根据ID获取书籍逻辑
return new Book(id, "Example Book");
}
}
在配置类中启用全局方法安全性支持:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
// 配置类,可以用于自定义方法安全性配置
}
总结
通过以上分析,我们深入了解了 Spring Security 的认证和授权流程,包括如何在实际项目中应用这些知识。Spring Security 提供了灵活且强大的安全机制,通过合理的配置和扩展,你可以轻松实现各种复杂的安全需求。
以下是整个流程的总结:
-
认证流程:
- 用户提交凭证,通常由
UsernamePasswordAuthenticationFilter处理。 - 凭证被传递给
AuthenticationManager进行认证,常用的实现类是ProviderManager。 ProviderManager遍历所有注册的AuthenticationProvider,找到一个支持当前认证类型的提供者,并调用其authenticate方法。DaoAuthenticationProvider是一个常见的AuthenticationProvider实现,它使用UserDetailsService加载用户数据并验证密码。
- 用户提交凭证,通常由
-
授权流程:
SecurityContextHolder持有当前经过认证的Authentication对象。AccessDecisionManager负责做出授权决策,依靠一组AccessDecisionVoter来投票决定是否允许访问。- 常见的
AccessDecisionVoter实现包括RoleVoter,它根据用户的角色来投票。
-
配置:
- 可以通过 Java 配置类或 XML 文件进行配置。
- 定义内存中的用户和基于角色的 URL 访问控制策略。
-
扩展与自定义:
- 自定义
UserDetailsService,从数据库中加载用户数据。 - 使用注解如
@PreAuthorize和@Secured在方法级别上进行授权。 - 启用全局方法安全性支持。
- 自定义