- 数据库:在adm_charge_item表添加SourceBillNo字段 - 后端实体类:更新ChargeItem.java添加SourceBillNo字段 - 前端组件:创建手术计费界面(基于门诊划价界面) - 后端API:扩展PrePrePaymentDto支持手术计费标识 - 后端Service:扩展getChargeItems方法支持手术计费过滤 - 门诊手术安排界面:添加【计费】按钮 注意事项: - 需要手动执行SQL脚本:openhis-server-new/sql/add_source_bill_no_to_adm_charge_item.sql - 术后一站式结算功能待后续开发
460 lines
15 KiB
Markdown
460 lines
15 KiB
Markdown
# Security - Spring Security 6
|
|
|
|
## Security Configuration
|
|
|
|
```java
|
|
@Configuration
|
|
@EnableWebSecurity
|
|
@EnableMethodSecurity
|
|
public class SecurityConfig {
|
|
|
|
@Bean
|
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
|
http
|
|
.csrf(csrf -> csrf
|
|
.ignoringRequestMatchers("/api/auth/**")
|
|
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
|
)
|
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
|
.authorizeHttpRequests(auth -> auth
|
|
.requestMatchers("/api/auth/**", "/actuator/health").permitAll()
|
|
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
|
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
|
|
.anyRequest().authenticated()
|
|
)
|
|
.sessionManagement(session -> session
|
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
)
|
|
.exceptionHandling(ex -> ex
|
|
.authenticationEntryPoint(authenticationEntryPoint())
|
|
.accessDeniedHandler(accessDeniedHandler())
|
|
)
|
|
.addFilterBefore(jwtAuthenticationFilter(),
|
|
UsernamePasswordAuthenticationFilter.class);
|
|
|
|
return http.build();
|
|
}
|
|
|
|
@Bean
|
|
public CorsConfigurationSource corsConfigurationSource() {
|
|
CorsConfiguration configuration = new CorsConfiguration();
|
|
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
|
|
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
|
configuration.setAllowedHeaders(List.of("*"));
|
|
configuration.setAllowCredentials(true);
|
|
configuration.setMaxAge(3600L);
|
|
|
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
source.registerCorsConfiguration("/**", configuration);
|
|
return source;
|
|
}
|
|
|
|
@Bean
|
|
public AuthenticationManager authenticationManager(
|
|
AuthenticationConfiguration config) throws Exception {
|
|
return config.getAuthenticationManager();
|
|
}
|
|
|
|
@Bean
|
|
public PasswordEncoder passwordEncoder() {
|
|
return new BCryptPasswordEncoder(12);
|
|
}
|
|
}
|
|
```
|
|
|
|
## JWT Authentication Filter
|
|
|
|
```java
|
|
@Component
|
|
@RequiredArgsConstructor
|
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
private final JwtService jwtService;
|
|
private final UserDetailsService userDetailsService;
|
|
|
|
@Override
|
|
protected void doFilterInternal(
|
|
@NonNull HttpServletRequest request,
|
|
@NonNull HttpServletRequest response,
|
|
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
|
|
|
final String authHeader = request.getHeader("Authorization");
|
|
final String jwt;
|
|
final String username;
|
|
|
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
|
filterChain.doFilter(request, response);
|
|
return;
|
|
}
|
|
|
|
jwt = authHeader.substring(7);
|
|
|
|
try {
|
|
username = jwtService.extractUsername(jwt);
|
|
|
|
if (username != null && SecurityContextHolder.getContext()
|
|
.getAuthentication() == null) {
|
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
|
|
|
if (jwtService.isTokenValid(jwt, userDetails)) {
|
|
UsernamePasswordAuthenticationToken authToken =
|
|
new UsernamePasswordAuthenticationToken(
|
|
userDetails,
|
|
null,
|
|
userDetails.getAuthorities()
|
|
);
|
|
|
|
authToken.setDetails(
|
|
new WebAuthenticationDetailsSource().buildDetails(request)
|
|
);
|
|
|
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
|
}
|
|
}
|
|
} catch (JwtException e) {
|
|
log.error("JWT validation failed", e);
|
|
}
|
|
|
|
filterChain.doFilter(request, response);
|
|
}
|
|
}
|
|
```
|
|
|
|
## JWT Service
|
|
|
|
```java
|
|
@Service
|
|
public class JwtService {
|
|
@Value("${jwt.secret}")
|
|
private String secretKey;
|
|
|
|
@Value("${jwt.expiration}")
|
|
private long jwtExpiration;
|
|
|
|
@Value("${jwt.refresh-expiration}")
|
|
private long refreshExpiration;
|
|
|
|
public String extractUsername(String token) {
|
|
return extractClaim(token, Claims::getSubject);
|
|
}
|
|
|
|
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
|
final Claims claims = extractAllClaims(token);
|
|
return claimsResolver.apply(claims);
|
|
}
|
|
|
|
public String generateToken(UserDetails userDetails) {
|
|
Map<String, Object> extraClaims = new HashMap<>();
|
|
extraClaims.put("roles", userDetails.getAuthorities().stream()
|
|
.map(GrantedAuthority::getAuthority)
|
|
.collect(Collectors.toList()));
|
|
|
|
return generateToken(extraClaims, userDetails);
|
|
}
|
|
|
|
public String generateToken(
|
|
Map<String, Object> extraClaims,
|
|
UserDetails userDetails) {
|
|
return buildToken(extraClaims, userDetails, jwtExpiration);
|
|
}
|
|
|
|
public String generateRefreshToken(UserDetails userDetails) {
|
|
return buildToken(new HashMap<>(), userDetails, refreshExpiration);
|
|
}
|
|
|
|
private String buildToken(
|
|
Map<String, Object> extraClaims,
|
|
UserDetails userDetails,
|
|
long expiration) {
|
|
return Jwts
|
|
.builder()
|
|
.setClaims(extraClaims)
|
|
.setSubject(userDetails.getUsername())
|
|
.setIssuedAt(new Date(System.currentTimeMillis()))
|
|
.setExpiration(new Date(System.currentTimeMillis() + expiration))
|
|
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
|
|
.compact();
|
|
}
|
|
|
|
public boolean isTokenValid(String token, UserDetails userDetails) {
|
|
final String username = extractUsername(token);
|
|
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
|
|
}
|
|
|
|
private boolean isTokenExpired(String token) {
|
|
return extractExpiration(token).before(new Date());
|
|
}
|
|
|
|
private Date extractExpiration(String token) {
|
|
return extractClaim(token, Claims::getExpiration);
|
|
}
|
|
|
|
private Claims extractAllClaims(String token) {
|
|
return Jwts
|
|
.parserBuilder()
|
|
.setSigningKey(getSignInKey())
|
|
.build()
|
|
.parseClaimsJws(token)
|
|
.getBody();
|
|
}
|
|
|
|
private Key getSignInKey() {
|
|
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
|
|
return Keys.hmacShaKeyFor(keyBytes);
|
|
}
|
|
}
|
|
```
|
|
|
|
## UserDetailsService Implementation
|
|
|
|
```java
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class CustomUserDetailsService implements UserDetailsService {
|
|
private final UserRepository userRepository;
|
|
|
|
@Override
|
|
@Transactional(readOnly = true)
|
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
|
User user = userRepository.findByEmailWithRoles(username)
|
|
.orElseThrow(() -> new UsernameNotFoundException(
|
|
"User not found with email: " + username));
|
|
|
|
return org.springframework.security.core.userdetails.User
|
|
.builder()
|
|
.username(user.getEmail())
|
|
.password(user.getPassword())
|
|
.authorities(user.getRoles().stream()
|
|
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
|
|
.collect(Collectors.toList()))
|
|
.accountExpired(false)
|
|
.accountLocked(!user.getActive())
|
|
.credentialsExpired(false)
|
|
.disabled(!user.getActive())
|
|
.build();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Authentication Controller
|
|
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/api/auth")
|
|
@RequiredArgsConstructor
|
|
public class AuthenticationController {
|
|
private final AuthenticationService authenticationService;
|
|
|
|
@PostMapping("/register")
|
|
public ResponseEntity<AuthenticationResponse> register(
|
|
@Valid @RequestBody RegisterRequest request) {
|
|
AuthenticationResponse response = authenticationService.register(request);
|
|
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
|
}
|
|
|
|
@PostMapping("/login")
|
|
public ResponseEntity<AuthenticationResponse> login(
|
|
@Valid @RequestBody LoginRequest request) {
|
|
AuthenticationResponse response = authenticationService.login(request);
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
|
|
@PostMapping("/refresh")
|
|
public ResponseEntity<AuthenticationResponse> refreshToken(
|
|
@RequestBody RefreshTokenRequest request) {
|
|
AuthenticationResponse response = authenticationService.refreshToken(request);
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
|
|
@PostMapping("/logout")
|
|
@PreAuthorize("isAuthenticated()")
|
|
public ResponseEntity<Void> logout() {
|
|
SecurityContextHolder.clearContext();
|
|
return ResponseEntity.noContent().build();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Authentication Service
|
|
|
|
```java
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
@Transactional
|
|
public class AuthenticationService {
|
|
private final UserRepository userRepository;
|
|
private final PasswordEncoder passwordEncoder;
|
|
private final JwtService jwtService;
|
|
private final AuthenticationManager authenticationManager;
|
|
|
|
public AuthenticationResponse register(RegisterRequest request) {
|
|
if (userRepository.existsByEmail(request.email())) {
|
|
throw new DuplicateResourceException("Email already registered");
|
|
}
|
|
|
|
User user = User.builder()
|
|
.email(request.email())
|
|
.password(passwordEncoder.encode(request.password()))
|
|
.username(request.username())
|
|
.active(true)
|
|
.roles(Set.of(Role.builder().name("USER").build()))
|
|
.build();
|
|
|
|
user = userRepository.save(user);
|
|
|
|
String accessToken = jwtService.generateToken(convertToUserDetails(user));
|
|
String refreshToken = jwtService.generateRefreshToken(convertToUserDetails(user));
|
|
|
|
return new AuthenticationResponse(accessToken, refreshToken);
|
|
}
|
|
|
|
public AuthenticationResponse login(LoginRequest request) {
|
|
authenticationManager.authenticate(
|
|
new UsernamePasswordAuthenticationToken(
|
|
request.email(),
|
|
request.password()
|
|
)
|
|
);
|
|
|
|
User user = userRepository.findByEmail(request.email())
|
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
|
|
|
String accessToken = jwtService.generateToken(convertToUserDetails(user));
|
|
String refreshToken = jwtService.generateRefreshToken(convertToUserDetails(user));
|
|
|
|
return new AuthenticationResponse(accessToken, refreshToken);
|
|
}
|
|
|
|
public AuthenticationResponse refreshToken(RefreshTokenRequest request) {
|
|
String username = jwtService.extractUsername(request.refreshToken());
|
|
|
|
User user = userRepository.findByEmail(username)
|
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
|
|
|
UserDetails userDetails = convertToUserDetails(user);
|
|
|
|
if (!jwtService.isTokenValid(request.refreshToken(), userDetails)) {
|
|
throw new InvalidTokenException("Invalid refresh token");
|
|
}
|
|
|
|
String accessToken = jwtService.generateToken(userDetails);
|
|
|
|
return new AuthenticationResponse(accessToken, request.refreshToken());
|
|
}
|
|
|
|
private UserDetails convertToUserDetails(User user) {
|
|
return org.springframework.security.core.userdetails.User
|
|
.builder()
|
|
.username(user.getEmail())
|
|
.password(user.getPassword())
|
|
.authorities(user.getRoles().stream()
|
|
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
|
|
.collect(Collectors.toList()))
|
|
.build();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Method Security
|
|
|
|
```java
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class UserService {
|
|
private final UserRepository userRepository;
|
|
|
|
@PreAuthorize("hasRole('ADMIN')")
|
|
public List<User> getAllUsers() {
|
|
return userRepository.findAll();
|
|
}
|
|
|
|
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
|
|
public User getUserById(Long userId) {
|
|
return userRepository.findById(userId)
|
|
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
|
|
}
|
|
|
|
@PreAuthorize("isAuthenticated()")
|
|
@PostAuthorize("returnObject.email == authentication.principal.username")
|
|
public User updateProfile(Long userId, UserUpdateRequest request) {
|
|
User user = getUserById(userId);
|
|
// Update logic
|
|
return userRepository.save(user);
|
|
}
|
|
|
|
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
|
|
public void deleteUser(Long userId) {
|
|
userRepository.deleteById(userId);
|
|
}
|
|
}
|
|
```
|
|
|
|
## OAuth2 Resource Server (JWT)
|
|
|
|
```java
|
|
@Configuration
|
|
@EnableWebSecurity
|
|
public class OAuth2ResourceServerConfig {
|
|
|
|
@Bean
|
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
http
|
|
.authorizeHttpRequests(auth -> auth
|
|
.requestMatchers("/public/**").permitAll()
|
|
.anyRequest().authenticated()
|
|
)
|
|
.oauth2ResourceServer(oauth2 -> oauth2
|
|
.jwt(jwt -> jwt
|
|
.jwtAuthenticationConverter(jwtAuthenticationConverter())
|
|
)
|
|
);
|
|
|
|
return http.build();
|
|
}
|
|
|
|
@Bean
|
|
public JwtDecoder jwtDecoder() {
|
|
return JwtDecoders.fromIssuerLocation("https://auth.example.com");
|
|
}
|
|
|
|
@Bean
|
|
public JwtAuthenticationConverter jwtAuthenticationConverter() {
|
|
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
|
|
new JwtGrantedAuthoritiesConverter();
|
|
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
|
|
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
|
|
|
|
JwtAuthenticationConverter jwtAuthenticationConverter =
|
|
new JwtAuthenticationConverter();
|
|
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
|
|
grantedAuthoritiesConverter);
|
|
|
|
return jwtAuthenticationConverter;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Quick Reference
|
|
|
|
| Annotation | Purpose |
|
|
|------------|---------|
|
|
| `@EnableWebSecurity` | Enables Spring Security |
|
|
| `@EnableMethodSecurity` | Enables method-level security annotations |
|
|
| `@PreAuthorize` | Checks authorization before method execution |
|
|
| `@PostAuthorize` | Checks authorization after method execution |
|
|
| `@Secured` | Role-based method security |
|
|
| `@WithMockUser` | Mock authenticated user in tests |
|
|
| `@AuthenticationPrincipal` | Inject current user in controller |
|
|
|
|
## Security Best Practices
|
|
|
|
- Always use HTTPS in production
|
|
- Store JWT secret in environment variables
|
|
- Use strong password encoding (BCrypt with strength 12+)
|
|
- Implement token refresh mechanism
|
|
- Add rate limiting to authentication endpoints
|
|
- Validate all user inputs
|
|
- Log security events
|
|
- Keep dependencies updated
|
|
- Use CSRF protection for state-changing operations
|
|
- Implement proper session timeout
|