Spring Boot 安全认证与授权实战
在 Web 开发中,安全是绕不开的话题。Spring Security 作为 Spring 生态的事实标准,提供了强大的认证与授权能力。本文将带你从零开始,掌握如何在 Spring Boot 中集成 Spring Security,实现基于内存和数据库的认证,以及基于角色的权限控制。
一、快速集成
要在项目中使用 Spring Security,首先需要在 pom.xml 中添加依赖。除了基础的 Web 支持,我们还需要引入 spring-boot-starter-security。
<dependencies>
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
添加依赖后,Spring Boot 会自动启用安全配置。默认情况下,所有请求都需要登录,且密码会随机生成并打印到控制台。为了自定义行为,我们需要创建一个配置类。
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.crypto.password.NoOpPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置内存用户:admin/admin123 (ADMIN), user/user123 (USER)
auth.inMemoryAuthentication()
.passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("admin")
.password("admin123")
.roles("ADMIN")
.and()
.withUser("user")
.password("user123")
.roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/").permitAll()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
}
这里我们定义了两个角色:ADMIN 和 USER。/admin/** 路径仅允许管理员访问,而首页对所有用户开放。formLogin() 启用了默认的表单登录功能。
二、认证方式详解
1. 基于内存的认证
上面的示例就是典型的基于内存的认证。它适合开发环境或原型验证,因为用户数据硬编码在代码里。优点是简单直接,缺点是重启服务后数据丢失,不适合生产环境。
2. 基于数据库的认证
实际项目中,用户信息通常存储在数据库中。这需要引入 JPA 和数据库驱动(如 H2 用于演示)。
依赖补充:
<!-- Data JPA 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 数据库依赖 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
实体类设计:
import javax.persistence.*;
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role;
// Getter 和 Setter 方法省略...
}
Repository 接口:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
核心服务层:
Spring Security 通过 UserDetailsService 加载用户详情。我们需要实现这个接口来从数据库查询用户。
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.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 UserDetailsServiceImpl 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("用户不存在:" + username);
}
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(authorities)
.build();
}
}
配置更新:
在 SecurityConfig 中注入这个服务,替换掉内存配置。
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
此时,系统会从数据库读取用户名和密码进行校验。记得在 application.properties 中配置好数据库连接信息。
三、授权策略
认证通过后,接下来要解决'能做什么'的问题。Spring Security 主要提供两种授权方式:基于角色和基于权限。
1. 基于角色的授权
这是最常用的方式,通过 hasRole("ROLE_NAME") 来控制 URL 访问。注意,Spring Security 会自动在角色前加 ROLE_ 前缀。
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER");
2. 基于权限的授权
如果需要更细粒度的控制(例如某个按钮可见),可以使用 hasAuthority("PERMISSION_NAME")。这通常需要结合方法级注解或更复杂的表达式来实现。
四、前端视图与测试
为了让体验更完整,我们可以准备简单的 HTML 模板。使用 Thymeleaf 引擎可以方便地嵌入 Spring Security 标签。
登录页 (login.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>登录</title></head>
<body>
<div class="container">
<h1>登录</h1>
<form th:action="@{/login}" method="post">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
<button type="submit">登录</button>
</form>
<a href="/">返回首页</a>
</div>
</body>
</html>
注册页 (register.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>注册</title></head>
<body>
<div class="container">
<h1>注册</h1>
<form th:action="@{/register}" method="post">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
<label for="role">角色:</label>
<select id="role" name="role" required>
<option value="USER">用户</option>
<option value="ADMIN">管理员</option>
</select>
<button type="submit">注册</button>
</form>
<a href="/">返回首页</a>
</div>
</body>
</html>
控制器逻辑:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class SecurityController {
@Autowired
private UserRepository userRepository;
@GetMapping("/")
public String index() { return "index"; }
@GetMapping("/login")
public String login() { return "login"; }
@PostMapping("/register")
public String registerUser(String username, String password, String role) {
User user = new User(username, password, role);
userRepository.save(user);
return "redirect:/login";
}
}
自动化测试: 最后,别忘了写单元测试来验证安全逻辑是否生效。下面是一个整合测试类的示例,模拟了未登录和已登录状态下的访问情况。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SecurityApplicationTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testIndexPage() {
String response = restTemplate.getForObject("http://localhost:" + port + "/", String.class);
assertThat(response).contains("首页");
}
@Test
void testUserPageWithUserAuthentication() {
String credentials = "user:user123";
String base64Credentials = Base64.getEncoder().encodeToString(credentials.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + base64Credentials);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
"http://localhost:" + port + "/user", HttpMethod.GET, entity, String.class
);
assertThat(response.getStatusCodeValue()).isEqualTo(200);
}
}
五、总结
通过本文,我们完成了 Spring Boot 与 Spring Security 的全流程集成。从依赖引入到内存/数据库认证切换,再到基于角色的权限拦截,每一步都是构建安全应用的基础。记住,安全不是一蹴而就的,随着业务复杂度的增加,你可能还需要引入 OAuth2、JWT 等更高级的方案。但在起步阶段,掌握这些核心概念足以应对大多数场景。
在实际开发中,建议始终开启 HTTPS,并对密码进行加密存储(如 BCrypt),避免使用 NoOpPasswordEncoder 这样的明文编码器。希望这些经验能帮助你在项目中少走弯路。


