今天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.
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()
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是否有效
代码如下:
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()
}
}
代码如下:
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)
}
}
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
}
}