使用JWT的Spring Security - JakubLeško
Spring Security的預設行為很容易用於標準Web應用程式。它使用基於cookie的身份驗證和會話。此外,它會自動為您處理CSRF令牌(防止中間人攻擊)。在大多數情況下,您只需要為特定路由設定授權許可權,這是透過從資料庫中檢索使用者的方式實現的。
另一方面,如果您只構建一個將與外部服務或SPA /移動應用程式一起使用的REST API,則可能不需要完整會話Session。這是JWT (JSON Web令牌) 一個小型數字簽名令牌的用途。所有需要的資訊都可以儲存在令牌中,因此您的伺服器可以實現無會話(no httpsession)。
JWT需要附加到每個HTTP請求,以便伺服器可以授權您的使用者。有一些選項如何傳送令牌。例如,作為URL引數或使用Bearer架構的HTTP Authorization標頭:
Authorization: Bearer <token string>
SON Web Token包含三個主要部分:
- 標頭 - 通常包括令牌型別和雜湊演算法。
- 有效負載 - 通常包括有關使用者的資料以及為其頒發令牌的資料。
- 簽名 - 它用於驗證訊息是否在此過程中未被更改
示例令牌
授權標頭中的JWT令牌可能如下所示:
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDI1ODksInJvbCI6WyJST0xFX1VTRVIiXX0.GzUPUWStRofrWI9Ctfv2h-XofGZwcOog9swtuqg1vSkA8kDWLcY3InVgmct7rq4ZU3lxI6CGupNgSazypHoFOA |
三個部分用逗號分隔 - 標頭,宣告和簽名。標頭和有效負載宣告是Base64編碼的JSON物件。
Header:
{ "typ": "JWT", "alg": "HS512" } |
有效負載宣告:
{ "iss": "secure-api", "aud": "secure-app", "sub": "user", "exp": 1548242589, "rol": [ "ROLE_USER" ] } |
示例應用
在下面的示例中,我們將建立一個包含2個路由的簡單API - 一個公開可用,一個僅授權使用者。
我們將使用頁面start.spring.io來建立我們的應用程式框架並選擇安全性和Web依賴項。其餘選項取決於您的喜好。
JWT對Java的支援由庫JJWT提供,因此我們還需要將以下依賴項新增到pom.xml檔案中:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.10.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.5</version> <scope>runtime</scope> </dependency> |
控制器
我們的示例應用程式中的控制器將盡可能簡單。如果使用者未獲得授權,他們將只返回訊息或HTTP 403錯誤程式碼。
@RestController @RequestMapping("/api/public") public class PublicController { @GetMapping public String getMessage() { return "Hello from public API controller"; } } |
@RestController @RequestMapping("/api/private") public class PrivateController { @GetMapping public String getMessage() { return "Hello from private API controller"; } } |
過濾器
首先,我們將定義一些可重用的常量和預設值,用於生成和驗證JWT。
注意:您不應該將JWT簽名金鑰硬編碼到您的應用程式程式碼中(我們將在示例中暫時忽略它)。您應該使用環境變數或.properties檔案。此外,鍵需要有適當的長度。例如,HS512演算法需要金鑰,其大小至少為512位元組。
public final class SecurityConstants { public static final String AUTH_LOGIN_URL = "/api/authenticate"; // Signing key for HS512 algorithm // You can use the page http://www.allkeysgenerator.com/ to generate all kinds of keys public static final String JWT_SECRET = "n2r5u8x/A%D*G-KaPdSgVkYp3s6v9y$B&E(H+MbQeThWmZq4t7w!z%C*F-J@NcRf"; // JWT token defaults public static final String TOKEN_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; public static final String TOKEN_TYPE = "JWT"; public static final String TOKEN_ISSUER = "secure-api"; public static final String TOKEN_AUDIENCE = "secure-app"; } |
第一個過濾器將直接用於使用者身份驗證。它將從URL檢查使用者名稱和密碼引數,並呼叫Spring的身份驗證管理器來驗證它們。
如果使用者名稱和密碼正確,則filter將建立一個JWT令牌並在HTTP Authorization標頭中返回它。
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { var username = request.getParameter("username"); var password = request.getParameter("password"); var authenticationToken = new UsernamePasswordAuthenticationToken(username, password); return authenticationManager.authenticate(authenticationToken); } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, Authentication authentication) { var user = ((User) authentication.getPrincipal()); var roles = user.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); var signingKey = SecurityConstants.JWT_SECRET.getBytes(); var token = Jwts.builder() .signWith(Keys.hmacShaKeyFor(signingKey), SignatureAlgorithm.HS512) .setHeaderParam("typ", SecurityConstants.TOKEN_TYPE) .setIssuer(SecurityConstants.TOKEN_ISSUER) .setAudience(SecurityConstants.TOKEN_AUDIENCE) .setSubject(user.getUsername()) .setExpiration(new Date(System.currentTimeMillis() + 864000000)) .claim("rol", roles) .compact(); response.addHeader(SecurityConstants.TOKEN_HEADER, SecurityConstants.TOKEN_PREFIX + token); } } |
第二個過濾器處理所有HTTP請求,並檢查是否存在具有正確令牌的Authorization標頭。例如,如果令牌未過期或簽名金鑰正確。
如果令牌有效,那麼過濾器會將身份驗證資料新增到Spring的安全上下文中。
public class JwtAuthorizationFilter extends BasicAuthenticationFilter { private static final Logger log = LoggerFactory.getLogger(JwtAuthorizationFilter.class); public JwtAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { var authentication = getAuthentication(request); var header = request.getHeader(SecurityConstants.TOKEN_HEADER); if (StringUtils.isEmpty(header) || !header.startsWith(SecurityConstants.TOKEN_PREFIX)) { filterChain.doFilter(request, response); return; } SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(request, response); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { var token = request.getHeader(SecurityConstants.TOKEN_HEADER); if (StringUtils.isNotEmpty(token)) { try { var signingKey = SecurityConstants.JWT_SECRET.getBytes(); var parsedToken = Jwts.parser() .setSigningKey(signingKey) .parseClaimsJws(token.replace("Bearer ", "")); var username = parsedToken .getBody() .getSubject(); var authorities = ((List<?>) parsedToken.getBody() .get("rol")).stream() .map(authority -> new SimpleGrantedAuthority((String) authority)) .collect(Collectors.toList()); if (StringUtils.isNotEmpty(username)) { return new UsernamePasswordAuthenticationToken(username, null, authorities); } } catch (ExpiredJwtException exception) { log.warn("Request to parse expired JWT : {} failed : {}", token, exception.getMessage()); } catch (UnsupportedJwtException exception) { log.warn("Request to parse unsupported JWT : {} failed : {}", token, exception.getMessage()); } catch (MalformedJwtException exception) { log.warn("Request to parse invalid JWT : {} failed : {}", token, exception.getMessage()); } catch (SignatureException exception) { log.warn("Request to parse JWT with invalid signature : {} failed : {}", token, exception.getMessage()); } catch (IllegalArgumentException exception) { log.warn("Request to parse empty or null JWT : {} failed : {}", token, exception.getMessage()); } } return null; } } |
安全配置
我們需要配置的最後一部分是Spring Security本身。配置很簡單,我們需要設定一些細節:
- 密碼編碼器 - 在我們的例子中是bcrypt
- CORS配置
- 身份驗證管理器 - 在我們的例子中簡單的記憶體身份驗證,但在現實生活中,你需要像UserDetailsService這樣的東西
- 設定哪些端點是安全的以及哪些端點是公開可用的
- 將2個過濾器新增到安全上下文中
- 禁用會話管理 - 我們不需要會話,因此這將阻止會話cookie的建立
@EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and() .csrf().disable() .authorizeRequests() .antMatchers("/api/public").permitAll() .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager())) .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user") .password(passwordEncoder().encode("password")) .authorities("ROLE_USER"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public CorsConfigurationSource corsConfigurationSource() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); return source; } } |
測試
請求公共API:
GET http://localhost:8080/api/public
HTTP/1.1 200 X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Type: text/plain;charset=UTF-8 Content-Length: 32 Date: Sun, 13 Jan 2019 12:22:14 GMT Hello from public API controller Response code: 200; Time: 18ms; Content length: 32 bytes |
驗證使用者:
POST http://localhost:8080/api/authenticate?username=user&password=password
HTTP/1.1 200 Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDYwNzUsInJvbCI6WyJST0xFX1VTRVIiXX0.yhskhWyi-PgIluYY21rL0saAG92TfTVVVgVT1afWd_NnmOMg__2kK5lcna3lXzYI4-0qi9uGpI6Ul33-b9KTnA X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Length: 0 Date: Sun, 13 Jan 2019 12:21:15 GMT <Response body is empty> Response code: 200; Time: 167ms; Content length: 0 bytes |
使用令牌請求私有API:
GET http://localhost:8080/api/private Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDI1ODksInJvbCI6WyJST0xFX1VTRVIiXX0.GzUPUWStRofrWI9Ctfv2h-XofGZwcOog9swtuqg1vSkA8kDWLcY3InVgmct7rq4ZU3lxI6CGupNgSazypHoFOA
輸出:
HTTP/1.1 200 X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Type: text/plain;charset=UTF-8 Content-Length: 33 Date: Sun, 13 Jan 2019 12:22:48 GMT Hello from private API controller Response code: 200; Time: 12ms; Content length: 33 bytes |
請求沒有令牌的私有API:
當您在沒有有效JWT的情況下呼叫安全端點時,您將收到HTTP 403訊息。
GET http://localhost:8080/api/private
結論
本文的目的不是展示如何在Spring Security中使用JWT的正確方法。這是一個如何在現實應用程式中執行此操作的示例。
GitHub儲存庫中找到此示例API的完整原始碼。
相關文章
- Spring Security + JWTSpringJWT
- Spring Security(一):整合JWTSpringJWT
- [譯] JWT 整合 Spring Security OAuthJWTSpringOAuth
- Spring Cloud Security:Oauth2結合JWT使用SpringCloudOAuthJWT
- 使用JWT保護你的Spring Boot應用 - Spring Security實戰JWTSpring Boot
- Spring Security原始碼分析十一:Spring Security OAuth2整合JWTSpring原始碼OAuthJWT
- 乾貨|一個案例學會Spring Security 中使用 JWTSpringJWT
- 適合新手入門Spring Security With JWT的demoSpringJWT
- Spring Security 實戰乾貨:使用 JWT 認證訪問介面SpringJWT
- Spring Security OAuth2.0認證授權三:使用JWT令牌SpringOAuthJWT
- Spring Boot 3中將JWT與Spring Security 6整合Spring BootJWT
- 使用 Spring Security JWT 令牌簽名實現 REST API 安全性SpringJWTRESTAPI
- 重拾後端之Spring Boot(四):使用JWT和Spring Security保護REST API後端Spring BootJWTRESTAPI
- 二、Spring Security的使用Spring
- Spring Boot 2 + Spring Security 5 + JWT 的單頁應用Restful解決方案Spring BootJWTREST
- Spring Security原始碼分析十二:Spring Security OAuth2基於JWT實現單點登入Spring原始碼OAuthJWT
- 基於Spring Security和 JWT的許可權系統設計SpringJWT
- Spring Security + jwt 許可權系統設計,包含SQLSpringJWTSQL
- Spring Cloud實戰系列(十) - 單點登入JWT與Spring Security OAuthSpringCloudJWTOAuth
- Spring Boot Security 整合 JWT 實現 無狀態的分散式API介面Spring BootJWT分散式API
- 【專案實踐】一文帶你搞定Spring Security + JWTSpringJWT
- Spring Security 使用總結Spring
- Spring Boot中使用token:jwtSpring BootJWT
- 記錄springboot 3.3.5 版本整合 swagger +spring security + jwtSpring BootSwaggerJWT
- Spring Security OAuth2.0認證授權五:使用者資訊擴充套件到jwtSpringOAuth套件JWT
- 使用Spring Security控制會話Spring會話
- Spring Boot Security OAuth2 實現支援JWT令牌的授權伺服器Spring BootOAuthJWT伺服器
- SpringBoot整合Spring security JWT實現介面許可權認證Spring BootJWT
- 譯見|構建使用者管理微服務(五):使用 JWT 令牌和 Spring Security 來實現身份驗證微服務JWTSpring
- spring security之httpSecurity使用示例SpringHTTP
- Spring Boot + Security + JWT 實現Token驗證+多Provider——登入系統Spring BootJWTIDE
- spring security oauth2搭建resource-server demo及token改造成JWT令牌SpringOAuthServerJWT
- Spring Security 6.3基於JWT身份驗證與授權開源專案SpringJWT
- Spring SecuritySpring
- Spring Security原始碼分析八:Spring Security 退出Spring原始碼
- Spring Security使用(二) 非同步登入Spring非同步
- Spring Boot —— Spring SecuritySpring Boot
- Spring Security原始碼分析九:Spring Security Session管理Spring原始碼Session