乐闻世界logo
搜索文章和话题

Spring Boot 中如何实现安全认证(Spring Security)?

3月6日 21:59

Spring Boot + Spring Security 安全认证详解

Spring Security 核心功能

  • 认证(Authentication):验证用户身份
  • 授权(Authorization):控制用户访问权限
  • 防护(Protection):CSRF、会话固定等攻击防护
  • 加密(Encryption):密码加密存储

基础集成

1. 添加依赖

xml
<dependencies> <!-- Spring Security Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- JWT 支持(可选) --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.12.3</version> </dependency> <!-- OAuth2 支持(可选) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> </dependencies>

2. 基础安全配置

java
@Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 禁用 CSRF(仅用于 API 开发,Web 应用需启用) .csrf(csrf -> csrf.disable()) // 配置授权规则 .authorizeHttpRequests(auth -> auth // 公开路径 .requestMatchers("/", "/login", "/register", "/public/**").permitAll() // 静态资源 .requestMatchers("/css/**", "/js/**", "/images/**").permitAll() // API 文档 .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // 管理员路径 .requestMatchers("/admin/**").hasRole("ADMIN") // 用户路径 .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // 其他需要认证 .anyRequest().authenticated() ) // 表单登录配置 .formLogin(form -> form .loginPage("/login") .loginProcessingUrl("/login") .defaultSuccessUrl("/home", false) .failureUrl("/login?error=true") .permitAll() ) // 注销配置 .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout=true") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() ) // 会话管理 .sessionManagement(session -> session .maximumSessions(1) .maxSessionsPreventsLogin(false) ); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { // 使用 BCrypt 加密 return new BCryptPasswordEncoder(); } }

基于内存的用户认证

java
@Configuration public class InMemoryUserConfig { @Bean public UserDetailsService userDetailsService() { UserDetails user = User.builder() .username("user") .password(new BCryptPasswordEncoder().encode("password")) .roles("USER") .build(); UserDetails admin = User.builder() .username("admin") .password(new BCryptPasswordEncoder().encode("admin")) .roles("ADMIN", "USER") .build(); return new InMemoryUserDetailsManager(user, admin); } }

基于数据库的用户认证

1. 用户实体类

java
@Entity @Table(name = "users") @Data public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; @Column(nullable = false) private String password; @Column(unique = true, nullable = false) private String email; private boolean enabled = true; @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id") ) private Set<Role> roles = new HashSet<>(); @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName())) .collect(Collectors.toList()); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } } @Entity @Table(name = "roles") @Data public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String name; }

2. 自定义 UserDetailsService

java
@Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); return org.springframework.security.core.userdetails.User.builder() .username(user.getUsername()) .password(user.getPassword()) .roles(user.getRoles().stream() .map(Role::getName) .toArray(String[]::new)) .disabled(!user.isEnabled()) .build(); } }

3. 更新 Security 配置

java
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final CustomUserDetailsService userDetailsService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/register").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard") .permitAll() ) .logout(logout -> logout.permitAll()) .userDetailsService(userDetailsService); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }

JWT 认证实现

1. JWT 工具类

java
@Component public class JwtTokenProvider { @Value("${jwt.secret}") private String jwtSecret; @Value("${jwt.expiration}") private long jwtExpiration; private final SecretKey key; public JwtTokenProvider() { this.key = Keys.secretKeyFor(SignatureAlgorithm.HS256); } /** * 生成 JWT Token */ public String generateToken(Authentication authentication) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpiration); return Jwts.builder() .setSubject(userDetails.getUsername()) .claim("roles", userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(key) .compact(); } /** * 从 Token 获取用户名 */ public String getUsernameFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } /** * 验证 Token */ public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } }

2. JWT 认证过滤器

java
@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider tokenProvider; private final CustomUserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { String username = tokenProvider.getUsernameFromToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { logger.error("Cannot set user authentication: {}", e.getMessage()); } filterChain.doFilter(request, response); } private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }

3. JWT Security 配置

java
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class JwtSecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomUserDetailsService userDetailsService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }

4. 认证 Controller

