Skip to content

安全认证

LCGYL Framework 提供全面的安全认证支持,包括身份认证、权限控制、加密解密等功能。

安全配置

基本配置

properties
# application.properties
security.enabled=true
security.session.timeout=3600
security.remember-me.enabled=true
security.remember-me.token-validity=604800
yaml
# application.yaml
security:
  enabled: true
  session:
    timeout: 3600
  remember-me:
    enabled: true
    token-validity: 604800

JWT 配置

properties
security.jwt.enabled=true
security.jwt.secret=your-secret-key-at-least-256-bits
security.jwt.expiration=3600
security.jwt.refresh-expiration=604800
security.jwt.issuer=lcgyl-app
yaml
# application.yaml
security:
  jwt:
    enabled: true
    secret: your-secret-key-at-least-256-bits
    expiration: 3600
    refresh-expiration: 604800
    issuer: lcgyl-app

身份认证

用户认证

java
@Component
public class AuthenticationService {
    
    @Inject
    private UserRepository userRepository;
    
    @Inject
    private PasswordEncoder passwordEncoder;
    
    @Inject
    private TokenService tokenService;
    
    public AuthResult authenticate(String username, String password) {
        // 查找用户
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new AuthenticationException("用户不存在");
        }
        
        // 验证密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new AuthenticationException("密码错误");
        }
        
        // 检查账户状态
        if (!user.isEnabled()) {
            throw new AuthenticationException("账户已禁用");
        }
        
        // 生成令牌
        String accessToken = tokenService.generateAccessToken(user);
        String refreshToken = tokenService.generateRefreshToken(user);
        
        return new AuthResult(accessToken, refreshToken, user);
    }
}

自定义认证提供者

java
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    @Inject
    private UserService userService;
    
    @Inject
    private PasswordEncoder passwordEncoder;
    
    @Override
    public Authentication authenticate(Authentication authentication) {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new BadCredentialsException("用户不存在");
        }
        
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("密码错误");
        }
        
        return new UsernamePasswordAuthenticationToken(
            user,
            null,
            user.getAuthorities()
        );
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

JWT 认证

生成 JWT

java
@Component
public class JwtTokenService {
    
    @Value("security.jwt.secret")
    private String secret;
    
    @Value("security.jwt.expiration")
    private long expiration;
    
    public String generateToken(User user) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration * 1000);
        
        return Jwts.builder()
            .setSubject(user.getId().toString())
            .claim("username", user.getUsername())
            .claim("roles", user.getRoles())
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
    
    public Claims parseToken(String token) {
        return Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }
    
    public String refreshToken(String token) {
        Claims claims = parseToken(token);
        claims.setIssuedAt(new Date());
        claims.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000));
        
        return Jwts.builder()
            .setClaims(claims)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
}

JWT 过滤器

java
@Component
public class JwtAuthenticationFilter implements Filter {
    
    @Inject
    private JwtTokenService tokenService;
    
    @Inject
    private UserService userService;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String token = extractToken(httpRequest);
        
        if (token != null && tokenService.validateToken(token)) {
            Claims claims = tokenService.parseToken(token);
            Long userId = Long.parseLong(claims.getSubject());
            User user = userService.findById(userId);
            
            if (user != null) {
                SecurityContext.setCurrentUser(user);
            }
        }
        
        try {
            chain.doFilter(request, response);
        } finally {
            SecurityContext.clear();
        }
    }
    
    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

权限控制

基于注解的权限控制

java
@Controller
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping
    @RequiresPermission("user:read")
    public List<User> list() {
        return userService.findAll();
    }
    
    @PostMapping
    @RequiresPermission("user:create")
    public User create(@RequestBody User user) {
        return userService.create(user);
    }
    
    @DeleteMapping("/{id}")
    @RequiresPermission("user:delete")
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }
    
    @GetMapping("/admin")
    @RequiresRole("ADMIN")
    public List<User> adminList() {
        return userService.findAll();
    }
    
    @PostMapping("/sensitive")
    @RequiresRoles(value = {"ADMIN", "MANAGER"}, logical = Logical.OR)
    public void sensitiveOperation() {
        // 需要 ADMIN 或 MANAGER 角色
    }
}

权限检查切面

java
@Aspect
@Component
public class SecurityAspect {
    
    @Before("@annotation(requiresPermission)")
    public void checkPermission(JoinPoint joinPoint, RequiresPermission requiresPermission) {
        User user = SecurityContext.getCurrentUser();
        
        if (user == null) {
            throw new UnauthorizedException("请先登录");
        }
        
        String permission = requiresPermission.value();
        if (!user.hasPermission(permission)) {
            throw new ForbiddenException("权限不足: " + permission);
        }
    }
    
