Spring Security + JWT + Spring Boot 整合教程(增强版)
权限控制增强
Spring Security 提供了强大的基于角色和权限的访问控制机制。我们可以通过以下方式增强权限控制:
细化权限模型
创建权限枚举类:
package com.example.securityjwt.model;
public enum Permission {
USER_READ("user:read"),
USER_WRITE("user:write"),
ADMIN_READ("admin:read"),
ADMIN_WRITE("admin:write");
private final String permission;
Permission(String permission) {
this.permission = permission;
}
public String getPermission() {
return permission;
}
}
修改角色枚举类:
package com.example.securityjwt.model;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static com.example.securityjwt.model.Permission.*;
@RequiredArgsConstructor
public enum Role {
USER(Collections.emptySet()),
ADMIN(
Set.of(
ADMIN_READ,
ADMIN_WRITE,
USER_READ,
USER_WRITE
)
);
@Getter
private final Set<Permission> permissions;
public List<SimpleGrantedAuthority> getAuthorities() {
var authorities = getPermissions()
.stream()
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
return authorities;
}
}
修改用户实体类:
package com.example.securityjwt.entity;
import com.example.securityjwt.model.Role;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "_user")
public class User implements UserDetails {
@Id
@GeneratedValue
private Integer id;
private String firstname;
private String lastname;
private String username;
private String password;
@Enumerated(EnumType.STRING)
private Role role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return role.getAuthorities();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
增强安全配置
修改 SecurityConfig 以配置更精细的权限控制:
package com.example.securityjwt.config;
import com.example.securityjwt.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import static com.example.securityjwt.model.Permission.*;
import static com.example.securityjwt.model.Role.*;
import static org.springframework.http.HttpMethod.*;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
// 用户管理API权限控制
.antMatchers(GET, "/api/users/**").hasAnyAuthority(USER_READ.getPermission(), ADMIN_READ.getPermission())
.antMatchers(POST, "/api/users/**").hasAnyAuthority(USER_WRITE.getPermission(), ADMIN_WRITE.getPermission())
.antMatchers(PUT, "/api/users/**").hasAnyAuthority(USER_WRITE.getPermission(), ADMIN_WRITE.getPermission())
.antMatchers(DELETE, "/api/users/**").hasAuthority(ADMIN_WRITE.getPermission())
// 管理员专用API
.antMatchers("/api/admin/**").hasRole(ADMIN.name())
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
在控制器中使用注解进行权限控制
package com.example.securityjwt.controller;
import com.example.securityjwt.entity.User;
import com.example.securityjwt.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
@PreAuthorize("hasAuthority('user:read') or hasAuthority('admin:read')")
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping
@PreAuthorize("hasAuthority('user:write') or hasAuthority('admin:write')")
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('admin:write')")
public void deleteUser(@PathVariable Integer id) {
userService.deleteUser(id);
}
}
刷新令牌机制
为了提高安全性和用户体验,我们添加刷新令牌机制:
创建刷新令牌实体类
java
package com.example.securityjwt.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.Instant;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "refresh_token")
public class RefreshToken {
@Id
@GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
@Column(nullable = false, unique = true)
private String token;
@Column(nullable = false)
private Instant expiryDate;
}
创建刷新令牌服务
package com.example.securityjwt.service;
import com.example.securityjwt.entity.RefreshToken;
import com.example.securityjwt.entity.User;
import com.example.securityjwt.repository.RefreshTokenRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
@Service
public class RefreshTokenService {
@Value("${jwt.refresh.expiration}")
private Long refreshTokenDurationMs;
@Autowired
private RefreshTokenRepository refreshTokenRepository;
@Autowired
private UserService userService;
public Optional<RefreshToken> findByToken(String token) {
return refreshTokenRepository.findByToken(token);
}
public RefreshToken createRefreshToken(String username) {
User user = userService.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
RefreshToken refreshToken = RefreshToken.builder()
.user(user)
.token(UUID.randomUUID().toString())
.expiryDate(Instant.now().plusMillis(refreshTokenDurationMs))
.build();
return refreshTokenRepository.save(refreshToken);
}
public RefreshToken verifyExpiration(RefreshToken token) {
if (token.getExpiryDate().isBefore(Instant.now())) {
refreshTokenRepository.delete(token);
throw new RuntimeException("Refresh token was expired. Please make a new signin request");
}
return token;
}
@Transactional
public int deleteByUserId(Integer userId) {
return refreshTokenRepository.deleteByUser(userService.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found")));
}
}
修改认证服务以支持刷新令牌
package com.example.securityjwt.service;
import com.example.securityjwt.entity.*;
import com.example.securityjwt.repository.UserRepository;
import com.example.securityjwt.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final UserRepository repository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
private final AuthenticationManager authenticationManager;
private final RefreshTokenService refreshTokenService;
public AuthenticationResponse register(RegisterRequest request) {
var user = User.builder()
.firstname(request.getFirstname())
.lastname(request.getLastname())
.username(request.getUsername())
.password(passwordEncoder.encode(request.getPassword()))
.role(request.getRole())
.build();
repository.save(user);
var jwtToken = jwtUtil.generateToken(user);
var refreshToken = refreshTokenService.createRefreshToken(user.getUsername());
return AuthenticationResponse.builder()
.accessToken(jwtToken)
.refreshToken(refreshToken.getToken())
.build();
}
public AuthenticationResponse authenticate(AuthenticationRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
var user = repository.findByUsername(request.getUsername())
.orElseThrow();
var jwtToken = jwtUtil.generateToken(user);
var refreshToken = refreshTokenService.createRefreshToken(user.getUsername());
return AuthenticationResponse.builder()
.accessToken(jwtToken)
.refreshToken(refreshToken.getToken())
.build();
}
public AuthenticationResponse refreshToken(RefreshTokenRequest request) {
return refreshTokenService.findByToken(request.getRefreshToken())
.map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUser)
.map(user -> {
String accessToken = jwtUtil.generateToken(user);
return AuthenticationResponse.builder()
.accessToken(accessToken)
.refreshToken(request.getRefreshToken())
.build();
})
.orElseThrow(() -> new RuntimeException("Refresh token not found"));
}
}
添加刷新令牌控制器
package com.example.securityjwt.controller;
import com.example.securityjwt.entity.AuthenticationResponse;
import com.example.securityjwt.entity.RefreshTokenRequest;
import com.example.securityjwt.service.AuthenticationService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authenticationService;
// 已有方法保持不变...
@PostMapping("/refresh-token")
public ResponseEntity<AuthenticationResponse> refreshToken(
@RequestBody RefreshTokenRequest request
) {
return ResponseEntity.ok(authenticationService.refreshToken(request));
}
}
增强 JWT 工具类
添加从令牌中提取权限的方法:
package com.example.securityjwt.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
public class JwtUtil {
private static final String SECRET_KEY = "YOUR_SECRET_KEY_HERE_THIS_SHOULD_BE_LONG_AND_SECURE";
// 已有方法保持不变...
public List<GrantedAuthority> extractAuthorities(String token) {
final Claims claims = extractAllClaims(token);
List<Map<String, String>> authorityMaps = claims.get("authorities", List.class);
return authorityMaps.stream()
.map(authorityMap ->
new org.springframework.security.core.authority.SimpleGrantedAuthority(
authorityMap.get("authority")
)
)
.collect(Collectors.toList());
}
public String generateToken(
Map<String, Object> extraClaims,
UserDetails userDetails
) {
// 添加权限信息到claims
extraClaims.put("authorities", userDetails.getAuthorities().stream()
.map(authority -> Map.of("authority", authority.getAuthority()))
.collect(Collectors.toList()));
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 24小时
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
}
使用方法更新
-
刷新令牌请求:
POST /api/auth/refresh-token { "refreshToken": "your_refresh_token_here" } -
基于权限的访问控制:
- 普通用户可以访问
/api/users(需要user:read权限) - 管理员可以访问所有
/api/users接口(需要admin:write等权限) - 只有管理员可以访问
/api/admin下的接口
- 普通用户可以访问
总结
通过这些增强,我们的安全架构现在具有:
-
更精细的权限控制:
- 基于角色和权限的双重访问控制
- 使用注解和配置相结合的方式定义访问规则
- 清晰的权限模型设计
-
刷新令牌机制:
- 分离访问令牌和刷新令牌的职责
- 延长用户会话时间,提高用户体验
- 增强安全性,缩短访问令牌的有效期
-
优化的 JWT 实现:
-
在令牌中包含详细的权限信息
-
更安全的令牌生成和验证机制
-
这些改进使系统更加安全、灵活,同时保持良好的用户体验。根据实际需求,你还可以进一步扩展这些功能,如添加多因素认证、IP 绑定等安全措施。