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 地址不会变

SpringBoot 集成SpringSecurity JWT的更多相关文章

  1. SpringBoot集成Security,JWT,Swagger全分析

    GitHub地址: https://github.com/li-jun0201/springsecuritydemo本项目采用SpringBoot1.5.9, SpringSecurity,JWT, ...

  2. 【使用篇二】SpringBoot集成SpringSecurity(22)

    SpringSecurity是专门针对基于Spring项目的安全框架,充分利用了依赖注入和AOP来实现安全管控.在很多大型企业级系统中权限是最核心的部分,一个系统的好与坏全都在于权限管控是否灵活,是否 ...

  3. SpringBoot - 集成Auth0 JWT

    目录 前言 session认证与Token认证 session认证 Token认证 JWT简介 JWT定义 JWT数据结构 JWT的类库 具体实现 JWT配置 JWT工具类 测试接口 前言 说说JWT ...

  4. Springboot集成SpringSecurity

    一.Spring security 是什么? Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架. 它提供了一组可以在Spring应用上 ...

  5. SpringBoot集成JWT实现token验证

    原文:https://www.jianshu.com/p/e88d3f8151db JWT官网: https://jwt.io/ JWT(Java版)的github地址:https://github. ...

  6. SpringBoot使用SpringSecurity搭建基于非对称加密的JWT及前后端分离的搭建

    SpringBoot使用SpringSecurity搭建基于非对称加密的JWT及前后端分离的搭建 - lhc0512的博客 - CSDN博客 https://blog.csdn.net/lhc0512 ...

  7. SpringBoot集成JWT 实现接口权限认证

    JWT介绍 Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的, 特别适用于分布式站点 ...

  8. SpringBoot集成Spring Security(5)——权限控制

    在第一篇中,我们说过,用户<–>角色<–>权限三层中,暂时不考虑权限,在这一篇,是时候把它完成了. 为了方便演示,这里的权限只是对角色赋予权限,也就是说同一个角色的用户,权限是 ...

  9. SpringBoot集成Spring Security(4)——自定义表单登录

    通过前面三篇文章,你应该大致了解了 Spring Security 的流程.你应该发现了,真正的 login 请求是由 Spring Security 帮我们处理的,那么我们如何实现自定义表单登录呢, ...

随机推荐

  1. 题解 CF917D 【Stranger Trees】

    生成树计数问题用矩阵树定理来考虑. 矩阵树定理求得的为\(\sum\limits_T\prod\limits_{e\in T}v_e\),也就是所有生成树的边权积的和. 这题边是不带权的,应用矩阵树定 ...

  2. Python 正则表达式简单了解

    match 从字符串的开始匹配  如果开头不符合要求  就会报错 search  用字符串里的每一个元素  去匹配找的元素 1.匹配单个字符 \d 数字 \D 非数字 . 匹配任意字符 除了\n [] ...

  3. xctf-web supersqli

    单引号注入,用order by查到了两个column.用union select的时候发现select关键字被过滤了 用分号尝试堆叠注入显示出了两张表 分别查询字段 flag在表19198109311 ...

  4. scrapy中选择器用法

    一.Selector选择器介绍 python从网页中提取数据常用以下两种方法: lxml:基于ElementTree的XML解析库(也可以解析HTML),不是python的标准库 BeautifulS ...

  5. Django开发之模态框提交内容到后台[Object Object]

    版本 Python 3.8.2 Django 3.0.6 场景 前端页面:使用bootstrap-table展示后台传入数据,选中多行提交修改,弹出bootstrap模态框 模态框内容:根据选中表格行 ...

  6. Kubernetes/K8s CKA认证全套实训视频教程下载

    地址: 链接:https://pan.baidu.com/s/1bwEUZTCVzqM3mGjrlISbcg 提取码:r1kx 目录: 目录: │ 1-1.kubernetes理论教程 - 云原生技术 ...

  7. Python里的目录方法

    Python里的目录_文件.目录相关的方法: mkdir(目录名): 在当前目录下创建新的目录 程序: import os # 创建新的目录-包结构 os.mkdir('新目录-test') getc ...

  8. PHP XML DOM:DOM 是什么?

    PHP XML DOM 内建的 DOM 解析器使在 PHP 中处理 XML 文档成为可能. DOM 是什么? W3C DOM 提供了针对 HTML 和 XML 文档的标准对象集,以及用于访问和操作这些 ...

  9. Python time sleep()方法

    描述 Python time sleep() 函数推迟调用线程的运行,可通过参数secs指秒数,表示进程挂起的时间.高佣联盟 www.cgewang.com 语法 sleep()方法语法: time. ...

  10. C++模板沉思录(上)

    花下猫语: 在我们读者群里,最近出现了比较多关于 C++ 的讨论,还兴起了一股学习 C++ 的风气.樱雨楼小姐姐对 C++ 的模板深有研究,系统地梳理成了一篇近 4 万字的文章!本文是上篇,分享给大家 ...