安全认证
LCGYL Framework 提供全面的安全认证支持,包括身份认证、权限控制、加密解密等功能。
安全配置
基本配置
properties
# application.properties
security.enabled=true
security.session.timeout=3600
security.remember-me.enabled=true
security.remember-me.token-validity=604800yaml
# application.yaml
security:
enabled: true
session:
timeout: 3600
remember-me:
enabled: true
token-validity: 604800JWT 配置
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-appyaml
# 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-TOKENyaml
# application.yaml
security:
csrf:
enabled: true
cookie-name: XSRF-TOKEN
header-name: X-XSRF-TOKENCSRF 过滤器
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("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """)
.replaceAll("'", "'")
.replaceAll("/", "/");
}
}审计日志
安全审计
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=secret3. 敏感数据保护
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:
- 限制登录尝试次数
- 使用验证码
- 账户锁定机制
- IP 黑名单
Q: 如何安全存储密钥?
A:
- 使用环境变量
- 使用密钥管理服务
- 不要硬编码在代码中