Spring Security入門到實戰(zhàn)應(yīng)用
前言
Spring Security是Spring生態(tài)系統(tǒng)中最強大的安全框架,為Java應(yīng)用提供了全面的安全解決方案。它提供了認證、授權(quán)、防護等核心功能,廣泛應(yīng)用于企業(yè)級應(yīng)用開發(fā)。本文將從基礎(chǔ)概念到實戰(zhàn)應(yīng)用,全面講解Spring Security的核心知識點。
一、Spring Security概述
1.1 什么是Spring Security
Spring Security是一個功能強大且高度可定制的身份驗證和訪問控制框架,它為Java應(yīng)用提供:
- 身份認證(Authentication)
- 授權(quán)(Authorization)
- 防護常見攻擊(CSRF、Session Fixation等)
- 與Spring生態(tài)無縫集成
Spring Security核心架構(gòu) ┌─────────────────────────────────────────┐ │ Security Filter Chain │ │ ┌───────────────────────────────────┐ │ │ │ UsernamePasswordAuthFilter │ │ │ │ BasicAuthenticationFilter │ │ │ │ OAuth2AuthenticationFilter │ │ │ │ ExceptionTranslationFilter │ │ │ │ FilterSecurityInterceptor │ │ │ └───────────────────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────────┐ │ │ │ Authentication Manager │ │ │ └───────────────────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────────┐ │ │ │ Authentication Provider │ │ │ └───────────────────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────────┐ │ │ │ UserDetailsService │ │ │ └───────────────────────────────────┘ │ └─────────────────────────────────────────┘
1.2 核心概念
核心組件說明 ┌──────────────────────┬────────────────────────┐ │ 組件 │ 說明 │ ├──────────────────────┼────────────────────────┤ │ Authentication │ 認證信息(用戶憑證) │ │ SecurityContext │ 安全上下文(存儲認證) │ │ UserDetails │ 用戶詳情接口 │ │ UserDetailsService │ 加載用戶數(shù)據(jù) │ │ GrantedAuthority │ 授權(quán)信息(角色/權(quán)限) │ │ AccessDecisionManager│ 訪問決策管理器 │ └──────────────────────┴────────────────────────┘
二、快速開始
2.1 添加依賴
<!-- Spring Boot項目 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>2.2 基礎(chǔ)配置
/**
* Spring Security基礎(chǔ)配置
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置授權(quán)規(guī)則
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll() // 公開接口
.requestMatchers("/admin/**").hasRole("ADMIN") // 需要ADMIN角色
.anyRequest().authenticated() // 其他需要認證
)
// 配置表單登錄
.formLogin(form -> form
.loginPage("/login") // 自定義登錄頁
.defaultSuccessUrl("/home") // 登錄成功跳轉(zhuǎn)
.permitAll()
)
// 配置登出
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
/**
* 配置用戶詳情服務(wù)
*/
@Bean
public UserDetailsService userDetailsService() {
// 內(nèi)存用戶(僅用于測試)
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.3 基礎(chǔ)使用
/**
* 測試Controller
*/
@RestController
public class TestController {
@GetMapping("/public/hello")
public String publicHello() {
return "公開接口,無需認證";
}
@GetMapping("/user/profile")
public String userProfile() {
return "用戶信息,需要認證";
}
@GetMapping("/admin/dashboard")
public String adminDashboard() {
return "管理后臺,需要ADMIN角色";
}
@GetMapping("/current-user")
public String currentUser(Authentication authentication) {
return "當(dāng)前用戶: " + authentication.getName();
}
}三、認證(Authentication)
3.1 自定義UserDetailsService
/**
* 自定義用戶詳情服務(wù)
*/
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 從數(shù)據(jù)庫加載用戶
com.example.entity.User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用戶不存在: " + username));
// 轉(zhuǎn)換為Spring Security的UserDetails
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(getAuthorities(user))
.accountExpired(!user.isAccountNonExpired())
.accountLocked(!user.isAccountNonLocked())
.credentialsExpired(!user.isCredentialsNonExpired())
.disabled(!user.isEnabled())
.build();
}
/**
* 獲取用戶權(quán)限
*/
private Collection<? extends GrantedAuthority> getAuthorities(com.example.entity.User user) {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
}
}
/**
* 用戶實體
*/
@Entity
@Table(name = "users")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String email;
private boolean enabled = true;
private boolean accountNonExpired = true;
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = 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<>();
}
/**
* 角色實體
*/
@Entity
@Table(name = "roles")
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
private String description;
}3.2 自定義認證提供者
/**
* 自定義認證提供者
*/
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// 加載用戶
UserDetails user = userDetailsService.loadUserByUsername(username);
// 驗證密碼
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("密碼錯誤");
}
// 額外的業(yè)務(wù)校驗
if (!user.isEnabled()) {
throw new DisabledException("賬戶已禁用");
}
if (!user.isAccountNonLocked()) {
throw new LockedException("賬戶已鎖定");
}
// 創(chuàng)建認證令牌
return new UsernamePasswordAuthenticationToken(
user, password, user.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}3.3 登錄接口
/**
* 登錄控制器
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider jwtTokenProvider;
/**
* 用戶登錄
*/
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
// 認證
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 設(shè)置到安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成JWT Token
String token = jwtTokenProvider.generateToken(authentication);
return ResponseEntity.ok(new LoginResponse(token, "登錄成功"));
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("用戶名或密碼錯誤"));
} catch (DisabledException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("賬戶已禁用"));
}
}
/**
* 用戶注冊
*/
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
// 實現(xiàn)注冊邏輯
return ResponseEntity.ok("注冊成功");
}
}
@Data
class LoginRequest {
private String username;
private String password;
}
@Data
@AllArgsConstructor
class LoginResponse {
private String token;
private String message;
}
@Data
@AllArgsConstructor
class ErrorResponse {
private String message;
}
@Data
class RegisterRequest {
private String username;
private String password;
private String email;
}四、授權(quán)(Authorization)
4.1 基于URL的授權(quán)
/**
* URL授權(quán)配置
*/
@Configuration
@EnableWebSecurity
public class UrlAuthorizationConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// 公開接口
.requestMatchers("/", "/public/**", "/auth/**").permitAll()
// 需要認證
.requestMatchers("/user/**").authenticated()
// 角色授權(quán)
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
// 權(quán)限授權(quán)
.requestMatchers("/api/users/**").hasAuthority("USER_MANAGE")
.requestMatchers("/api/posts/**").hasAnyAuthority("POST_READ", "POST_WRITE")
// 使用SpEL表達式
.requestMatchers("/api/account/**").access(
new WebExpressionAuthorizationManager(
"hasRole('USER') and #username == authentication.name"
)
)
// 其他請求需要認證
.anyRequest().authenticated()
);
return http.build();
}
}4.2 基于方法的授權(quán)
/**
* 啟用方法級安全
*/
@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // 啟用@PreAuthorize/@PostAuthorize
securedEnabled = true, // 啟用@Secured
jsr250Enabled = true // 啟用@RolesAllowed
)
public class MethodSecurityConfig {
}
/**
* 方法級授權(quán)示例
*/
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* @PreAuthorize:方法執(zhí)行前檢查權(quán)限
*/
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
/**
* 基于參數(shù)的權(quán)限檢查
*/
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
/**
* 復(fù)雜表達式
*/
@PreAuthorize("hasRole('USER') and #user.username == authentication.name")
public void updateUser(User user) {
userRepository.save(user);
}
/**
* @PostAuthorize:方法執(zhí)行后檢查權(quán)限
*/
@PostAuthorize("returnObject.username == authentication.name or hasRole('ADMIN')")
public User findUser(Long id) {
return userRepository.findById(id).orElse(null);
}
/**
* @Secured:基于角色的簡單授權(quán)
*/
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
/**
* @RolesAllowed:JSR-250標(biāo)準(zhǔn)
*/
@RolesAllowed("ADMIN")
public void resetPassword(Long userId, String newPassword) {
User user = userRepository.findById(userId).orElseThrow();
user.setPassword(newPassword);
userRepository.save(user);
}
/**
* @PreFilter:過濾方法參數(shù)
*/
@PreFilter("filterObject.owner == authentication.name")
public void deleteItems(List<Item> items) {
items.forEach(item -> System.out.println("刪除: " + item.getName()));
}
/**
* @PostFilter:過濾返回結(jié)果
*/
@PostFilter("filterObject.owner == authentication.name")
public List<Item> getItems() {
// 返回所有數(shù)據(jù),Spring Security會過濾
return Arrays.asList(
new Item("Item1", "user1"),
new Item("Item2", "user2"),
new Item("Item3", "user1")
);
}
}
@Data
@AllArgsConstructor
class Item {
private String name;
private String owner;
}4.3 自定義權(quán)限評估器
/**
* 自定義權(quán)限評估器
*/
@Component("permissionEvaluator")
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private UserRepository userRepository;
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject,
Object permission) {
if (authentication == null || targetDomainObject == null || !(permission instanceof String)) {
return false;
}
String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase();
return hasPrivilege(authentication, targetType, permission.toString());
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission) {
if (authentication == null || targetType == null || !(permission instanceof String)) {
return false;
}
return hasPrivilege(authentication, targetType.toUpperCase(), permission.toString());
}
private boolean hasPrivilege(Authentication auth, String targetType, String permission) {
String username = auth.getName();
User user = userRepository.findByUsername(username).orElse(null);
if (user == null) {
return false;
}
// 檢查用戶權(quán)限
return user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.anyMatch(p -> p.getName().equals(targetType + "_" + permission));
}
}
/**
* 使用自定義權(quán)限評估器
*/
@Service
public class DocumentService {
/**
* 使用hasPermission檢查權(quán)限
*/
@PreAuthorize("hasPermission(#document, 'EDIT')")
public void editDocument(Document document) {
System.out.println("編輯文檔: " + document.getTitle());
}
@PreAuthorize("hasPermission(#documentId, 'DOCUMENT', 'DELETE')")
public void deleteDocument(Long documentId) {
System.out.println("刪除文檔: " + documentId);
}
}
@Data
class Document {
private Long id;
private String title;
private String owner;
}五、JWT認證
5.1 JWT工具類
/**
* JWT Token提供者
*/
@Component
public class JwtTokenProvider {
@Value("${jwt.secret:mySecretKey}")
private String jwtSecret;
@Value("${jwt.expiration:86400000}") // 24小時
private long jwtExpiration;
/**
* 生成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())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
/**
* 從Token獲取用戶名
*/
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
/**
* 驗證Token
*/
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
System.err.println("Invalid JWT signature");
} catch (MalformedJwtException ex) {
System.err.println("Invalid JWT token");
} catch (ExpiredJwtException ex) {
System.err.println("Expired JWT token");
} catch (UnsupportedJwtException ex) {
System.err.println("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
System.err.println("JWT claims string is empty");
}
return false;
}
}5.2 JWT認證過濾器
/**
* JWT認證過濾器
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
// 從請求頭獲取Token
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
// 從Token獲取用戶名
String username = tokenProvider.getUsernameFromToken(jwt);
// 加載用戶詳情
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 創(chuàng)建認證對象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 設(shè)置到安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
/**
* 從請求頭提取Token
*/
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}5.3 JWT配置
/**
* JWT安全配置
*/
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthEntryPoint;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF(使用JWT不需要)
.csrf(csrf -> csrf.disable())
// 禁用Session
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 異常處理
.exceptionHandling(exception -> exception
.authenticationEntryPoint(jwtAuthEntryPoint)
)
// 授權(quán)配置
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
);
// 添加JWT過濾器
http.addFilterBefore(
jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
/**
* JWT認證入口點(處理認證失?。?
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Map<String, Object> error = new HashMap<>();
error.put("status", 401);
error.put("error", "Unauthorized");
error.put("message", authException.getMessage());
error.put("path", request.getServletPath());
ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(error));
}
}六、實戰(zhàn)案例
6.1 案例1:多租戶權(quán)限隔離
/**
* 租戶上下文
*/
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setCurrentTenant(String tenant) {
currentTenant.set(tenant);
}
public static String getCurrentTenant() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
/**
* 租戶過濾器
*/
public class TenantFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
// 從請求頭獲取租戶ID
String tenantId = request.getHeader("X-Tenant-ID");
if (tenantId != null) {
TenantContext.setCurrentTenant(tenantId);
}
filterChain.doFilter(request, response);
} finally {
TenantContext.clear();
}
}
}
/**
* 租戶權(quán)限評估器
*/
@Component
public class TenantPermissionEvaluator {
/**
* 檢查用戶是否屬于當(dāng)前租戶
*/
public boolean belongsToTenant(Authentication authentication) {
String currentTenant = TenantContext.getCurrentTenant();
if (currentTenant == null) {
return false;
}
UserDetails user = (UserDetails) authentication.getPrincipal();
// 從用戶信息中獲取租戶ID并比較
return true; // 簡化實現(xiàn)
}
}
/**
* 使用租戶權(quán)限
*/
@RestController
@RequestMapping("/api/tenants")
public class TenantController {
@PreAuthorize("@tenantPermissionEvaluator.belongsToTenant(authentication)")
@GetMapping("/data")
public List<TenantData> getTenantData() {
String tenantId = TenantContext.getCurrentTenant();
return Arrays.asList(new TenantData(tenantId, "數(shù)據(jù)"));
}
}
@Data
@AllArgsConstructor
class TenantData {
private String tenantId;
private String data;
}6.2 案例2:動態(tài)權(quán)限管理
/**
* 動態(tài)權(quán)限服務(wù)
*/
@Service
public class DynamicPermissionService {
@Autowired
private PermissionRepository permissionRepository;
/**
* 檢查動態(tài)權(quán)限
*/
public boolean hasPermission(String username, String resource, String action) {
// 從數(shù)據(jù)庫查詢用戶權(quán)限
List<Permission> permissions = permissionRepository.findByUsername(username);
return permissions.stream()
.anyMatch(p -> p.getResource().equals(resource) &&
p.getAction().equals(action));
}
}
/**
* 權(quán)限實體
*/
@Entity
@Table(name = "permissions")
@Data
class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String resource;
private String action;
}
/**
* 動態(tài)權(quán)限攔截器
*/
@Component
@Aspect
public class DynamicPermissionAspect {
@Autowired
private DynamicPermissionService permissionService;
@Around("@annotation(requirePermission)")
public Object checkPermission(ProceedingJoinPoint joinPoint,
RequirePermission requirePermission)
throws Throwable {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
boolean hasPermission = permissionService.hasPermission(
username,
requirePermission.resource(),
requirePermission.action()
);
if (!hasPermission) {
throw new AccessDeniedException("權(quán)限不足");
}
return joinPoint.proceed();
}
}
/**
* 自定義權(quán)限注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface RequirePermission {
String resource();
String action();
}
/**
* 使用動態(tài)權(quán)限
*/
@RestController
@RequestMapping("/api/users")
public class DynamicPermissionController {
@RequirePermission(resource = "USER", action = "CREATE")
@PostMapping
public String createUser(@RequestBody User user) {
return "創(chuàng)建用戶成功";
}
@RequirePermission(resource = "USER", action = "DELETE")
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
return "刪除用戶成功";
}
}6.3 案例3:OAuth2社交登錄
/**
* OAuth2配置
*/
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**", "/oauth2/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(oauth2UserService())
)
.successHandler(oauth2AuthenticationSuccessHandler())
);
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
return new CustomOAuth2UserService();
}
@Bean
public AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler() {
return new OAuth2AuthenticationSuccessHandler();
}
}
/**
* 自定義OAuth2用戶服務(wù)
*/
public class CustomOAuth2UserService
extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
OAuth2User oauth2User = super.loadUser(userRequest);
// 處理OAuth2用戶信息
return processOAuth2User(userRequest, oauth2User);
}
private OAuth2User processOAuth2User(OAuth2UserRequest userRequest,
OAuth2User oauth2User) {
String registrationId = userRequest.getClientRegistration()
.getRegistrationId();
String email = oauth2User.getAttribute("email");
// 查找或創(chuàng)建用戶
User user = userRepository.findByEmail(email)
.orElseGet(() -> {
User newUser = new User();
newUser.setEmail(email);
newUser.setUsername(oauth2User.getAttribute("name"));
newUser.setEnabled(true);
return userRepository.save(newUser);
});
return new CustomOAuth2User(user, oauth2User.getAttributes());
}
}
/**
* OAuth2成功處理器
*/
public class OAuth2AuthenticationSuccessHandler
extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException {
// 生成JWT Token
String token = tokenProvider.generateToken(authentication);
// 重定向到前端,攜帶Token
String targetUrl = "http://localhost:3000/oauth2/redirect?token=" + token;
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}6.4 案例4:驗證碼登錄
/**
* 驗證碼服務(wù)
*/
@Service
public class CaptchaService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 生成驗證碼
*/
public String generateCaptcha(String mobile) {
String code = String.format("%06d", new Random().nextInt(999999));
// 存儲到Redis,5分鐘過期
redisTemplate.opsForValue().set(
"captcha:" + mobile, code, 5, TimeUnit.MINUTES
);
// 實際應(yīng)用中,這里調(diào)用短信服務(wù)發(fā)送驗證碼
System.out.println("驗證碼: " + code);
return code;
}
/**
* 驗證驗證碼
*/
public boolean verifyCaptcha(String mobile, String code) {
String storedCode = redisTemplate.opsForValue().get("captcha:" + mobile);
return code.equals(storedCode);
}
}
/**
* 驗證碼認證Token
*/
public class CaptchaAuthenticationToken extends AbstractAuthenticationToken {
private final String mobile;
private String captcha;
public CaptchaAuthenticationToken(String mobile, String captcha) {
super(null);
this.mobile = mobile;
this.captcha = captcha;
setAuthenticated(false);
}
public CaptchaAuthenticationToken(String mobile,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.mobile = mobile;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return captcha;
}
@Override
public Object getPrincipal() {
return mobile;
}
public String getMobile() {
return mobile;
}
}
/**
* 驗證碼認證提供者
*/
@Component
public class CaptchaAuthenticationProvider implements AuthenticationProvider {
@Autowired
private CaptchaService captchaService;
@Autowired
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
CaptchaAuthenticationToken token = (CaptchaAuthenticationToken) authentication;
String mobile = token.getMobile();
String captcha = (String) token.getCredentials();
// 驗證驗證碼
if (!captchaService.verifyCaptcha(mobile, captcha)) {
throw new BadCredentialsException("驗證碼錯誤");
}
// 加載用戶
UserDetails user = userDetailsService.loadUserByUsername(mobile);
return new CaptchaAuthenticationToken(mobile, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return CaptchaAuthenticationToken.class.isAssignableFrom(authentication);
}
}
/**
* 驗證碼登錄接口
*/
@RestController
@RequestMapping("/auth")
public class CaptchaAuthController {
@Autowired
private CaptchaService captchaService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider jwtTokenProvider;
/**
* 發(fā)送驗證碼
*/
@PostMapping("/captcha/send")
public ResponseEntity<?> sendCaptcha(@RequestParam String mobile) {
captchaService.generateCaptcha(mobile);
return ResponseEntity.ok("驗證碼已發(fā)送");
}
/**
* 驗證碼登錄
*/
@PostMapping("/captcha/login")
public ResponseEntity<?> loginWithCaptcha(@RequestBody CaptchaLoginRequest request) {
try {
Authentication authentication = authenticationManager.authenticate(
new CaptchaAuthenticationToken(request.getMobile(), request.getCaptcha())
);
String token = jwtTokenProvider.generateToken(authentication);
return ResponseEntity.ok(new LoginResponse(token, "登錄成功"));
} catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse(e.getMessage()));
}
}
}
@Data
class CaptchaLoginRequest {
private String mobile;
private String captcha;
}七、安全最佳實踐
7.1 CSRF防護
/**
* CSRF配置
*/
@Configuration
public class CsrfConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 啟用CSRF保護
.csrf(csrf -> csrf
// 忽略某些URL
.ignoringRequestMatchers("/api/**")
// 使用Cookie存儲Token
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
}7.2 密碼策略
/**
* 密碼編碼器配置
*/
@Configuration
public class PasswordConfig {
/**
* 使用BCrypt編碼器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 強度為12
}
/**
* 密碼驗證規(guī)則
*/
public boolean isValidPassword(String password) {
// 至少8位
if (password.length() < 8) {
return false;
}
// 包含大寫字母、小寫字母、數(shù)字、特殊字符
String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
return password.matches(regex);
}
}7.3 會話管理
/**
* 會話管理配置
*/
@Configuration
public class SessionConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
// 會話創(chuàng)建策略
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
// 最大會話數(shù)
.maximumSessions(1)
// 阻止新會話
.maxSessionsPreventsLogin(true)
// 會話過期URL
.expiredUrl("/login?expired")
);
return http.build();
}
}八、總結(jié)
核心知識點回顧
Spring Security核心要點
│
├── 核心概念
│ ├── Authentication(認證)
│ ├── Authorization(授權(quán))
│ ├── SecurityContext(安全上下文)
│ └── FilterChain(過濾器鏈)
│
├── 認證方式
│ ├── 表單登錄
│ ├── HTTP Basic
│ ├── JWT Token
│ ├── OAuth2
│ └── 驗證碼登錄
│
├── 授權(quán)機制
│ ├── URL授權(quán)
│ ├── 方法授權(quán)
│ ├── 表達式授權(quán)
│ └── 自定義權(quán)限評估
│
├── 高級特性
│ ├── 多租戶隔離
│ ├── 動態(tài)權(quán)限
│ ├── 社交登錄
│ └── 驗證碼認證
│
└── 最佳實踐
├── CSRF防護
├── 密碼策略
├── 會話管理
└── 安全配置Spring Security是一個功能強大的安全框架,掌握其核心概念和使用方法對于構(gòu)建安全的企業(yè)級應(yīng)用至關(guān)重要。在實際應(yīng)用中,需要根據(jù)業(yè)務(wù)需求選擇合適的認證和授權(quán)策略,并遵循安全最佳實踐。
到此這篇關(guān)于Spring Security入門到實戰(zhàn)應(yīng)用的文章就介紹到這了,更多相關(guān)Spring Security使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring schedule配置多任務(wù)動態(tài)cron(增刪啟停)
這篇文章主要介紹了spring schedule配置多任務(wù)動態(tài)cron(增刪啟停),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Java的面向?qū)ο缶幊袒靖拍顚W(xué)習(xí)筆記整理
這篇文章主要介紹了Java的面向?qū)ο缶幊袒靖拍顚W(xué)習(xí)筆記整理,包括類與方法以及多態(tài)等支持面向?qū)ο笳Z言中的重要特點,需要的朋友可以參考下2016-01-01
SpringBoot URL帶有特殊字符([]/{}等),報400錯誤的解決
這篇文章主要介紹了SpringBoot URL帶有特殊字符([]/{}等),報400錯誤的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
IDEA SpringBoot項目配置熱更新的步驟詳解(無需每次手動重啟服務(wù)器)
這篇文章主要介紹了IDEA SpringBoot項目配置熱更新的步驟,無需每次手動重啟服務(wù)器,本文通過圖文實例代碼相結(jié)合給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04
spring?boot如何通過自定義注解和AOP攔截指定的請求
這篇文章主要介紹了spring?boot通過自定義注解和AOP攔截指定的請求,本文主要通過切面類和自定注解的方式,攔截指定的接口(代碼中已經(jīng)作了詳細的說明),需要的朋友可以參考下2024-06-06
spring cloud gateway請求跨域問題解決方案
這篇文章主要介紹了spring cloud gateway請求跨域問題解決方案,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-01-01

