SpringBoot 3.3 + Spring Security 6.3 实现基于JWT的验证

看了一圈,发现不少代码已经有些过时且不完整,所以索性再写一版供大家参考了。

1. 技术栈 及开发环境

  • Spring Boot 3.3.9
  • Spring Security 6.3.7
  • JJWT 0.12.5
  • Lombok 1.18.30
  • JDK 17 或更高版本
  • Maven 或 Gradle

2. 配置 JWT 工具类

生成及解析 JWT (基于0.12.5 版本的 jjwt API)

代码示例

配置文件

# application.properties

jwt.secret-key=my-ultra-secure-secret-key-1234567890
jwt.expiration-time=86400000

​​​​​​​JwtUtil 类的实现:

package com.example.demo.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Setter
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtUtil {

    // @Setter in class needed if no @Value here
    private String secretKey;
    private long expirationTime;

    public String generateToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        // 1. 将 SECRET KEY 转换为 Key 对象
        SecretKey signingKey = Keys.hmacShaKeyFor(secretKey.getBytes());

        return Jwts.builder()
                .claims(claims)
                .subject(username)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expirationTime))
                .signWith(signingKey)
                .compact();
    }

    public String extractUsername(String token) {
        return extractClaims(token).getSubject();
    }

    public Date extractExpiration(String token) {
        return extractClaims(token).getExpiration();
    }

    private Claims extractClaims(String token) {
        // 1. 将 SECRET KEY 转换为 Key 对象
        SecretKey signingKey = Keys.hmacShaKeyFor(secretKey.getBytes());

        // 2. 使用新 API 解析 Token
        return Jwts.parser().verifyWith(signingKey).build().parseSignedClaims(token).getPayload();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public Boolean validateToken(String token, String username) {
        final String extractedUsername = extractUsername(token);
        return (extractedUsername.equals(username) && !isTokenExpired(token));
    }
}

3. 创建用户实体和UserDetails实现

用户实体类

@Data
public class User {

    // 此两个字段必须,其他视情况添加
    private String username; 
    private String password; 
}

UserDetails的实现类,用于验证

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.demo.entity.User;

import java.util.Collection;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }
}

4. 创建UserDetails的服务

实现 UserDetailsService 接口用于加载用户信息

package com.example.demo.service;

import com.example.demo.domain.LoginUser;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库加载用户信息的逻辑
        User user = userMapper.findByUsername(username);

        if (Objects.isNull(user))
            throw new UsernameNotFoundException("用户不存在");
        // 实际将由 DaoAuthenticationProvider 验证此返回的UserDetails的账号密码
        LoginUser loginUser = new LoginUser(user);
        return loginUser;
    }
}

数据访问可用MyBatis或JPA等实现 User findByUsername(String username);

5. 创建 JWT 请求过滤器

创建一个自定义过滤器用于解析 JWT 并将其设置到 Spring Security 上下文中

package com.example.demo.security;

import com.example.demo.service.CustomUserDetailsService;
import com.example.demo.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }

        chain.doFilter(request, response);
    }
}

6. 配置 Spring Security

创建 SecurityConfig 类以配置 Spring Security,使其支持 JWT 认证

package com.example.demo.config;

import com.example.demo.exception.DelegateAccessDeniedHandler;
import com.example.demo.exception.DelegateAuthenticationEntryPoint;
import com.example.demo.security.JwtRequestFilter;
import org.springframework.beans.factory.annotation.Autowired;
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.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.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
public class SecurityConfig {
    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    public SecurityConfig(JwtRequestFilter jwtRequestFilter) {
        this.jwtRequestFilter = jwtRequestFilter;
    }

    // 将 Exception Handler 委托给 HandlerExceptionResolver 以便仍然使用全局异常处理, 当然亦可选择使用自定义
    @Bean
    public DelegateAccessDeniedHandler delegateAccessDeniedHandler() {
        return new DelegateAccessDeniedHandler();
    }

    @Bean
    public DelegateAuthenticationEntryPoint delegateAuthenticationEntryPoint() {
        return new DelegateAuthenticationEntryPoint();
    }

    @Bean
    // After 5.7的写法
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/token").permitAll()
                        .requestMatchers("/api/test").permitAll()
                        .anyRequest().authenticated()
                )
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        // 自定义异常处理,也可使用其委托回全局异常处理
        http.exceptionHandling(configurer -> {
            configurer
                    .accessDeniedHandler(delegateAccessDeniedHandler())
                    .authenticationEntryPoint(delegateAuthenticationEntryPoint());
        });

        return http.build();
    }

    // 鉴权
    @Bean
    public AuthenticationManager authenticationManager(
             AuthenticationConfiguration config
    ) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();     //安全且广泛使用的加密算法
    }
}

7. 异常处理 (6中的实现类)

package com.example.demo.exception;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.io.IOException;

// 将 Exception Handler 委托给 HandlerExceptionResolver
public class DelegateAccessDeniedHandler implements AccessDeniedHandler {
    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
            throws IOException, ServletException {
        resolver.resolveException(request, response, null, accessDeniedException);
    }
}