    @Before("@annotation(requiresRole)")
    public void checkRole(JoinPoint joinPoint, RequiresRole requiresRole) {
        User user = SecurityContext.getCurrentUser();
        
        if (user == null) {
            throw new UnauthorizedException("请先登录");
        }
        
        String role = requiresRole.value();
        if (!user.hasRole(role)) {
            throw new ForbiddenException("需要角色: " + role);
        }
    }
}

编程式权限检查

java
@Component
public class OrderService {
    
    @Inject
    private SecurityContext securityContext;
    
    public void deleteOrder(Long orderId) {
        User user = securityContext.getCurrentUser();
        Order order = orderRepository.findById(orderId);
        
        // 检查是否是订单所有者或管理员
        if (!order.getUserId().equals(user.getId()) && !user.hasRole("ADMIN")) {
            throw new ForbiddenException("无权删除此订单");
        }
        
        orderRepository.delete(orderId);
    }
}

密码加密

使用 BCrypt

java
@Component
public class BCryptPasswordEncoder implements PasswordEncoder {
    
    private static final int STRENGTH = 12;
    
    @Override
    public String encode(String rawPassword) {
        return BCrypt.hashpw(rawPassword, BCrypt.gensalt(STRENGTH));
    }
    
    @Override
    public boolean matches(String rawPassword, String encodedPassword) {
        return BCrypt.checkpw(rawPassword, encodedPassword);
    }
}

使用示例

java
@Component
public class UserService {
    
    @Inject
    private PasswordEncoder passwordEncoder;
    
    public User createUser(String username, String password) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password));
        return userRepository.save(user);
    }
    
    public void changePassword(Long userId, String oldPassword, String newPassword) {
        User user = userRepository.findById(userId);
        
        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
            throw new BadCredentialsException("原密码错误");
        }
        
        user.setPassword(passwordEncoder.encode(newPassword));
        userRepository.save(user);
    }
}

数据加密

AES 加密

java
@Component
public class AESEncryptionService {
    
    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_IV_LENGTH = 12;
    private static final int GCM_TAG_LENGTH = 128;
    
    @Value("security.encryption.key")
    private String secretKey;
    
    public String encrypt(String plainText) throws Exception {
        SecretKey key = new SecretKeySpec(secretKey.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        
        byte[] iv = new byte[GCM_IV_LENGTH];
        SecureRandom random = new SecureRandom();
        random.nextBytes(iv);
        
        GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
        
        byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        
        byte[] encrypted = new byte[iv.length + cipherText.length];
        System.arraycopy(iv, 0, encrypted, 0, iv.length);
        System.arraycopy(cipherText, 0, encrypted, iv.length, cipherText.length);
        
        return Base64.getEncoder().encodeToString(encrypted);
    }
    
    public String decrypt(String encryptedText) throws Exception {
        byte[] encrypted = Base64.getDecoder().decode(encryptedText);
        
        byte[] iv = new byte[GCM_IV_LENGTH];
        System.arraycopy(encrypted, 0, iv, 0, iv.length);
        
        byte[] cipherText = new byte[encrypted.length - GCM_IV_LENGTH];
        System.arraycopy(encrypted, GCM_IV_LENGTH, cipherText, 0, cipherText.length);
        
        SecretKey key = new SecretKeySpec(secretKey.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
        
        byte[] plainText = cipher.doFinal(cipherText);
        return new String(plainText, StandardCharsets.UTF_8);
    }
}

敏感数据加密

java
@Component
public class SensitiveDataService {
    
    @Inject
    private AESEncryptionService encryptionService;
    
    public User saveUser(User user) {
        // 加密敏感数据
        if (user.getIdCard() != null) {
            user.setIdCard(encryptionService.encrypt(user.getIdCard()));
        }
        if (user.getBankAccount() != null) {
            user.setBankAccount(encryptionService.encrypt(user.getBankAccount()));
        }
        
        return userRepository.save(user);
    }
    
    public User getUser(Long id) {
        User user = userRepository.findById(id);
        
        // 解密敏感数据
        if (user.getIdCard() != null) {
            user.setIdCard(encryptionService.decrypt(user.getIdCard()));
        }
        if (user.getBankAccount() != null) {
            user.setBankAccount(encryptionService.decrypt(user.getBankAccount()));
        }
        
        return user;
    }
}

CSRF 防护

配置 CSRF

properties
security.csrf.enabled=true
security.csrf.cookie-name=XSRF-TOKEN
security.csrf.header-name=X-XSRF-TOKEN
yaml
# application.yaml
security:
  csrf:
    enabled: true
    cookie-name: XSRF-TOKEN
    header-name: X-XSRF-TOKEN

CSRF 过滤器

java
@Component
public class CsrfFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 跳过 GET 请求
        if ("GET".equals(httpRequest.getMethod())) {
            chain.doFilter(request, response);
            return;
        }
        
        // 验证 CSRF Token
        String cookieToken = getCsrfTokenFromCookie(httpRequest);
        String headerToken = httpRequest.getHeader("X-XSRF-TOKEN");
        
        if (cookieToken == null || !cookieToken.equals(headerToken)) {
            httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
            httpResponse.getWriter().write("CSRF token invalid");
            return;
        }
        
        chain.doFilter(request, response);
    }
}

