Spring Security 6 快速入门指南
下面是一个完整的 Spring Security 6 快速入门实现,包含用户认证、授权、自定义登录页和API保护等核心功能。
项目结构
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── security/
│ │ ├── SecurityConfig.java
│ │ ├── WebController.java
│ │ ├── CustomUserDetailsService.java
│ │ └── SecurityApplication.java
│ └── resources/
│ ├── static/
│ │ ├── css/
│ │ │ └── style.css
│ │ └── js/
│ │ └── script.js
│ ├── templates/
│ │ ├── home.html
│ │ ├── login.html
│ │ ├── admin.html
│ │ └── user.html
│ └── application.properties
核心代码实现
1. Spring Security 配置类
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/public/**", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.permitAll()
)
.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
)
.exceptionHandling(exception -> exception
.accessDeniedPage("/access-denied")
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**") // API端点禁用CSRF
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin"))
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2. 控制器类
package com.example.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class WebController {
@GetMapping("/")
public String home() {
return "home";
}
@GetMapping("/dashboard")
public String dashboard(Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
model.addAttribute("username", auth.getName());
model.addAttribute("roles", auth.getAuthorities());
return "dashboard";
}
@GetMapping("/admin/dashboard")
public String adminDashboard() {
return "admin";
}
@GetMapping("/user/dashboard")
public String userDashboard() {
return "user";
}
@GetMapping("/access-denied")
public String accessDenied() {
return "access-denied";
}
// 受保护的API端点
@GetMapping("/api/greet")
@ResponseBody
public String apiGreet() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return "Hello, " + auth.getName() + "! You have roles: " + auth.getAuthorities();
}
// 公共API端点
@GetMapping("/api/public")
@ResponseBody
public String publicApi() {
return "This is a public API endpoint";
}
}
3. 自定义登录页面 (login.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="login-container">
<h2>Spring Security 6 Login</h2>
<!-- 错误消息显示 -->
<div th:if="${param.error}" class="error">
Invalid username or password
</div>
<!-- 登出成功消息 -->
<div th:if="${param.logout}" class="success">
You have been logged out
</div>
<form th:action="@{/login}" method="post">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group remember">
<label>
<input type="checkbox" name="remember-me"> Remember me
</label>
</div>
<button type="submit">Login</button>
</form>
<div class="footer">
<p>Demo Accounts:</p>
<ul>
<li>User: user/password</li>
<li>Admin: admin/admin</li>
</ul>
</div>
</div>
</body>
</html>
4. 仪表盘页面 (dashboard.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Dashboard</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<header>
<h1>Welcome, <span th:text="${username}">User</span>!</h1>
<form th:action="@{/logout}" method="post">
<button type="submit">Logout</button>
</form>
</header>
<section class="user-info">
<h2>Your Information</h2>
<p><strong>Username:</strong> <span th:text="${username}"></span></p>
<p><strong>Roles:</strong> <span th:text="${roles}"></span></p>
</section>
<section class="navigation">
<h2>Navigation</h2>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/user/dashboard">User Dashboard</a></li>
<li sec:authorize="hasRole('ADMIN')">
<a href="/admin/dashboard">Admin Dashboard</a>
</li>
<li><a href="/api/greet">API Greeting</a></li>
<li><a href="/api/public">Public API</a></li>
</ul>
</section>
</div>
</body>
</html>
5. 基本样式 (style.css)
/* 基础样式 */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
width: 80%;
max-width: 1200px;
margin: 20px auto;
padding: 20px;
background: #fff;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.login-container {
width: 400px;
margin: 50px auto;
padding: 30px;
background: #fff;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0,0,0,0.1);
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #0056b3;
}
.error {
background: #ffdddd;
color: #d8000c;
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
}
.success {
background: #ddffdd;
color: #4f8a10;
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
}
.footer {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #eee;
font-size: 0.9em;
color: #666;
}
.navigation ul {
list-style: none;
padding: 0;
}
.navigation li {
margin-bottom: 10px;
}
.navigation a {
text-decoration: none;
color: #007bff;
}
.navigation a:hover {
text-decoration: underline;
}
关键功能说明
1. 安全配置解析
-
请求授权:
.authorizeHttpRequests(auth -> auth .requestMatchers("/", "/home", "/public/**", "/css/**", "/js/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() ) -
表单登录:
.formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard", true) .permitAll() ) -
登出功能:
.logout(logout -> logout .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/login?logout") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() ) -
异常处理:
.exceptionHandling(exception -> exception .accessDeniedPage("/access-denied") )
2. 用户管理
Spring Security 6 提供多种用户存储方式:
-
内存用户存储:
@Bean public UserDetailsService userDetailsService() { UserDetails user = User.builder() .username("user") .password(passwordEncoder().encode("password")) .roles("USER") .build(); UserDetails admin = User.builder() .username("admin") .password(passwordEncoder().encode("admin")) .roles("ADMIN", "USER") .build(); return new InMemoryUserDetailsManager(user, admin); } -
JDBC用户存储:
@Bean public UserDetailsService jdbcUserDetailsService(DataSource dataSource) { return new JdbcUserDetailsManager(dataSource); } -
自定义用户服务:
@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")); return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .authorities(user.getAuthorities()) .build(); } }
3. 密码加密
使用 BCrypt 密码编码器:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
进阶功能
1. 方法级安全控制
在配置类上添加 @EnableMethodSecurity 注解:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
// ...
}
在服务方法上使用安全注解:
@Service
public class ProductService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteProduct(Long id) {
// 只有管理员可以删除产品
}
@PreAuthorize("hasRole('USER')")
@PostAuthorize("returnObject.owner == authentication.name")
public Product getProduct(Long id) {
// 用户只能访问自己的产品
}
}
2. OAuth2 登录集成
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
配置 OAuth2 登录:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
);
return http.build();
}
在 application.properties 中配置客户端:
# Google OAuth2 配置
spring.security.oauth2.client.registration.google.client-id=your-client-id
spring.security.oauth2.client.registration.google.client-secret=your-client-secret
3. JWT 认证
添加 JWT 依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
创建 JWT 工具类:
public class JwtUtil {
private static final String SECRET_KEY = "your-secret-key";
private static final long EXPIRATION_TIME = 864_000_000; // 10 days
public static String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public static String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public static boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private static boolean isTokenExpired(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getExpiration()
.before(new Date());
}
}
配置 JWT 过滤器:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
在安全配置中添加过滤器:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
运行和测试
-
启动应用程序:
./mvnw spring-boot:run -
访问以下端点:
http://localhost:8080- 公共主页http://localhost:8080/login- 登录页面http://localhost:8080/dashboard- 用户仪表盘(需要登录)http://localhost:8080/admin/dashboard- 管理员仪表盘http://localhost:8080/api/public- 公共APIhttp://localhost:8080/api/greet- 受保护API
-
测试用户:
- 普通用户:user/password
- 管理员:admin/admin
最佳实践
-
生产环境建议:
- 使用数据库存储用户凭据
- 配置 HTTPS
- 设置强密码策略
- 实施多因素认证
-
安全加固:
.headers(headers -> headers .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'")) .frameOptions(frame -> frame.sameOrigin()) .httpStrictTransportSecurity(hsts -> hsts .includeSubDomains(true) .maxAgeInSeconds(31536000) // 1年 ) ) .sessionManagement(session -> session .maximumSessions(1) // 单会话限制 .sessionFixation().migrateSession() ) -
审计日志:
@Bean public AuditorAware<String> auditorAware() { return () -> Optional.ofNullable(SecurityContextHolder.getContext()) .map(SecurityContext::getAuthentication) .filter(Authentication::isAuthenticated) .map(Authentication::getName); }
Spring Security 6 提供了强大而灵活的安全解决方案,通过这个快速入门指南,您可以
2288

被折叠的 条评论
为什么被折叠?



