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

posted @ 2020-08-19 08:15  ITDragon龙  阅读(1449)  评论(3编辑  收藏