SpringBoot 集成SpringSecurity JWT


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
    }
}

文章到這里就結束了,感謝各位看官!!😘😘😘

擴展鏈接:

Sort 坑爹的字符串排序

完整代碼訪問GitHub地址:https://github.com/ITDragonBlog/daydayup/tree/master/SpringBoot/spring-boot-springsecurity-jwt

項目所在目錄可能會發生變化,但是https://github.com/ITDragonBlog/daydayup 地址不會變


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM