Spring Security 6 【1-快速入门指南】

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. 安全配置解析

  1. 请求授权

    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/", "/home", "/public/**", "/css/**", "/js/**").permitAll()
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
        .anyRequest().authenticated()
    )
    
  2. 表单登录

    .formLogin(form -> form
        .loginPage("/login")
        .defaultSuccessUrl("/dashboard", true)
        .permitAll()
    )
    
  3. 登出功能

    .logout(logout -> logout
        .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
        .logoutSuccessUrl("/login?logout")
        .invalidateHttpSession(true)
        .deleteCookies("JSESSIONID")
        .permitAll()
    )
    
  4. 异常处理

    .exceptionHandling(exception -> exception
        .accessDeniedPage("/access-denied")
    )
    

2. 用户管理

Spring Security 6 提供多种用户存储方式:

  1. 内存用户存储

    @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);
    }
    
  2. JDBC用户存储

    @Bean
    public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {
        return new JdbcUserDetailsManager(dataSource);
    }
    
  3. 自定义用户服务

    @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();
}

运行和测试

  1. 启动应用程序:

    ./mvnw spring-boot:run
    
  2. 访问以下端点:

    • http://localhost:8080 - 公共主页
    • http://localhost:8080/login - 登录页面
    • http://localhost:8080/dashboard - 用户仪表盘(需要登录)
    • http://localhost:8080/admin/dashboard - 管理员仪表盘
    • http://localhost:8080/api/public - 公共API
    • http://localhost:8080/api/greet - 受保护API
  3. 测试用户:

    • 普通用户:user/password
    • 管理员:admin/admin

最佳实践

  1. 生产环境建议

    • 使用数据库存储用户凭据
    • 配置 HTTPS
    • 设置强密码策略
    • 实施多因素认证
  2. 安全加固

    .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()
    )
    
  3. 审计日志

    @Bean
    public AuditorAware<String> auditorAware() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getName);
    }
    

Spring Security 6 提供了强大而灵活的安全解决方案,通过这个快速入门指南,您可以

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值