1.引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>3.5.7</version></dependency><!-- JWT (兼容 JDK 21) --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.12.3</version></dependency>
2.yml配置
app:jwt:secret: "mySuperSecretKeyForJWTGenerationInSpringBoot3WithJDK21"expiration: 86400000 # 24小时refresh-expiration: 604800000 # 7天
3.代码
3.1 GrantedAuthority
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
@Data
@JsonSerialize
@NoArgsConstructor
@AllArgsConstructor
public class CustomGrantedAuthority implements GrantedAuthority {private String authority;@Overridepublic String getAuthority() {return this.authority;}
}
3.2 UserDetails实现
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@Data
public class MyUserDetails implements UserDetails {private String id;private String userName;private String password;private String role;private Collection<CustomGrantedAuthority> authorities;public MyUserDetails(User user) {this.id = user.getId();this.userName = user.getAccountName();this.password = user.getPassword();}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.userName;}
}
3.3 UserDetailsService实现
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;@Service
@RequiredArgsConstructor
public class MyUserService implements UserDetailsService {private final IUserService iUserService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User one = iUserService.lambdaQuery().eq(User::getAccountName, username).one();if (one == null) {throw new UsernameNotFoundException("用户不存在: " + username);}one.setPassword(new BCryptPasswordEncoder().encode(one.getPassword()));MyUserDetails myUserDetails = new MyUserDetails(one);List<CustomGrantedAuthority> authorities = new ArrayList<>();authorities.add(new CustomGrantedAuthority("TEST"));authorities.add(new CustomGrantedAuthority("menu:ecu:list"));myUserDetails.setAuthorities(authorities);return myUserDetails;}
}
3.4 JwtTokenUtil
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;@Component
public class JwtTokenUtil {@Value("${app.jwt.secret}")private String secret;@Value("${app.jwt.expiration}")private Long expiration;@Value("${app.jwt.refresh-expiration}")private Long refreshExpiration;private SecretKey getSigningKey() {return Keys.hmacShaKeyFor(secret.getBytes());}public String extractUsername(String token) {return extractClaim(token, Claims::getSubject);}public Date extractExpiration(String token) {return extractClaim(token, Claims::getExpiration);}public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {final Claims claims = extractAllClaims(token);return claimsResolver.apply(claims);}private Claims extractAllClaims(String token) {return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token).getPayload();}private Boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());}public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();if (userDetails instanceof MyUserDetails myUserDetails) {claims.put("userId", myUserDetails.getId());claims.put("role", myUserDetails.getRole());claims.put("authorities",userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));}return createToken(claims, userDetails.getUsername(), expiration);}public String generateRefreshToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();return createToken(claims, userDetails.getUsername(), refreshExpiration);}private String createToken(Map<String, Object> claims, String subject, Long expiration) {return Jwts.builder().claims(claims).subject(subject).issuedAt(new Date(System.currentTimeMillis())).expiration(new Date(System.currentTimeMillis() + expiration)).signWith(getSigningKey()).compact();}public Boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}public String extractUserId(String token) {final Claims claims = extractAllClaims(token);return claims.get("userId", String.class);}public String extractRole(String token) {final Claims claims = extractAllClaims(token);return claims.get("role", String.class);}public Authentication getAuthentication(String token) {Claims claims = extractAllClaims(token);Collection<? extends GrantedAuthority> authorities =Arrays.stream(claims.get("authorities").toString().split(",")).map(CustomGrantedAuthority::new).collect(Collectors.toList());UserDetails userDetails = new org.springframework.security.core.userdetails.User(claims.getSubject(), "", authorities);return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);}
}
3.5 OncePerRequestFilter
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {private final JwtTokenUtil jwtTokenUtil;private final MyUserService userService;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws ServletException, IOException {final String requestTokenHeader = request.getHeader("Authorization");String username = null;String jwtToken = null;if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {jwtToken = requestTokenHeader.substring(7);try {username = jwtTokenUtil.extractUsername(jwtToken);} catch (Exception e) {logger.warn("JWT Token 解析失败: " + e.getMessage());}}if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {MyUserDetails userDetails = (MyUserDetails) this.userService.loadUserByUsername(username);if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {UsernamePasswordAuthenticationToken authToken =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authToken);}}chain.doFilter(request, response);}
}
3.6 SecurityConfig
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {private final UserDetailsService userDetailsService;private final JwtAuthenticationFilter jwtAuthenticationFilter;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}public static void main(String[] args) {BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();String encode = bCryptPasswordEncoder.encode("123456");System.out.println(bCryptPasswordEncoder.matches("123456", encode));}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf(AbstractHttpConfigurer::disable).sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**","/v3/api-docs/**","/swagger-ui/**","/swagger-ui.html").permitAll().requestMatchers("/api/admin/**").hasRole("ADMIN").requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN").anyRequest().authenticated()).userDetailsService(userDetailsService) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}
}
4. 登录验证
4.1 DTO
@Data
public class LoginRequest {@NotBlank(message = "用户名不能为空")@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")private String username;@NotBlank(message = "密码不能为空")@Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间")private String password;
}
@Data
public class RegisterRequest {@NotBlank(message = "用户名不能为空")@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")private String username;@NotBlank(message = "邮箱不能为空")@Email(message = "邮箱格式不正确")private String email;@NotBlank(message = "密码不能为空")@Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间")private String password;private String confirmPassword;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JwtResponse {private String token;private String refreshToken;private String type = "Bearer";private String username;private String email;private String role;private Long userId;private List<String> authorities;public JwtResponse(String token, String refreshToken, String username, String role,List<String> authorities) {this.token = token;this.refreshToken = refreshToken;this.username = username;this.role = role;this.authorities = authorities;}public JwtResponse(String token, String refreshToken, String username, String email, String role, Long userId) {this.token = token;this.refreshToken = refreshToken;this.username = username;this.email = email;this.role = role;this.userId = userId;}
}
4.2 controller
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;import java.util.stream.Collectors;@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {private final AuthenticationManager authenticationManager;private final MyUserService userService;private final JwtTokenUtil jwtTokenUtil;@PostMapping("/login")public RespResult<?> login(@Valid @RequestBody LoginRequest request) {Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));SecurityContextHolder.getContext().setAuthentication(authentication);MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();String token = jwtTokenUtil.generateToken(userDetails);String refreshToken = jwtTokenUtil.generateRefreshToken(userDetails);return RespResult.success(new JwtResponse(token, refreshToken,userDetails.getUsername(),userDetails.getRole(),userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())));}
@PostMapping("/refresh")public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String refreshToken) {if (refreshToken.startsWith("Bearer ")) {refreshToken = refreshToken.substring(7);}String username = jwtTokenUtil.extractUsername(refreshToken);MyUserDetails userDetails = (MyUserDetails) userService.loadUserByUsername(username);if (jwtTokenUtil.validateToken(refreshToken, userDetails)) {String newToken = jwtTokenUtil.generateToken(userDetails);return ResponseEntity.ok(new JwtResponse(newToken, refreshToken, username, userDetails.getRole(),userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())));}return ResponseEntity.badRequest().body("Refresh Token 无效");}}
4.3 Postman
