1. Introduction
Today ITDragon shares an article about using JWT in the Spring Security framework and how to handle invalid tokens.
1.1 SpringSecurity
Spring Security is a Security framework provided by Spring. Provides authentication, authorization, and common attack defense functions. Rich and powerful functions.
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
Open Authorization (OAuth) Defines a secure and Open standard for user resource Authorization. OAuth2 is the second version of the OAuth protocol. OAuth is commonly used for third-party application login authorization. Obtain the user’s authorization information without knowing the user’s account and password. The common authorization modes are authorization code mode, simplified mode, password mode, and client mode.
1.3 JWT
JWT (JSON Web Token) is an open standard that securely transfers information between parties as JSON objects. Digital signatures can be used for verification and trust. JWT can solve the problems of distributed system login authorization, single sign-on cross-domain, etc.
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 integrates With SpringSecurity
SpringBoot’s integration with Spring Security is very convenient and involves two simple steps: the tutorial and configuration
2.1 Importing the Spring Security library
As Spring’s own project, you just need to import spring-boot-starter- Security
compile('org.springframework.boot:spring-boot-starter-security')
2.2 Configuring Spring Security
Step 1: create a Spring Security Web configuration, and Web application Security adapter WebSecurityConfigurerAdapter inheritance.
Step 2: Override the configure method to add login authentication failure handlers, exit success handlers, and enable ant style interception rules.
Step 3: Configure default or custom password encryption logic, AuthenticationManager, various filters, etc., such as JWT filters.
The configuration code is as follows:
`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 configuration password encoder fun passwordEncoder () : PasswordEncoder{ return BCryptPasswordEncoder() } override fun configure(http: HttpSecurity) {/ / configuration exception handler HTTP. ExceptionHandling () authenticationEntryPoint (authenticationEntryPoint) / / configure appropriate logic .and().logout().logoutSuccesshandler (logoutSuccessHandler) // Open permission blocking. And ().authorizerequests () // open requests that do not need to be blocked .antmatchers (httpmethod. POST, "/itdragon/ API /v1/user").permitall () // Allow all OPTIONS requests to.antmatchers (httpmethod. OPTIONS, "/ * *"). PermitAll () / / allow access to resources. Static antMatchers (HttpMethod. GET, "/", "/ *. HTML", "/ favicon. Ico", "/ / *. * * HTML", "/ * * / *. CSS", "/**/*.js").permitall ().antmatchers ("/itdragon/ API /v1/**").authenticated() // close cross-site request forgeries for now, It limits most methods except get. And (). () to CSRF. Disable () / / allow cross-domain request. The cors (). The disable ()}Copy the code
Note:
1) The anti-cross-site request forgery function of CSRF is enabled by default, and can be temporarily disabled in the debugging process.
2), after the logout() successfully exits, it will jump to the /login route by default, which is not friendly to the projects with the front and back ends separated.
3) The permitAll() method decorates configuration suggestions written above authenticated() method.
3. SpringSecurity configures JWT
The advantages of JWT are many and simple to use. However, we also need to pay attention to the failure of JWT during the use of ITDragon.
3.1 Importing the JWT Library
Spring Security integration with JWT also requires the additional introduction of IO. Jsonwebtoken: the JJWT library
The compile (' IO. Jsonwebtoken: JJWT: 0.9.1 ')
3.2 Creating a JWT utility class
JWT tools are mainly responsible for:
1) Token generation. It is recommended to use the login account of the user as the attribute for generating the token. In this case, the uniqueness and readability of the account are considered.
2) Token authentication. The information includes whether the token has expired naturally, whether the token is invalid due to human operation, and whether the data format is valid.
The code is as follows:
`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 /** * Generate Token * 1. You are advised to use a unique and readable field as the token generation parameter */ Fun generateToken(username: String): String { return try { val claims = HashMap<String, Any>() claims[CLAIM_KEY_USERNAME] = username generateJWT(claims)} Catch (e: Exception) {""}} /** * Verify token * 1. */ fun validateToken(token: String, userDetails: userDetails): / fun validateToken(token: String, userDetails: userDetails): Boolean { userDetails as JwtUser return getUsernameFromToken(token) == userDetails.username && ! IsInvalid (token, populated userDetails. Model. TokenInvalidDate)} / * * * token failure judgment, on the basis of the following: * 1. The token becomes invalid after key fields are changed, including password change and user logout * 2. Token expires */ 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 expiration Based on local memory, the problem is that restart service fails * 2. Based on database, commonly used Redis database, but frequent requests are also not small expense * 3. */ Private fun isExpired(token: String): Boolean { return try { val claims = parseJWT(token) claims!! .expiration. Before (Date())} catch (e: Exception) {false}} /** * getUsernameFromToken */ fun getUsernameFromToken(token: String): String { return try { val claims = parseJWT(token) claims!! [CLAIM_KEY_USERNAME].tostring ()} catch (e: Exception) {""}} /** * generateJWT(CLAIM_KEY_USERNAME) Map<String, Any>): String {return jwts.builder ().setClaims(claims) // Define attributes. The design is as follows: SetExpiration (Date(System.CurrentTimemillis () + expiration * 1000)) // Set the token validity period .signWith(SignatureAlgorithm.HS512, Private fun parseJWT(token: String): private fun parseJWT(token: String): Claim? {return try {jwts.parser ().setSigningKey(secret) // Set key.parseclaimsJws (token) // Parse token. Body} Catch (e: Exception) { null } }Copy the code
} `
3.3 Adding a JWT Filter
The added JWT filter needs to implement the following functions:
1) The custom JWT filter should be executed before the user name and password filter provided by Spring Security. 2) The request to be intercepted must carry token information. 3) The code to determine whether the passed token is valid is as follows:
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 /** * filter validation step * Step 1: Obtain the token from the request header * Step 2: obtain the user information from the token and determine whether the token data is valid * Step 3: Check whether the token is valid, including whether the token has expired and whether the token has been refreshed * Step 4: */ Override fun doFilterInternal(Request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {val authHeader = Request.getheader (this.tokenHeader) if (authHeader! = null && Authheader.startswith (tokenHead)) {val authToken = Authheader.substring (tokenheader.length) // Obtain user information from 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 : The ${tempUser. Username} ")} / / authentication token is valid val populated userDetails = this. UserDetailsService. LoadUserByUsername (username) the if (jwtTokenUtil.validateToken(authToken, UserDetails)) {// Add user information to the Context val authentication = of SecurityContextHolder UsernamePasswordAuthenticationToken(userDetails, userDetails.password, userDetails.authorities) authentication.details = WebAuthenticationDetailsSource().buildDetails(request) SecurityContextHolder.getContext().authentication = authentication } } filterChain.doFilter(request, response) } }Copy the code
Add JWT filter to the UsernamePasswordAuthenticationFilter filter before
http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter::class.java)
Complete ITDragonWebSecurityConfig class code is as follows:
`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 ()} /** * Configure exception handlers and logout handlers * Step 3: Enable permission interception for all requests * Step 4: Open requests that do not require interception, such as user registration, OPTIONS requests, and static resources * Step 5: Allow OPTIONS requests to prepare for cross-domain configuration * Step 6: */ Override Fun configure(HTTP: HttpSecurity) {/ / add JWT filters HTTP addFilterBefore (jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter: : class. Java) / / configuration exception handler ExceptionHandling (.) authenticationEntryPoint (authenticationEntryPoint) / / configure appropriate logic. And (). Logout () .logoutSuccesshandler (logoutSuccessHandler) // Enable permission blocking. And ().authorizerequests () // Open requests that do not need to be blocked .antmatchers (httpmethod. POST, "/itdragon/ API /v1/user").permitall () // Allow all OPTIONS requests to.antmatchers (httpmethod. OPTIONS, "/ * *"). PermitAll () / / allow access to resources. Static antMatchers (HttpMethod. GET, "/", "/ *. HTML", "/ favicon. Ico", "/ / *. * * HTML", "/ * * / *. CSS", "/**/*.js").permitall ().antmatchers ("/itdragon/ API /v1/**").authenticated() // close cross-site request forgeries for now, It limits most methods except get. And (). () to CSRF. Disable () / / allow cross-domain request. The cors (). The disable ()}Copy the code
}
3.4 Login Verification
The code is as follows:
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 {/ / object initialization UsernamePasswordAuthenticationToken val upAuthenticationToken = UsernamePasswordAuthenticationToken(username, Password) / / authentication val authentication. = the authenticationManager authenticate (upAuthenticationToken) / / back to store user information to after a successful verification Context of securityContextHolder securityContextHolder. GetContext (). The authentication = authentication / / token is generated and returns the val userDetails = userDetailsService.loadUserByUsername(username) return jwtTokenUtil.generateToken(userDetails.username) } }Copy the code
3.5 About JWT Failure handling
Token invalidation includes the common expiration invalidation, refresh invalidation, change password invalidation, and user logout invalidation (some scenarios do not need to be used).
ITDragon is based on the creation time and expiration time of JWT, and the incoming time. To determine whether the token is invalid, which can reduce the interaction with the database.
The design to solve the natural expiration token invalidation is as follows:
1) Set the setExpiration attribute when the token is generated
1) When checking the token, obtain the expiration attribute and compare it with the current time. If the token is earlier than the current time, it indicates that the token has expired
The design to solve the token invalidation in artificial operation is as follows:
Set the setIssuedAt attribute when the token is generated. Add tokenInvalidDate field to the user table. Update this field when refreshing the token or changing the user password. When verifying the token, obtain the issuedAt attribute and compare it with the tokenInvalidDate date. If the value is earlier than the tokenInvalidDate date, the token is invalid
The code is as follows:
* The token becomes invalid after key fields are changed, including password change and user logout * 2. The token expires and becomes invalid */ 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 expiration Based on local memory, the problem is that the system fails after restart * 2. Based on database, commonly used Redis database, but frequent requests are also not small expense * 3. */ Private fun isExpired(token: String): Boolean { return try { val claims = parseJWT(token) claims!! .expiration.before(Date()) } catch (e: Exception) { false } }Copy the code