1. 程式人生 > >SpringBoot 整合SpringSecurity JWT

SpringBoot 整合SpringSecurity JWT

[TOC] ## 1. 簡介 今天[ITDragon](https://www.cnblogs.com/itdragon/)分享一篇在Spring Security 框架中使用JWT,以及對失效Token的處理方法。 ### 1.1 SpringSecurity Spring Security 是Spring提供的安全框架。提供認證、授權和常見的攻擊防護的功能。功能豐富和強大。 >Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications. ### 1.2 OAuth2 OAuth(Open Authorization)開放授權是為使用者資源的授權定義一個安全、開放的標準。而OAuth2是OAuth協議的第二個版本。OAuth常用於第三方應用授權登入。在第三方無需知道使用者賬號密碼的情況下,獲取使用者的授權資訊。常見的授權模式有:授權碼模式、簡化模式、密碼模式和客戶端模式。 ### 1.3 JWT JWT(json web token)是一個開放的標準,它可以在各方之間作為JSON物件安全地傳輸資訊。可以通過數字簽名進行驗證和信任。JWT可以解決分散式系統登陸授權、單點登入跨域等問題。 > JSON Web Token (JWT) is an open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. ## 2. SpringBoot 整合 SpringSecurity SpringBoot 整合Spring Security 非常方便,也是簡單的兩個步驟:導包和配置 ### 2.1 匯入Spring Security 庫 作為Spring的自家專案,只需要匯入spring-boot-starter-security 即可 ```groovy compile('org.springframework.boot:spring-boot-starter-security') ``` ### 2.2 配置Spring Security 第一步:建立Spring Security Web的配置類,並繼承web應用的安全介面卡WebSecurityConfigurerAdapter。 第二步:重寫configure方法,可以新增登入驗證失敗處理器、退出成功處理器、並按照ant風格開啟攔截規則等相關配置。 第三步:配置預設或者自定義的密碼加密邏輯、AuthenticationManager、各種過濾器等,比如JWT過濾器。 配置程式碼如下: ```kotlin package com.itdragon.server.config import com.itdragon.server.security.service.ITDragonJwtAuthenticationEntryPoint import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod 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.configuration.WebSecurityConfigurerAdapter import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder @Configuration @EnableWebSecurity class ITDragonWebSecurityConfig: WebSecurityConfigurerAdapter() { @Autowired lateinit var authenticationEntryPoint: ITDragonJwtAuthenticationEntryPoint /** * 配置密碼編碼器 */ @Bean fun passwordEncoder(): PasswordEncoder{ return BCryptPasswordEncoder() } override fun configure(http: HttpSecurity) { // 配置異常處理器 http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) // 配置登出邏輯 .and().logout() .logoutSuccessHandler(logoutSuccessHandler) // 開啟許可權攔截 .and().authorizeRequests() // 開放不需要攔截的請求 .antMatchers(HttpMethod.POST, "/itdragon/api/v1/user").permitAll() // 允許所有OPTIONS請求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 允許靜態資源訪問 .antMatchers(HttpMethod.GET, "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() // 對除了以上路徑的所有請求進行許可權攔截 .antMatchers("/itdragon/api/v1/**").authenticated() // 先暫時關閉跨站請求偽造,它限制除了get以外的大多數方法。 .and().csrf().disable() // 允許跨域請求 .cors().disable() } } ``` 注意: * 1)、csrf防跨站請求偽造的功能是預設開啟,除錯過程中可以先暫時關閉。 * 2)、logout()退出成功後預設跳轉到/login路由上,對於前後端分離的專案並不友好。 * 3)、permitAll()方法修飾的配置建議寫在authenticated()方法的上面。 ## 3. SpringSecurity 配置JWT JWT的優點有很多,使用也很簡單。但是我們[ITDragon](https://www.cnblogs.com/itdragon/)在使用的過程中也需要注意處理JWT的失效問題。 ### 3.1 匯入JWT庫 Spring Security 整合JWT還需要額外引入io.jsonwebtoken:jjwt 庫 ```groovy compile('io.jsonwebtoken:jjwt:0.9.1') ``` ### 3.2 建立JWT工具類 JWT工具類主要負責: * 1)、token的生成。建議使用使用者的登入賬號作為生成token的屬性,這是考慮到賬號的唯一性和可讀性都很高。 * 2)、token的驗證。包括token是否已經自然過期、是否因為人為操作導致失效、資料的格式是否合法等。 程式碼如下: ```kotlin package com.itdragon.server.security.utils import com.itdragon.server.security.service.JwtUser import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import org.springframework.beans.factory.annotation.Value import org.springframework.security.core.userdetails.UserDetails import org.springframework.stereotype.Component import java.util.* private const val CLAIM_KEY_USERNAME = "itdragon" @Component class JwtTokenUtil { @Value("\${itdragon.jwt.secret}") private val secret: String = "ITDragon" @Value("\${itdragon.jwt.expiration}") private val expiration: Long = 24 * 60 * 60 /** * 生成令牌Token * 1. 建議使用唯一、可讀性高的欄位作為生成令牌的引數 */ fun generateToken(username: String): String { return try { val claims = HashMap() claims[CLAIM_KEY_USERNAME] = username generateJWT(claims) } catch (e: Exception) { "" } } /** * 校驗token * 1. 判斷使用者名稱和token包含的屬性一致 * 2. 判斷token是否失效 */ fun validateToken(token: String, userDetails: UserDetails): Boolean { userDetails as JwtUser return getUsernameFromToken(token) == userDetails.username && !isInvalid(token, userDetails.model.tokenInvalidDate) } /** * token 失效判斷,依據如下: * 1. 關鍵欄位被修改後token失效,包括密碼修改、使用者退出登入等 * 2. token 過期失效 */ private fun isInvalid(token: String, tokenInvalidDate: Date?): Boolean { return try { val claims = parseJWT(token) claims!!.issuedAt.before(tokenInvalidDate) && isExpired(token) } catch (e: Exception) { false } } /** * token 過期判斷,常見邏輯有幾種: * 1. 基於本地記憶體,問題是重啟服務失效 * 2. 基於資料庫,常用的有Redis資料庫,但是頻繁請求也是不小的開支 * 3. 用jwt的過期時間和當前時間做比較(推薦) */ private fun isExpired(token: String): Boolean { return try { val claims = parseJWT(token) claims!!.expiration.before(Date()) } catch (e: Exception) { false } } /** * 從token 中獲取使用者名稱 */ fun getUsernameFromToken(token: String): String { return try { val claims = parseJWT(token) claims!![CLAIM_KEY_USERNAME].toString() } catch (e: Exception) { "" } } /** * 生成jwt方法 */ fun generateJWT(claims: Map): String { return Jwts.builder() .setClaims(claims) // 定義屬性 .設計如下:(Date()) // 設定發行時間 .setExpiration(Date(System.currentTimeMillis() + expiration * 1000)) // 設定令牌有效期 .signWith(SignatureAlgorithm.HS512, secret) // 使用指定的演算法和金鑰對jwt進行簽名 .compact() // 壓縮字串 } /** * 解析jwt方法 */ private fun parseJWT(token: String): Claims? { return try { Jwts.parser() .setSigningKey(secret) // 設定金鑰 .parseClaimsJws(token) // 解析token .body } catch (e: Exception) { null } } } ``` ### 3.3 新增JWT過濾器 新增的JWT過濾器需要實現以下幾個功能: * 1)、自定義的JWT過濾器要在Spring Security 提供的使用者名稱密碼過濾器之前執行 * 2)、要保證需要攔截的請求都必須帶上token資訊 * 3)、判斷傳入的token是否有效 程式碼如下: ```kotlin package com.itdragon.server.security.service import com.itdragon.server.security.utils.JwtTokenUtil import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter import javax.servlet.FilterChain import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @Component class ITDragonJwtAuthenticationTokenFilter: OncePerRequestFilter() { @Value("\${itdragon.jwt.header}") lateinit var tokenHeader: String @Value("\${itdragon.jwt.tokenHead}") lateinit var tokenHead: String @Autowired lateinit var userDetailsService: UserDetailsService @Autowired lateinit var jwtTokenUtil: JwtTokenUtil /** * 過濾器驗證步驟 * 第一步:從請求頭中獲取token * 第二步:從token中獲取使用者資訊,判斷token資料是否合法 * 第三步:校驗token是否有效,包括token是否過期、token是否已經重新整理 * 第四步:檢驗成功後將使用者資訊存放到SecurityContextHolder Context中 */ override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { // 從請求頭中獲取token val authHeader = request.getHeader(this.tokenHeader) if (authHeader != null && authHeader.startsWith(tokenHead)) { val authToken = authHeader.substring(tokenHead.length) // 從token中獲取使用者資訊 val username = jwtTokenUtil.getUsernameFromToken(authToken) if (username.isBlank()) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Auth token is illegal") return } if (null != SecurityContextHolder.getContext().authentication) { val tempUser = SecurityContextHolder.getContext().authentication.principal tempUser as JwtUser println("SecurityContextHolder : ${tempUser.username}") } // 驗證token是否有效 val userDetails = this.userDetailsService.loadUserByUsername(username) if (jwtTokenUtil.validateToken(authToken, userDetails)) { // 將使用者資訊新增到SecurityContextHolder 的Context val authentication = UsernamePasswordAuthenticationToken(userDetails, userDetails.password, userDetails.authorities) authentication.details = WebAuthenticationDetailsSource().buildDetails(request) SecurityContextHolder.getContext().authentication = authentication } } filterChain.doFilter(request, response) } } ``` 將JWT過濾器新增到UsernamePasswordAuthenticationFilter 過濾器之前 ```kotlin http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter::class.java) ``` 完整的ITDragonWebSecurityConfig類的程式碼如下: ```kotlin package com.itdragon.server.config import com.itdragon.server.security.service.ITDragonJwtAuthenticationEntryPoint import com.itdragon.server.security.service.ITDragonJwtAuthenticationTokenFilter import com.itdragon.server.security.service.ITDragonLogoutSuccessHandler import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import org.springframework.security.authentication.AuthenticationManager 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.configuration.WebSecurityConfigurerAdapter import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter @Configuration @EnableWebSecurity class ITDragonWebSecurityConfig: WebSecurityConfigurerAdapter() { @Autowired lateinit var jwtAuthenticationTokenFilter: ITDragonJwtAuthenticationTokenFilter @Autowired lateinit var authenticationEntryPoint: ITDragonJwtAuthenticationEntryPoint @Autowired lateinit var logoutSuccessHandler: ITDragonLogoutSuccessHandler @Bean fun passwordEncoder(): PasswordEncoder{ return BCryptPasswordEncoder() } @Bean fun itdragonAuthenticationManager(): AuthenticationManager { return authenticationManager() } /** * 第一步:將JWT過濾器新增到預設的賬號密碼過濾器之前,表示token驗證成功後無需登入 * 第二步:配置異常處理器和登出處理器 * 第三步:開啟許可權攔截,對所有請求進行攔截 * 第四步:開放不需要攔截的請求,比如使用者註冊、OPTIONS請求和靜態資源等 * 第五步:允許OPTIONS請求,為跨域配置做準備 * 第六步:允許訪問靜態資源,訪問swagger時需要 */ override fun configure(http: HttpSecurity) { // 新增jwt過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter::class.java) // 配置異常處理器 .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) // 配置登出邏輯 .and().logout() .logoutSuccessHandler(logoutSuccessHandler) // 開啟許可權攔截 .and().authorizeRequests() // 開放不需要攔截的請求 .antMatchers(HttpMethod.POST, "/itdragon/api/v1/user").permitAll() // 允許所有OPTIONS請求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 允許靜態資源訪問 .antMatchers(HttpMethod.GET, "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() // 對除了以上路徑的所有請求進行許可權攔截 .antMatchers("/itdragon/api/v1/**").authenticated() // 先暫時關閉跨站請求偽造,它限制除了get以外的大多數方法。 .and().csrf().disable() // 允許跨域請求 .cors().disable() } } ``` ### 3.4 登入驗證 程式碼如下: ```kotlin package com.itdragon.server.security.service import com.itdragon.server.security.utils.JwtTokenUtil import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.stereotype.Service @Service class ITDragonAuthService { @Autowired lateinit var authenticationManager: AuthenticationManager @Autowired lateinit var userDetailsService: UserDetailsService @Autowired lateinit var jwtTokenUtil: JwtTokenUtil fun login(username: String, password: String): String { // 初始化UsernamePasswordAuthenticationToken物件 val upAuthenticationToken = UsernamePasswordAuthenticationToken(username, password) // 身份驗證 val authentication = authenticationManager.authenticate(upAuthenticationToken) // 驗證成功後回將使用者資訊存放到 securityContextHolder的Context中 SecurityContextHolder.getContext().authentication = authentication // 生成token並返回 val userDetails = userDetailsService.loadUserByUsername(username) return jwtTokenUtil.generateToken(userDetails.username) } } ``` ### 3.5 關於JWT失效處理 Token的失效包括常見的過期失效、重新整理失效、修改密碼失效還有就是使用者登出失效(有的場景不需要) [ITDragon](https://www.cnblogs.com/itdragon/)是以JWT自帶的建立時間和到期時間、與傳入的時間做判斷。來判斷token是否失效,這樣可以減少和資料庫的互動。 解決自然過期的token失效設計如下: * 1)、生成token時,設定setExpiration屬性 * 1)、校驗token時,通過獲取expiration屬性,並和當前時間做比較,若在當前時間之前則說明token已經過期 解決人為操作上的token失效設計如下: * 1)、生成token時,設定setIssuedAt屬性 * 2)、使用者表新增tokenInvalidDate欄位。在重新整理token、修改使用者密碼等操作時,更新這個欄位 * 3)、校驗token時,通過獲取issuedAt屬性,並和tokenInvalidDate時間做比較,若在tokenInvalidDate時間之前則說明token已經失效 程式碼如下: ```kotlin /** * token 失效判斷,依據如下: * 1. 關鍵欄位被修改後token失效,包括密碼修改、使用者退出登入等 * 2. token 過期失效 */ private fun isInvalid(token: String, tokenInvalidDate: Date?): Boolean { return try { val claims = parseJWT(token) claims!!.issuedAt.before(tokenInvalidDate) && isExpired(token) } catch (e: Exception) { false } } /** * token 過期判斷,常見邏輯有幾種: * 1. 基於本地記憶體,問題是系統重啟後失效 * 2. 基於資料庫,常用的有Redis資料庫,但是頻繁請求也是不小的開支 * 3. 用jwt的過期時間和當前時間做比較(推薦) */ private fun isExpired(token: String): Boolean { return try { val claims = parseJWT(token) claims!!.expiration.before(Date()) } catch (e: Exception) { false } } ``` 文章到這裡就結束了,感謝各位看官!!