使用 JWT 身份驗證保護你的 Spring Boot 應用

banq發表於2024-06-14

本文深入探討如何使用 JSON Web Tokens (JWT) 進行身份驗證來保護 Spring Boot 應用程式。我們將探索 Spring Security、JWT 基礎知識,然後實現具有使用者註冊、登入和訪問控制的安全 API。我們的資料將使用 Spring Data JPA 儲存在 PostgreSQL 資料庫中。

為什麼選擇 Spring Security 
Spring Security 是用於保護 Spring 應用程式的行業標準框架。它提供全面的身份驗證、授權和訪問控制功能。透過利用 Spring Security,我們可以有效地管理使用者對我們的 API 端點的訪問。

JWT 身份驗證
JWT 是一種基於令牌的身份驗證機制。與傳統的基於會話的方法不同,JWT 將使用者資訊儲存在緊湊、自包含的令牌中。此令牌隨每次請求一起傳送,允許伺服器驗證使用者的身份,而無需依賴伺服器端會話。

以下是 JWT 優勢的細分:

  • 無狀態:無需伺服器上的會話管理。
  • 安全:採用數字簽名防止篡改。
  • 靈活:可以配置各種宣告來儲存使用者資訊。

持久
在本文中,我們將使用 PostgreSQL 作為資料庫。您可以在任何資料庫管理系統中維護資料庫。為了獲得方便的部署選項,請考慮使用基於雲的解決方案,例如 Rapidapp,它提供託管的 PostgreSQL 資料庫,簡化設定和維護。


確保您已經使用您最喜歡的依賴項管理工具(例如 maven、gradle)安裝了以下依賴項。

pom.xml

 <dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.3</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.32</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.12.5</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