java
@RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthController { private final AuthenticationManager authenticationManager; private final JwtTokenProvider tokenProvider; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @PostMapping("/login") public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest request) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = tokenProvider.generateToken(authentication); return ResponseEntity.ok(new JwtAuthResponse(jwt)); } @PostMapping("/register") public ResponseEntity<?> registerUser(@RequestBody RegisterRequest request) { if (userRepository.existsByUsername(request.getUsername())) { return ResponseEntity.badRequest() .body(new ApiResponse(false, "Username already taken")); } User user = new User(); user.setUsername(request.getUsername()); user.setEmail(request.getEmail()); user.setPassword(passwordEncoder.encode(request.getPassword())); userRepository.save(user); return ResponseEntity.ok(new ApiResponse(true, "User registered successfully")); } }

方法级安全控制

java
@RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping("/me") @PreAuthorize("isAuthenticated()") public ResponseEntity<User> getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) { return ResponseEntity.ok(userService.findByUsername(userDetails.getUsername())); } @GetMapping("/{id}") @PreAuthorize("hasRole('ADMIN') or @userSecurity.hasUserId(authentication, #id)") public ResponseEntity<User> getUserById(@PathVariable Long id) { return ResponseEntity.ok(userService.findById(id)); } @DeleteMapping("/{id}") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity<?> deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.ok().build(); } @PostMapping @PreAuthorize("hasRole('ADMIN')") public ResponseEntity<User> createUser(@RequestBody User user) { return ResponseEntity.ok(userService.createUser(user)); } } // 自定义权限判断 @Component("userSecurity") public class UserSecurity { public boolean hasUserId(Authentication authentication, Long userId) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); User user = userRepository.findByUsername(userDetails.getUsername()) .orElse(null); return user != null && user.getId().equals(userId); } }

OAuth2 / OIDC 集成

java
@Configuration @EnableWebSecurity public class OAuth2SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 .loginPage("/login") .defaultSuccessUrl("/home") .userInfoEndpoint(userInfo -> userInfo .userAuthoritiesMapper(userAuthoritiesMapper()) ) ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) ); return http.build(); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); authoritiesConverter.setAuthorityPrefix("ROLE_"); authoritiesConverter.setAuthoritiesClaimName("roles"); JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); return converter; } }

安全配置

yaml
spring: security: user: name: admin password: admin roles: ADMIN oauth2: client: registration: google: client-id: your-client-id client-secret: your-client-secret scope: - email - profile jwt: secret: your-secret-key-here-must-be-at-least-256-bits-long expiration: 86400000 # 24 hours

测试安全配置

java
@SpringBootTest @AutoConfigureMockMvc public class SecurityTest { @Autowired private MockMvc mockMvc; @Test @WithMockUser(username = "user", roles = "USER") public void testUserAccess() throws Exception { mockMvc.perform(get("/api/user/profile")) .andExpect(status().isOk()); } @Test @WithMockUser(username = "admin", roles = "ADMIN") public void testAdminAccess() throws Exception { mockMvc.perform(get("/api/admin/users")) .andExpect(status().isOk()); } @Test @WithMockUser(username = "user", roles = "USER") public void testUserCannotAccessAdmin() throws Exception { mockMvc.perform(get("/api/admin/users")) .andExpect(status().isForbidden()); } @Test public void testPublicAccess() throws Exception { mockMvc.perform(get("/public/info")) .andExpect(status().isOk()); } @Test public void testProtectedRequiresAuth() throws Exception { mockMvc.perform(get("/api/protected")) .andExpect(status().isUnauthorized()); } }

总结

认证方式适用场景优点缺点
Session传统 Web 应用简单、成熟不适合分布式
JWTREST API、移动端无状态、可扩展Token 无法撤销
OAuth2第三方登录标准化、安全实现复杂
LDAP/AD企业环境集中管理需要 LDAP 服务器

安全建议:

  1. 始终使用 HTTPS
  2. 密码使用 BCrypt 加密
  3. 启用 CSRF 防护(Web 应用)
  4. 设置合理的会话超时
  5. 实现密码强度校验
  6. 添加登录失败锁定机制
标签:Spring Boot