Spring Security JWT认证完整实现
墨韵云阁
0 阅读
0
开发笔记
一、自定义匿名用户访问返回处理:AnonymousAuthenticationEntryPoint
核心作用
当未登录的匿名用户访问需要认证的接口时,替代 Security 默认的「401 页面响应」,返回自定义的 JSON 格式提示(明确告知 “未登录”),适配前后端分离场景(前端需解析 JSON 做跳转或提示)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
@Slf4j @Component public class AnonymousAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { log.info("用户需要登录,访问[{}]失败,AuthenticationException={}", request.getRequestURI(), authException.getMessage(), authException); response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Content-type", "application/json;charset=UTF-8"); Result<String> object = Result.error(424, "未登录,请登录访问"); response.getWriter().print(JSONUtil.toJsonStr(object)); } }
|
二、自定义权限访问异常处理:CustomAccessDeniedHandler
核心作用
当已登录但权限不足的用户访问接口时(比如普通用户访问管理员接口),替代 Security 默认的「403 页面响应」,返回自定义 JSON 格式的 “无权限” 提示,适配前后端分离。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { Result<Object> error = Result.error(403, "无权限访问"); response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Content-type", "application/json;charset=UTF-8"); response.getWriter().print(JSONUtil.toJsonStr(error)); } }
|
三、自定义跳过认证注解类和 AOP 实现
3.1 免认证注解:NoAuth
核心作用
定义一个「标记注解」,用于标注不需要登录就能访问的接口 / 控制器(比如登录接口、注册接口、验证码接口),替代传统的 “在 Security 配置中硬编码白名单路径”,更灵活易维护。
1 2 3 4 5 6 7 8 9 10 11
|
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface NoAuth { }
|
3.2 免认证路径收集:RequestMappingCollector
核心作用
通过实现 Spring 的 BeanPostProcessor 接口,在项目启动时自动扫描所有加了 @NoAuth 注解的接口路径,收集成 “免认证白名单”,供 Security 配置使用(避免手动写死白名单)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
@Service public class RequestMappingCollector implements BeanPostProcessor {
@Getter @Setter private Set<String> permitAllUrls = new LinkedHashSet<>();
@Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof RequestMappingHandlerMapping handlerMapping) { Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods(); for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) { RequestMappingInfo info = entry.getKey(); HandlerMethod method = entry.getValue();
boolean hasNoAuth = AnnotationUtils.findAnnotation(method.getBeanType(), NoAuth.class) != null || AnnotationUtils.findAnnotation(method.getMethod(), NoAuth.class) != null;
if (hasNoAuth) { permitAllUrls.addAll(info.getPathPatternsCondition().getPatternValues()); } } } return bean; } }
|
四、自定义实现登录用户信息:SecurityUser
核心作用
继承 Security 提供的 User 类(实现 UserDetails 接口),扩展存储项目自定义的用户实体(UserEntity) —— 因为 Security 默认的 User 只包含用户名、密码、权限,无法满足业务需求(比如需要用户 ID、昵称等)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@Getter @Setter public class SecurityUser extends User {
private UserEntity userEntity;
public SecurityUser(String username, String password, Collection<? extends GrantedAuthority> authorities) { super(username, password, authorities); } }
|
五、自定义实现 UserDetailsService 接口:UserDetailsServiceImpl
核心作用
实现 Security 的 UserDetailsService 接口,是用户认证的核心数据源—— 当用户登录时,Security 会调用该类的 loadUserByUsername 方法,从数据库查询用户信息,供后续密码校验和权限赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
@Service public class UserDetailsServiceImpl implements UserDetailsService {
@Resource private IUserService userService;
@Override public SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException { UserEntity userEntity = userService.findByUsername(username); if (userEntity == null) { throw new UsernameNotFoundException("用户不存在:" + username); }
Collection<GrantedAuthority> authorities = new ArrayList<>(); if (StrUtil.isNotBlank(userEntity.getPermissionsCode())) { for (String code : userEntity.getPermissionsCode().split(",")) { authorities.add(new SimpleGrantedAuthority(code)); } }
SecurityUser securityUser = new SecurityUser(userEntity.getUsername(), userEntity.getPassword(), authorities); securityUser.setUserEntity(userEntity);
return securityUser; } }
|
六、JWT 工具类和 JWT 过滤器实现
6.1 JWT 工具类:JwtUtils
核心作用
封装 JWT 的核心操作:生成 Token(登录成功后返回给前端)、解析 Token(从请求头提取用户信息)、验证 Token(是否过期、签名是否合法),是前后端分离认证的 “核心工具”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
|
@Component public class JwtUtils {
@Value("${jwt.secret}") private String jwtSecret;
@Value("${jwt.expiration}") private int jwtExpirationMs;
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.parserBuilder() .setSigningKey(key()) .build() .parseClaimsJws(token) .getBody(); }
public Boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); }
public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return createToken(claims, userDetails.getUsername()); }
private SecretKey key() { byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); return Keys.hmacShaKeyFor(keyBytes); }
private String createToken(Map<String, Object> claims, String subject) { return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs)) .signWith(key(), SignatureAlgorithm.HS256) .compact(); }
public Boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); }
public String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (headerAuth != null && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7); } return null; } }
|
6.2 JWT 过滤器:JwtAuthenticationFilter
核心作用
继承 Security 的 OncePerRequestFilter(确保每次请求只执行一次),拦截所有请求,从请求头提取 JWT Token,验证合法性后将用户信息存入 SecurityContext(让 Security 后续能识别 “当前用户已登录”)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource private JwtUtils jwtUtils;
@Resource private UserDetailsService userDetailsService;
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = jwtUtils.parseJwt(request); if (Objects.nonNull(jwt)) { String username = jwtUtils.extractUsername(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtils.validateToken(jwt, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } catch (Exception e) { logger.error("Cannot set user authentication: {}", e); } filterChain.doFilter(request, response); } }
|
七、Spring Security 配置:SecurityConfig
核心作用
Security 的核心配置类:整合上述所有自定义组件(过滤器、异常处理器、白名单),配置认证授权规则(哪些路径免认证、哪些需权限)、跨域、会话管理等,是整个 Security 流程的 “总指挥”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
|
@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig {
@Resource private RequestMappingCollector requestMappingCollector; @Resource private AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint; @Resource private CustomAccessDeniedHandler customAccessDeniedHandler;
@Bean("pms") public PermissionService permissionService() { return new PermissionService(); }
@Bean public AnnotationTemplateExpressionDefaults prePostTemplateDefaults() { return new AnnotationTemplateExpressionDefaults(); }
@Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); }
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(List.of("*")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; }
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(se -> se.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers(requestMappingCollector.getPermitAllUrls().toArray(new String[0])) .permitAll() .anyRequest() .authenticated()) .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) .exceptionHandling(exception -> { exception.authenticationEntryPoint(anonymousAuthenticationEntryPoint); exception.accessDeniedHandler(customAccessDeniedHandler); }) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } }
|
八、登录实现:AuthController
核心作用
提供自定义登录接口(/login)和 Token 校验接口(/check),是用户触发认证流程的入口:接收前端传入的用户名密码,调用 Security 的 AuthenticationManager 校验,成功后生成 JWT 返回给前端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
|
@RestController public class AuthController {
@Resource private AuthenticationManager authenticationManager;
@Resource private JwtUtils jwtUtils;
@Resource private IUserService userService;
@Resource private HttpServletRequest request;
@NoAuth @PostMapping("/login") public Result<JwtResponse> login(@RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.username(), loginRequest.password())); SecurityContextHolder.getContext().setAuthentication(authentication); SecurityUser userDetails = (SecurityUser) authentication.getPrincipal(); String jwt = jwtUtils.generateToken(userDetails); userDetails.getUserEntity().setPassword("只有聪明的人才能看到密码"); return Result.success(new JwtResponse(userDetails.getUserEntity(), userDetails.getUserEntity().getRole(), jwt)); }
@GetMapping("/check") public Result<String> check() { try { String token = jwtUtils.parseJwt(request); boolean tokenExpired = jwtUtils.isTokenExpired(token); return tokenExpired ? Result.error(424, "token已过期") : Result.success(); } catch (Exception ignored) { } return Result.error(424, "token 无效"); }
public record LoginRequest(String username, String password) implements Serializable { }
public record JwtResponse(UserEntity user, RoleEntity role, String token) { } }
|
整体流程总结
- 未登录访问需认证接口:JWT 过滤器未提取到有效 Token → Security 触发
AnonymousAuthenticationEntryPoint → 返回 “未登录” JSON。
- 已登录但权限不足:JWT 验证通过,但用户权限不匹配 → Security 触发
CustomAccessDeniedHandler → 返回 “无权限” JSON。
- 登录流程:前端调用
/login(@NoAuth 免认证)→ 传入用户名密码 → AuthenticationManager 校验 → 成功生成 JWT → 返回给前端。
- 已登录访问接口:前端在 Authorization 头携带 JWT → JWT 过滤器解析验证 Token → 合法则设置认证信息 → Security 允许访问接口。
评论