啟用 Spring Web 
為了啟用 Spring Web Security,您需要在SecurityConfig.java檔案中進行配置,如下所示。


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private static final String[] AUTH_WHITELIST = {
        <font>"/api/v1/auth/login",
       
"/api/v1/auth/register"
    };

    private final JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                        .requestMatchers(AUTH_WHITELIST).permitAll()
                    .anyRequest().authenticated()
            )
            .sessionManagement(sessionManagement ->
                sessionManagement
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

  • 第 2 行:新增@EnableWebSecurity到SecurityConfig類以保護 API 端點。
  • 第 6 行:允許來自/api/v1/auth/login和/api/v1/auth/register端點的請求,無需進行身份驗證。
  • 第 16 行:禁用 CSRF 保護,因為 JWT 身份驗證是無狀態的。
  • 第 24 行:設定會話建立策略,以STATELESS確保不維護會話。
  • 第 25 行:將 新增JwtAuthFilter到 之前的安全過濾器鏈中UsernamePasswordAuthenticationFilter。我們JwtAuthFilter很快會解釋類。

JWT 身份
為了啟用 JWT 身份驗證,您需要在JwtAuthFilter.java檔案中進行配置,如下所示。

JwtAuthFilter.java

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request.getServletPath().contains(<font>"/api/v1/auth")) {
            filterChain.doFilter(request, response);
            return;
        }

        final String authorizationHeader = request.getHeader(
"Authorization");
        final String jwtToken;
        final String email;

        if (authorizationHeader == null || !authorizationHeader.startsWith(
"Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        jwtToken = authorizationHeader.substring(7);
        email = jwtService.extractEmail(jwtToken);

        if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(email);
            if (jwtService.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

  • 第 10 行:不要對/api/v1/auth端點應用 JWT 身份驗證過濾器。
  • 第 24 行:從 header 中提取 JWT token Authorization。它的格式是Bearer <token>,所以是substring(7)。
  • 第 25 行:從 JWT 令牌中提取電子郵件JwtService,我們將在下一節中對其進行介紹。
  • 第 28-32 行:使用 驗證 JWT 令牌JwtService,使用UserDetailsfrom載入使用者詳細資訊UserDetailsService並將身份驗證儲存在 中SecurityContextHolder。

實現
此類包含所有 JWT 相關功能,如下所示。

JwtService.java

@Service
public class JwtService {

    @Value(<font>"${jwt.secret}")
    private String secret;

    public String extractEmail(String jwtToken) {
        return extractClaim(jwtToken, Claims::getSubject);
    }

    public <T> T extractClaim(String jwtToken, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(jwtToken);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String jwtToken) {
        return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(jwtToken).getPayload();
    }

    private SecretKey getSigningKey() {
        byte [] bytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(bytes);
    }

    public boolean validateToken(String jwtToken, UserDetails userDetails) {
        final String email = extractEmail(jwtToken);
        return email.equals(userDetails.getUsername()) && !isTokenExpired(jwtToken);
    }

    private boolean isTokenExpired(String jwtToken) {
        return extractExpiration(jwtToken).before(new Date());
    }

    private Date extractExpiration(String jwtToken) {
        return extractClaim(jwtToken, Claims::getExpiration);
    }

    public String generateToken(User u) {
        return createToken(u.getEmail());
    }

    private String createToken(String email) {
        return Jwts.builder()
                .subject(email)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
                .signWith(getSigningKey())
                .compact();
    }
}

  • 第 5 行:這是用於簽署 JWT 令牌的金鑰。這應該小心保護,我們不能分享或公開它。所有其他功能都是不言自明的。

UserDetailsS​​ervice 旨在展示 Spring Boot 安全身份驗證如何從資料庫載入使用者詳細資訊,如下所示。


@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {
    private final UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
                .map(user -> User.builder().username(user.getEmail())
                        .password(user.getPassword())
                        .build())
                .orElseThrow(() -> new UsernameNotFoundException(<font>"User not found"));
    }
}


到目前為止,我們只關注 JWT 身份驗證。但是,下一節中我們將如何生成 JWT 令牌?它的用例是什麼?

註冊
在生成 JWT 令牌來驗證使用者身份之前,我們需要註冊使用者。我們將使用它AuthController來註冊使用者。

AuthController.java

@RestController
@RequestMapping(path = <font>"api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;


    @PostMapping(path =
"/register")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void register(@RequestBody RegisterRequest registerRequest) {
        authService.register(registerRequest);
    }

    @PostMapping(path =
"/login")
    public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
        return ResponseEntity.ok(authService.login(loginRequest));
    }
}


在上面的控制器中,我們用來AuthService註冊和登入使用者。AuthService用於UserRepository與資料庫互動以進行與使用者相關的操作。

AuthService.java

@Service
@RequiredArgsConstructor
public class AuthService {

    private final UserRepository userRepository;
    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public void register(RegisterRequest registerRequest) {
        User u = User.builder()
                .email(registerRequest.getEmail())
                .password(bCryptPasswordEncoder.encode(registerRequest.getPassword()))
                .firstName(registerRequest.getFirstName())
                .lastName(registerRequest.getLastName())
                .build();
        userRepository.save(u);
    }

    public String login(LoginRequest loginRequest) {
        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
        User u = userRepository.findByEmail(loginRequest.getEmail()).orElseThrow(() -> new EntityNotFoundException(<font>"User not found"));
        return jwtService.generateToken(u);

    }
}

  • 第 10 行:使用請求負載中提供的詳細資訊註冊使用者。用於bCryptPasswordEncoder在將密碼儲存到資料庫之前對其進行雜湊處理。
  • 第 21 行:authenticationManager由於它知道如何驗證使用者名稱和密碼,因此登入操作已完成。

授限訪問 UserController
您可以看到使用者物件的示例端點實現。


@RestController
@RequestMapping(path = <font>"api/v1")
public class UserController {
    private final UserRepository userRepository;
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping(
"/users")
    public List<User> getUsers() {
        return userRepository.findAll();
    }
}

假設你使用電子郵件admin密碼註冊了一個新使用者ssshhhh。然後為了生成 JWT 令牌,你可以使用以下 curl 請求。

curl -X POST -H <font>"Content-Type: application/json" \
  -d '{
"email": "admin", "password": "ssshhhh"}' http://localhost:8080/api/v1/auth/login<i>


它將返回一個 JWT 令牌,您可以使用它來驗證使用者身份。將其儲存在某處。

現在,為了訪問受限使用者端點,您可以使用以下 curl 請求。

curl -X GET -H <font>"Authorization: Bearer <token>" http://localhost:8080/api/v1/users<i>

總結
本動手教程為您提供了在 Spring Boot 應用程式中實現 JWT 身份驗證的知識。我們探索了使用者註冊、登入和訪問控制,利用 Spring Security 和 JPA 實現資料永續性。透過遵循這些步驟並根據您的特定需求自定義程式碼示例,您可以保護 API 端點並確保授權使用者訪問。請記住優先考慮安全最佳實踐。以下是一些需要考慮的其他要點:

  • 金鑰管理:將您的 JWT 金鑰安全地儲存在環境變數或專用金鑰管理服務中。切勿將其暴露在您的程式碼庫中。
  • Token過期:為JWT token設定合理的過期時間,防止因token被洩露導致未經授權的訪問。
  • 錯誤處理:針對無效或過期的令牌實施適當的錯誤處理機制,以便向使用者提供資訊反饋。
  • 高階功能:探索高階 JWT 功能,例如用於延長會話壽命的重新整理令牌和用於細粒度授權的基於角色的訪問控制 (RBAC)。有了 JWT 身份驗證,您的 Spring Boot 應用程式就有望成為一個安全且強大的平臺。您可以放心部署,因為您知道使用者訪問得到了妥善控制。

原始碼:GitHub.

相關文章