1. 簡介
今天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) 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 即可
compile('org.springframework.boot:spring-boot-starter-security')
2.2 配置Spring Security
第一步:建立Spring Security Web的配置類,並繼承web應用的安全介面卡WebSecurityConfigurerAdapter。
第二步:重寫configure方法,可以新增登入驗證失敗處理器、退出成功處理器、並按照ant風格開啟攔截規則等相關配置。
第三步:配置預設或者自定義的密碼加密邏輯、AuthenticationManager、各種過濾器等,比如JWT過濾器。
配置程式碼如下:
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在使用的過程中也需要注意處理JWT的失效問題。
3.1 匯入JWT庫
Spring Security 整合JWT還需要額外引入io.jsonwebtoken:jjwt 庫
compile('io.jsonwebtoken:jjwt:0.9.1')
3.2 建立JWT工具類
JWT工具類主要負責:
-
1)、token的生成。建議使用使用者的登入賬號作為生成token的屬性,這是考慮到賬號的唯一性和可讀性都很高。
-
2)、token的驗證。包括token是否已經自然過期、是否因為人為操作導致失效、資料的格式是否合法等。
程式碼如下:
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<String, Any>()
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, Any>): 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是否有效
程式碼如下:
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 過濾器之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter::class.java)
完整的ITDragonWebSecurityConfig類的程式碼如下:
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 登入驗證
程式碼如下:
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是以JWT自帶的建立時間和到期時間、與傳入的時間做判斷。來判斷token是否失效,這樣可以減少和資料庫的互動。
解決自然過期的token失效設計如下:
-
1)、生成token時,設定setExpiration屬性
-
1)、校驗token時,通過獲取expiration屬性,並和當前時間做比較,若在當前時間之前則說明token已經過期
解決人為操作上的token失效設計如下:
- 1)、生成token時,設定setIssuedAt屬性
- 2)、使用者表新增tokenInvalidDate欄位。在重新整理token、修改使用者密碼等操作時,更新這個欄位
- 3)、校驗token時,通過獲取issuedAt屬性,並和tokenInvalidDate時間做比較,若在tokenInvalidDate時間之前則說明token已經失效
程式碼如下:
/**
* 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
}
}
文章到這裡就結束了,感謝各位看官!!???
完整程式碼訪問GitHub地址:https://github.com/ITDragonBlog/daydayup/tree/master/SpringBoot/spring-boot-springsecurity-jwt
專案所在目錄可能會發生變化,但是https://github.com/ITDragonBlog/daydayup 地址不會變