package com.example.demo.exception;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.web.servlet.HandlerExceptionResolver;

// 将 Exception Handler 委托给 HandlerExceptionResolver
public class DelegateAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
        resolver.resolveException(request, response, null, authException);
    }
}

此统一的全局异常处理可能较为简洁

package com.example.demo.exception;

import com.example.demo.pojo.Result;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.StringUtils;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;

import java.util.List;
import java.util.stream.Collectors;


/*
    Spring Security 核心异常(如 AuthenticationException 和 AccessDeniedException)属于运行时异常。
    由于这些异常是由 DispatcherServlet 后面的 Authentication Filter 在调用 Controller 方法之前抛出的,
    因此默认情况下 @RestControllerAdvice 无法捕获这些异常。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(AccessDeniedException.class)
    public Result<String> handleAccessDeniedException(AccessDeniedException e, HttpServletResponse response) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        String errorMsg = e.getMessage();
        return Result.failure(
                StringUtils.hasLength(errorMsg) ? errorMsg : "Access Denied!"
        );
    }

    @ExceptionHandler(AuthenticationException.class)
    public Result<String> handleAuthenticationException(AuthenticationException e, HttpServletResponse response) {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        String errorMsg = e.getMessage();
        return Result.failure(
                StringUtils.hasLength(errorMsg) ? errorMsg : "Authentication Failed!"
        );
    }


    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    public Result<String> handleBadRequestExceptions(Exception e, HttpServletResponse response) {
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        String errorMsg = e.getMessage();
        return Result.failure(
                StringUtils.hasLength(errorMsg) ? errorMsg : "Unsupported HTTP Method!"
        );
    }


    // 其他所有异常将会走到这里, 在此之前定义其他需要的异常处理
    @ExceptionHandler(Exception.class)
    public Result<String> handleAllExceptions(Exception e, HttpServletResponse response) {
        e.printStackTrace();
        String message = e.getMessage();
        response.setStatus(500);
        return Result.failure(
                StringUtils.hasLength(message) ? message : "Fatal error!"
        );
    }
}

8. 创建认证接口

创建一个控制器用于处理用户登录并生成 JWT

首先创建请求及返回值的POJO

package com.example.demo.pojo;

import lombok.Data;

@Data
public class AuthRequest {

    private String grantType;

    private String username;

    private String password;

}


package com.example.demo.pojo;

import lombok.Data;

@Data
public class AuthToken {

    private String token;

}


package com.example.demo.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

// 统一响应结果
@NoArgsConstructor
@AllArgsConstructor
@Data  //用于转换为JSON对象
public class Result<T> {

    private Integer code;
    private String message;
    private T data;

    // public static final Map<Integer, String> SUCCESS = new HashMap<>(1);
    public static final Integer SUCCESS = 0;
    public static final Integer FAILURE = 1;

    public static <E> Result<E> success(E data) {
        return new Result<>(SUCCESS, "操作成功", data);
    }

    public static <E> Result<E> failure(String message) {
        return new Result<>(FAILURE, message, null);
    }
}
package com.example.demo.controller;

import com.example.demo.pojo.AuthRequest;
import com.example.demo.pojo.AuthToken;
import com.example.demo.pojo.Result;
import com.example.demo.service.CustomUserDetailsService;
import com.example.demo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
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;

import java.util.Objects;

@Slf4j
@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/token")
    public Result<AuthToken> token(@RequestBody AuthRequest authRequest) {
        // AuthenticationManager 将调用 UserDetailsService 的"实现类"来加载用户信息和验证密码。
        Authentication authenticate = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                    authRequest.getUsername(),
                    authRequest.getPassword()
            )
        );

        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("验证失败");
        }

        // 返回Token
        AuthToken authToken = new AuthToken();
        authToken.setToken(jwtUtil.generateToken(authRequest.getUsername()));
        return Result.success(authToken);
    }
}

9. 测试代码

使用一个测试类添加测试用户数据

@SpringBootTest
public class DummyDataTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testInsertUser() {
        // 创建一个 User 对象
        User user = new User();
        user.setUsername("testuser");
        user.setPassword("123");

        // 插入用户
        userMapper.insert(user);
        // 验证插入是否成功
        assertNotNull(user.getId(), "插入后用户 ID 不应为空");
    }
}

测试接口

package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping(value = "/test")
    public String test() {
        return "this is a test";
    }

    @GetMapping(value = "/testauth")
    public String testAuth() {
        return "this is a test for auth";
    }
}

10. 测试

使用Postman之类的发送HTTP请求,获取Token

POST http://127.0.0.1:8080/api/auth/token
Content-Type: application/json
Payload: 
{
  "username":"testuser",
  "password":"123"
}

不验证Token的API

GET http://127.0.0.1:8080/api/test

需验证Header中的Bearer Token的API

GET http://127.0.0.1:8080/api/testauth
Authorization: Bearer <Generated Token>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值