使用 Spring Security JWT 令牌簽名實現 REST API 安全性

banq發表於2024-05-23

一種流行的方法是使用 JSON Web 令牌 (JWT)。 Spring Security 有助於在 Spring 應用程式中進行基於 JWT 的身份驗證和授權。在本文中,我們將瞭解如何建立用於簽署 JWT 令牌的 Spring Security 金鑰,並在 Spring Boot 應用程式中使用它來保護 REST API。

新增 JSON Web Token 依賴項
在pom.xml專案檔案中(如果使用 Maven),新增以下用於 JWT 令牌處理的依賴項:

<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>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>

建立 JWT 實用程式類
接下來,建立一個用於處理 JWT 操作的實用程式類。該類將負責生成 JWT 令牌並驗證它們。下面是 JWT 實用程式類的簡單實現:

@Component
public class JwtUtil {
 
    @Value(<font>"${jcg.jwt.secret}")
    private String jwtSecret;
 
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(
"Authorities", userDetails.getAuthorities());
 
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 86400))
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                .compact();
    }
 
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
 
    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()
                .setSigningKey(getSignInKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
 
    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
 
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
 
    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
     
    private SecretKey getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

上面的程式碼塊代表一個名為的類JwtUtil,負責處理我們應用程式中的 JWT(JSON Web Token)操作。以下是其功能的細分:

  • 該類使用 @Value 註解從 application.properties 檔案中注入 JWT 金鑰。
  • Token Generation令牌生成:generateToken 方法根據提供的 UserDetails 建立 JWT 令牌。該方法會設定主題(使用者名稱)、釋出日期、過期日期,並使用 HMAC SHA-256 演算法和秘鑰對令牌進行簽名。
  • extractUsername 方法:該方法將 JWT 令牌作為輸入,並返回從令牌的主題請求中提取的使用者名稱。該方法將權利要求提取過程委託給 extractClaim 方法,傳遞令牌和函式引用(Claims::getSubject)以提取主題權利要求。
  • extractClaim 方法:該通用方法接收一個 JWT 標記和一個函式,該函式可從標記的 Claims 物件中解析特定的權利要求。它首先透過呼叫 extractAllClaims 方法從令牌中提取所有索賠。然後,它將提供的 claimsResolver 函式應用到 Claims 物件,以提取所需的權利要求。
  • extractAllClaims 方法:該方法負責解析 JWT 令牌,使用提供的簽名金鑰驗證其簽名,並從令牌正文中提取所有宣告。它使用 Jwts.parser() 方法建立解析器例項,使用 setSigningKey(getSignInKey()) 設定簽名金鑰,然後使用 parseClaimsJws(token) 解析令牌。
  • Secret Key Retrieval:getSignInKey 方法會檢索用於簽署和驗證 JWT 標記的秘鑰。該方法會解碼從 application.properties 檔案中獲取的 base64 編碼秘鑰,並將其轉換為 SecretKey 物件。


生成並設定JWT金鑰
生成強金鑰對於確保 JWT 令牌的安全至關重要。這需要生成由 256 位組成的 HMAC 雜湊字串,並將其配置為檔案中的 JWT 金鑰application.properties。位於devglan.com/online-tools的線上工具生成器可以生成 256 位的 HMAC 雜湊字串。

在 application.properties 中設定 JWT Secret
金鑰生成後,需要在application.propertiesSpring Boot 應用程式的檔案中設定:
jcg.jwt.secret=YOUR_GENERATED_SECRET_KEY

實現認證令牌過濾器
接下來,讓我們實現一個身份驗證令牌過濾器,這對於使用 JWT 令牌保護 REST 端點至關重要。下面是身份驗證令牌過濾器的示例:

@Component
public class JwtRequestFilter extends OncePerRequestFilter {
 
    @Autowired
    private JwtUtil jwtUtil;
 
    @Autowired
    private UserDetailsService userDetailsService;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
 
        final String authorizationHeader = request.getHeader(<font>"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 = this.userDetailsService.loadUserByUsername(username);
 
            if (jwtUtil.validateToken(jwt, userDetails)) {
 
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                        .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                 
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

該過濾器攔截傳入的請求,從請求標頭中提取 JWT 令牌,驗證它們,如果令牌有效,則在 Spring Security 上下文中設定身份驗證。

配置Spring Security
接下來,配置 Spring Security 以使用 JWT 進行身份驗證。建立一個SecurityConfig類並配置如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    private static final String ADMIN = <font>"ADMIN";
    private static final String USER =
"USER";
 
    @Bean
    public JwtRequestFilter jwtRequestFilter() {
        return new JwtRequestFilter();
    }
 
    @Bean
    public DaoAuthenticationProvider authenticationProvider() throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
 
        return authProvider;
    }
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(req -> req
                .requestMatchers(
"/admin/**").hasRole(ADMIN)
                .requestMatchers(
"/user/**").hasAnyRole(USER, ADMIN)
                .requestMatchers(
"/authenticate")
                .permitAll()
                .anyRequest()
                .authenticated())
                .sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
                .authenticationProvider(authenticationProvider())
                .addFilterBefore(jwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
 
        return http.build();
    }
 
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User
                .withUsername(
"thomas")
                .password(encoder().encode(
"paine"))
                .roles(ADMIN).build());
        manager.createUser(User
                .withUsername(
"bill")
                .password(encoder().encode(
"withers"))
                .roles(USER).build());
        return manager;
    }
 
    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source
                = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin(
"*");
        config.addAllowedHeader(
"*");
        config.addAllowedMethod(
"*");
        source.registerCorsConfiguration(
"/**", config);
        return new CorsFilter(source);
    }
}