XSS 防护

输入过滤

java
@Component
public class XssFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        chain.doFilter(new XssRequestWrapper((HttpServletRequest) request), response);
    }
}

public class XssRequestWrapper extends HttpServletRequestWrapper {
    
    public XssRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return sanitize(value);
    }
    
    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) {
            return null;
        }
        return Arrays.stream(values)
            .map(this::sanitize)
            .toArray(String[]::new);
    }
    
    private String sanitize(String value) {
        if (value == null) {
            return null;
        }
        return value
            .replaceAll("<", "&lt;")
            .replaceAll(">", "&gt;")
            .replaceAll("\"", "&quot;")
            .replaceAll("'", "&#x27;")
            .replaceAll("/", "&#x2F;");
    }
}

审计日志

安全审计

java
@Component
public class SecurityAuditService {
    
    @Inject
    private AuditLogRepository auditLogRepository;
    
    public void logLogin(User user, String ip, boolean success) {
        AuditLog log = new AuditLog();
        log.setUserId(user.getId());
        log.setAction("LOGIN");
        log.setIpAddress(ip);
        log.setSuccess(success);
        log.setTimestamp(LocalDateTime.now());
        auditLogRepository.save(log);
    }
    
    public void logAccess(User user, String resource, String action) {
        AuditLog log = new AuditLog();
        log.setUserId(user.getId());
        log.setAction(action);
        log.setResource(resource);
        log.setTimestamp(LocalDateTime.now());
        auditLogRepository.save(log);
    }
    
    public void logPermissionDenied(User user, String permission) {
        AuditLog log = new AuditLog();
        log.setUserId(user.getId());
        log.setAction("PERMISSION_DENIED");
        log.setResource(permission);
        log.setSuccess(false);
        log.setTimestamp(LocalDateTime.now());
        auditLogRepository.save(log);
    }
}

最佳实践

1. 密码安全

java
// ✅ 推荐:使用强哈希算法
passwordEncoder.encode(password);  // BCrypt, Argon2

// ❌ 不推荐:使用弱哈希
MessageDigest.getInstance("MD5").digest(password.getBytes());

2. Token 安全

java
// ✅ 推荐:使用足够长的密钥
security.jwt.secret=your-256-bit-secret-key-here-must-be-long-enough

// ❌ 不推荐:使用短密钥
security.jwt.secret=secret

3. 敏感数据保护

java
// ✅ 推荐:加密敏感数据
user.setIdCard(encryptionService.encrypt(idCard));

// ❌ 不推荐:明文存储
user.setIdCard(idCard);

4. 权限最小化

java
// ✅ 推荐:精细权限控制
@RequiresPermission("order:delete")
public void deleteOrder(Long id) { }

// ❌ 不推荐:粗粒度权限
@RequiresRole("USER")
public void deleteOrder(Long id) { }

常见问题

Q: JWT 过期后如何处理?

A: 使用 Refresh Token 刷新 Access Token。

java
public AuthResult refresh(String refreshToken) {
    if (tokenService.validateRefreshToken(refreshToken)) {
        User user = tokenService.getUserFromRefreshToken(refreshToken);
        String newAccessToken = tokenService.generateAccessToken(user);
        return new AuthResult(newAccessToken, refreshToken);
    }
    throw new AuthenticationException("Refresh token invalid");
}

Q: 如何防止暴力破解?

A:

  1. 限制登录尝试次数
  2. 使用验证码
  3. 账户锁定机制
  4. IP 黑名单

Q: 如何安全存储密钥?

A:

  1. 使用环境变量
  2. 使用密钥管理服务
  3. 不要硬编码在代码中

下一步

Released under the Apache License 2.0