feat: 门诊手术中计费功能
- 数据库:在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 - 术后一站式结算功能待后续开发
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user