jwtRequestFilter 方法為 JwtRequestFilter 類定義了一個 bean。該過濾器會攔截傳入的請求、提取 JWT 標記並對其進行身份驗證。

DaoAuthenticationProvider Bean:authenticationProvider 方法為 DaoAuthenticationProvider 類定義了一個 Bean。該提供程式根據所提供的使用者詳細資訊服務和密碼編碼器對使用者進行身份驗證。

SecurityFilterChain Bean: BeansecurityFilterChain 方法根據請求匹配器和角色設定授權規則。
授權規則:.authorizeHttpRequests() 方法為不同型別的請求指定了授權規則:

  • 向 /admin/** 端點發出的請求需要 ADMIN 角色。
  • 向 /user/** 端點發出的請求需要 USER 或 ADMIN 角色。
  • 對 /authenticate 端點的請求無需身份驗證即可允許(permit all)。
  • 所有其他請求(anyRequest())都需要身份驗證。

會話管理:會話管理配置為無狀態(sessionCreationPolicy(STATELESS)),這意味著不會建立伺服器端會話或用於儲存使用者身份驗證狀態。

AuthenticationManager Bean:authenticationManager 方法為 AuthenticationManager 介面定義了一個 Bean。它從身份驗證配置中檢索身份驗證管理器。

UserDetailsService Bean:userDetailsService 方法使用 InMemoryUserDetailsManager 定義記憶體使用者儲存。我們建立了兩個不同角色的使用者:一個是使用者名稱為 "thomas "的 ADMIN 使用者,另一個是使用者名稱為 "bill "的 USER 使用者。

自定義使用者詳細資訊實現
Spring Security 依賴於介面來實現身份驗證和授權。下面是實現身份驗證和授權介面的類UserDetails的實現:UserUserDetails

public class User implements UserDetails {
 
    private int id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
 
    public User() {
    }
 
    public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this.password = password;
        this.username = username;
        this.authorities = authorities;
    }
 
    public User(String username, Collection<String> authorities) {
        this.username = username;
        this.authorities = authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }
 
    <font>// UserDetails interface methods<i>
    @Override
    public String getUsername() {
        return username;
    }
 
    @Override
    public String getPassword() {
        return password;
    }
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
}

User 類實現了 UserDetails 介面,代表應用程式中的一個使用者。它包括使用者名稱、密碼和許可權欄位。
UserDetails 介面是 Spring Security 的核心介面,用於使用者身份驗證和授權。它代表系統中的委託人(使用者),並提供訪問使用者詳細資訊和授權的方法。

  • getAuthorities()方法返回授予使用者的許可權(角色)。
  • getPassword()和getUsername()方法分別返回使用者的密碼和使用者名稱。
  • isAccountNonExpired() isAccountNonLocked()、isCredentialsNonExpired()和isEnabled()方法分別在使用者賬戶未過期、未鎖定、憑證未過期和使用者已啟用的情況下返回true。

與 Spring Boot 整合
最後,讓我們將 JWT 工具和 Spring Security 配置與 Spring Boot 應用程式整合。下面是一個用於身份驗證的 REST 控制器的基本示例:

@RestController
public class AuthController {
 
    @Autowired
    private AuthenticationManager authenticationManager;
 
    @Autowired
    private JwtUtil jwtUtil;
 
    @Autowired
    private UserDetailsService userDetailsService;
 
    @PostMapping(<font>"/authenticate")
    public ResponseEntity createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
            );
        } catch (BadCredentialsException e) {
            throw new Exception(
"Incorrect username or password", e);
        }
 
         
        final UserDetails userDetails = userDetailsService
                .loadUserByUsername(authRequest.getUsername());
 
        final String jwt = jwtUtil.generateToken(userDetails);
 
        return ResponseEntity.ok(new AuthResponse(jwt));
    }
     
    @GetMapping(
"/auth/details")
    public UserDetails getDetails(){
        var detail = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return detail;
    }
 
}

該程式碼塊定義了一個 AuthController 類,負責處理應用程式中與身份驗證相關的 HTTP 請求。
  • 該控制器類自動連線了身份驗證和令牌生成所需的依賴項(AuthenticationManager、JwtUtil 和 UserDetailsService)。
  • createAuthenticationToken 方法:該方法處理對 /authenticate 端點的 POST 請求。它透過將提供的憑證傳遞給 AuthenticationManager 來嘗試對使用者進行身份驗證。如果驗證成功,它會使用 JwtUtil 生成一個 JWT 令牌,並在響應體中返回。
  • getDetails 方法:該方法處理對 /auth/details 端點的 GET 請求。它會從 SecurityContextHolder 中檢索已驗證使用者的詳細資訊(如使用者名稱、授權)。

保護 REST 端點

@RestController
public class SimpleController {
     
    @RolesAllowed(<font>"ADMIN")
    @GetMapping(
"/admin")
    public ResponseEntity<String> testAdmin() {
        return ResponseEntity.ok(
"This is the Admin role");
    }
 
    @RolesAllowed(
"USER")
    @GetMapping(
"/user")
    public ResponseEntity<String> testUser() {
        return ResponseEntity.ok(
"This is the User role");
    }
}

該 SimpleController 類定義了只有具有特定角色的使用者才能訪問的端點。它執行基於角色的訪問控制(RBAC),確保只有具有相應角色的使用者才能執行某些操作。

  • testAdmin 方法:該方法處理對 /admin 端點的 GET 請求。該方法使用 @RolesAllowed("ADMIN")進行註解,指定只有角色為 "ADMIN "的使用者才能訪問該端點。
  • testUser 方法:該方法處理對 /user 端點的 GET 請求。與 testAdmin 方法類似,該方法也使用 @RolesAllowed("USER")註釋,表明只有角色為 "USER "的使用者才能訪問該端點。

為了驗證應用程式的功能,我們可以利用 POSTMAN 向 http://localhost:8080/authenticate 傳送請求並獲取 JWT 令牌。

 

相關文章