Spring Security 集成
本文档介绍如何在 LCGYL Framework 中集成 Spring Security,实现安全认证和授权。
快速开始
添加依赖
gradle
dependencies {
implementation 'com.lcgyl:lcgyl-spring-security:${version}'
implementation 'org.springframework.boot:spring-boot-starter-security'
}xml
<dependencies>
<dependency>
<groupId>com.lcgyl</groupId>
<artifactId>lcgyl-spring-security</artifactId>
<version>${version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>基本配置
yaml
spring:
security:
user:
name: admin
password: admin123
lcgyl:
security:
enabled: true
jwt:
enabled: true
secret: your-256-bit-secret-key-here
expiration: 3600安全配置
配置类
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").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();
}
}CORS 配置
java
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}用户认证
UserDetailsService
java
@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("用户不存在: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}自定义 UserDetails
java
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !user.isLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
public User getUser() {
return user;
}
}JWT 认证
JWT 工具类
java
@Component
public class JwtTokenProvider {
@Value("${lcgyl.security.jwt.secret}")
private String secret;
@Value("${lcgyl.security.jwt.expiration}")
private long expiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public boolean isTokenExpired(String token) {
Date expiration = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getExpiration();
return expiration.before(new Date());
}
}JWT 过滤器
java
@Component
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 {
String token = extractToken(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}认证控制器
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = tokenProvider.generateToken(userDetails);
return ResponseEntity.ok(new AuthResponse(token));
}
@PostMapping("/register")
public ResponseEntity<User> register(@RequestBody RegisterRequest request) {
// 注册逻辑
}
@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(@RequestHeader("Authorization") String token) {
// 刷新令牌逻辑
}
}方法级安全
启用方法安全
java
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {
}使用注解
java
@Service
public class UserService {
// 需要 ADMIN 角色
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// 需要特定权限
@PreAuthorize("hasAuthority('user:write')")
public User createUser(User user) {
return userRepository.save(user);
}
// 只能访问自己的数据
@PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
// 后置检查
@PostAuthorize("returnObject.userId == authentication.principal.id")
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId).orElse(null);
}
// 过滤集合
@PostFilter("filterObject.userId == authentication.principal.id")
public List<Order> getOrders() {
return orderRepository.findAll();
}
// 使用 @Secured
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void adminOperation() {
}
}自定义权限评估器
java
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private PermissionService permissionService;
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject, Object permission) {
if (authentication == null || targetDomainObject == null) {
return false;
}
String username = authentication.getName();
String permissionStr = permission.toString();
return permissionService.hasPermission(username,
targetDomainObject.getClass().getSimpleName(), permissionStr);
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId, String targetType, Object permission) {
if (authentication == null) {
return false;
}
String username = authentication.getName();
return permissionService.hasPermission(username, targetType,
targetId.toString(), permission.toString());
}
}
// 使用
@PreAuthorize("hasPermission(#order, 'write')")
public void updateOrder(Order order) {
}LCGYL 安全集成
集成 LCGYL 权限注解
java
@Aspect
@Component
public class LcgylSecurityAspect {
@Before("@annotation(requiresPermission)")
public void checkPermission(JoinPoint joinPoint, RequiresPermission requiresPermission) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new AccessDeniedException("未认证");
}
String permission = requiresPermission.value();
boolean hasPermission = authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals(permission));
if (!hasPermission) {
throw new AccessDeniedException("权限不足: " + permission);
}
}
@Before("@annotation(requiresRole)")
public void checkRole(JoinPoint joinPoint, RequiresRole requiresRole) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new AccessDeniedException("未认证");
}
String role = "ROLE_" + requiresRole.value();
boolean hasRole = authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals(role));
if (!hasRole) {
throw new AccessDeniedException("需要角色: " + requiresRole.value());
}
}
}安全上下文桥接
java
@Component
public class SecurityContextBridge {
// 将 Spring Security 上下文同步到 LCGYL
@EventListener
public void onAuthentication(AuthenticationSuccessEvent event) {
Authentication auth = event.getAuthentication();
if (auth.getPrincipal() instanceof CustomUserDetails userDetails) {
com.lcgyl.framework.security.SecurityContext.setCurrentUser(
userDetails.getUser()
);
}
}
// 获取当前用户
public User getCurrentUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails userDetails) {
return userDetails.getUser();
}
return null;
}
}OAuth2 集成
OAuth2 配置
yaml
spring:
security:
oauth2:
client:
registration:
github:
client-id: your-client-id
client-secret: your-client-secret
google:
client-id: your-client-id
client-secret: your-client-secretOAuth2 安全配置
java
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
)
);
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
return new CustomOAuth2UserService();
}
}自定义 OAuth2 用户服务
java
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oauth2User.getAttribute("id").toString();
String email = oauth2User.getAttribute("email");
String name = oauth2User.getAttribute("name");
// 查找或创建用户
User user = userRepository.findByProviderAndProviderId(provider, providerId)
.orElseGet(() -> {
User newUser = new User();
newUser.setProvider(provider);
newUser.setProviderId(providerId);
newUser.setEmail(email);
newUser.setName(name);
return userRepository.save(newUser);
});
return new CustomOAuth2User(oauth2User, user);
}
}异常处理
认证异常处理
java
@Component
public class CustomAuthenticationEntryPoint 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);
response.getWriter().write(
"{\"code\":401,\"message\":\"未认证: " + authException.getMessage() + "\"}"
);
}
}授权异常处理
java
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(
"{\"code\":403,\"message\":\"权限不足: " + accessDeniedException.getMessage() + "\"}"
);
}
}最佳实践
1. 密码安全
java
// ✅ 推荐:使用 BCrypt
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
// ❌ 不推荐:使用弱加密
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}2. JWT 安全
java
// ✅ 推荐:使用足够长的密钥
lcgyl.security.jwt.secret=your-256-bit-secret-key-must-be-long-enough
// ✅ 推荐:设置合理的过期时间
lcgyl.security.jwt.expiration=3600 // 1小时3. 最小权限原则
java
// ✅ 推荐:精细权限控制
@PreAuthorize("hasAuthority('order:delete')")
public void deleteOrder(Long id) { }
// ❌ 不推荐:粗粒度权限
@PreAuthorize("hasRole('USER')")
public void deleteOrder(Long id) { }常见问题
Q: 如何处理跨域认证?
A: 配置 CORS 并在响应中包含认证头。
Q: JWT 过期后如何处理?
A: 使用 Refresh Token 机制刷新 Access Token。
Q: 如何实现记住我功能?
A: 使用 Spring Security 的 Remember-Me 功能或延长 JWT 有效期。
下一步
- Spring Cloud 集成 - 微服务安全
- 安全认证 - 更多安全特性
- Web 开发 - Web 安全