0.9.0 增加JWT和Spring Security保证安全

This commit is contained in:
2021-04-06 21:53:29 +08:00
parent c1d2359ef3
commit 3ea9b815b8
13 changed files with 287 additions and 14 deletions

View File

@@ -21,7 +21,7 @@ allprojects {
subprojects { subprojects {
ext { ext {
version '0.8.0' version '0.9.0'
spring_version = "2.3.0.RELEASE" spring_version = "2.3.0.RELEASE"
} }

View File

@@ -1,13 +1,22 @@
buildscript {
dependencies {
classpath("org.jetbrains.kotlin:kotlin-allopen:$kotlin_version")
}
}
apply plugin: 'kotlin-spring'
dependencies { dependencies {
compile project(":src:gaea") compile project(":src:gaea")
compile("org.springframework.boot:spring-boot-starter-web:$spring_version") compile("org.springframework.boot:spring-boot-starter-web:$spring_version")
compile("org.springframework.boot:spring-boot-starter-aop:$spring_version") compile("org.springframework.boot:spring-boot-starter-aop:$spring_version")
compile("org.springframework.boot:spring-boot-starter-mail:$spring_version") compile("org.springframework.boot:spring-boot-starter-mail:$spring_version")
compile("org.springframework.boot:spring-boot-starter-security:$spring_version")
compile group: 'net.sf.dozer', name: 'dozer', version: '5.5.1' compile group: 'net.sf.dozer', name: 'dozer', version: '5.5.1'
compile group: 'org.apache.poi', name: 'poi', version: '4.1.2' compile group: 'org.apache.poi', name: 'poi', version: '4.1.2'
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6' compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
compile group: 'com.google.guava', name: 'guava', version: '30.1.1-jre' compile group: 'com.google.guava', name: 'guava', version: '30.1.1-jre'
compile group: 'com.auth0', name: 'java-jwt', version: '3.14.0'
} }
publishing { publishing {

View File

@@ -1,8 +1,10 @@
package com.synebula.gaea.app package com.synebula.gaea.app
import com.synebula.gaea.app.component.HttpMessage import com.google.gson.Gson
import com.synebula.gaea.app.struct.HttpMessage
import com.synebula.gaea.data.message.Status import com.synebula.gaea.data.message.Status
import com.synebula.gaea.log.ILogger import com.synebula.gaea.log.ILogger
import org.springframework.security.core.context.SecurityContextHolder
interface IApplication { interface IApplication {
@@ -47,4 +49,23 @@ interface IApplication {
} }
return msg return msg
} }
/**
* 获取用户信息
* @param clazz 用户信息结构类
*/
fun <T> sessionUser(clazz: Class<T>): T? {
try {
val authentication = SecurityContextHolder.getContext().authentication.principal.toString()
try {
val gson = Gson()
return gson.fromJson<T>(authentication, clazz)
} catch (ex: Exception) {
logger?.error(this, ex, "[$name]解析用户信息异常!用户信息:$authentication: ${ex.message}")
}
} catch (ex: Exception) {
logger?.error(this, ex, "[$name]获取用户信息异常!${ex.message}")
}
return null
}
} }

View File

@@ -1,7 +1,7 @@
package com.synebula.gaea.app.cmd package com.synebula.gaea.app.cmd
import com.synebula.gaea.app.IApplication import com.synebula.gaea.app.IApplication
import com.synebula.gaea.app.component.HttpMessage import com.synebula.gaea.app.struct.HttpMessage
import com.synebula.gaea.app.component.aop.annotation.ExceptionMessage import com.synebula.gaea.app.component.aop.annotation.ExceptionMessage
import com.synebula.gaea.data.message.Status import com.synebula.gaea.data.message.Status
import com.synebula.gaea.data.serialization.json.IJsonSerializer import com.synebula.gaea.data.serialization.json.IJsonSerializer

View File

@@ -2,7 +2,7 @@ package com.synebula.gaea.app.component.aop
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.synebula.gaea.app.IApplication import com.synebula.gaea.app.IApplication
import com.synebula.gaea.app.component.HttpMessage import com.synebula.gaea.app.struct.HttpMessage
import com.synebula.gaea.app.component.aop.annotation.ExceptionMessage import com.synebula.gaea.app.component.aop.annotation.ExceptionMessage
import com.synebula.gaea.app.component.aop.annotation.Handler import com.synebula.gaea.app.component.aop.annotation.Handler
import com.synebula.gaea.app.component.aop.annotation.ModuleName import com.synebula.gaea.app.component.aop.annotation.ModuleName

View File

@@ -1,5 +1,6 @@
package com.synebula.gaea.app.component.poi.excel package com.synebula.gaea.app.component.poi
import com.synebula.gaea.app.struct.ExcelData
import org.apache.poi.hssf.usermodel.HSSFCell import org.apache.poi.hssf.usermodel.HSSFCell
import org.apache.poi.hssf.usermodel.HSSFCellStyle import org.apache.poi.hssf.usermodel.HSSFCellStyle
import org.apache.poi.hssf.usermodel.HSSFWorkbook import org.apache.poi.hssf.usermodel.HSSFWorkbook
@@ -9,7 +10,6 @@ import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.VerticalAlignment import org.apache.poi.ss.usermodel.VerticalAlignment
import java.lang.Exception import java.lang.Exception
import java.lang.RuntimeException import java.lang.RuntimeException
import kotlin.math.ceil
/** /**
* Excel操作对象 * Excel操作对象
@@ -35,7 +35,7 @@ object Excel {
val titleFont = wb.createFont() val titleFont = wb.createFont()
titleFont.bold = true titleFont.bold = true
titleStyle.setFont(titleFont) titleStyle.setFont(titleFont)
this.setBorderStyle(titleStyle, BorderStyle.THIN) setBorderStyle(titleStyle, BorderStyle.THIN)
//声明列对象 //声明列对象
// 第三步在sheet中添加表头第0行,注意老版本poi对Excel的行数列数有限制 // 第三步在sheet中添加表头第0行,注意老版本poi对Excel的行数列数有限制
@@ -48,7 +48,7 @@ object Excel {
cell = row.createCell(col) cell = row.createCell(col)
cell.setCellStyle(titleStyle) cell.setCellStyle(titleStyle)
cell.setCellValue(data.columnNames[col]) cell.setCellValue(data.columnNames[col])
this.setColumnWidth(data.columnNames[col], col, sheet) setColumnWidth(data.columnNames[col], col, sheet)
} catch (ex: RuntimeException) { } catch (ex: RuntimeException) {
throw Exception("创建索引${col}列[${data.columnNames[col]}]时出现异常", ex) throw Exception("创建索引${col}列[${data.columnNames[col]}]时出现异常", ex)
} }
@@ -57,7 +57,7 @@ object Excel {
val contentStyle = wb.createCellStyle() val contentStyle = wb.createCellStyle()
contentStyle.alignment = HorizontalAlignment.LEFT// 创建一个修改居左格式 contentStyle.alignment = HorizontalAlignment.LEFT// 创建一个修改居左格式
contentStyle.verticalAlignment = VerticalAlignment.CENTER contentStyle.verticalAlignment = VerticalAlignment.CENTER
this.setBorderStyle(contentStyle, BorderStyle.THIN) setBorderStyle(contentStyle, BorderStyle.THIN)
//创建内容 //创建内容
var col = 0 var col = 0
@@ -70,7 +70,7 @@ object Excel {
cell = row.createCell(col) cell = row.createCell(col)
cell.setCellStyle(contentStyle) cell.setCellStyle(contentStyle)
cell.setCellValue(data.data[i][col]) cell.setCellValue(data.data[i][col])
this.setColumnWidth(data.data[i][col], col, sheet) setColumnWidth(data.data[i][col], col, sheet)
col++ col++
} }

View File

@@ -0,0 +1,131 @@
package com.synebula.gaea.app.component.security
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.google.gson.Gson
import com.synebula.gaea.app.struct.exception.TokenCloseExpireException
import com.synebula.gaea.log.ILogger
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.util.*
@Component
class TokenManager {
@Autowired
private lateinit var logger: ILogger
@Autowired
private lateinit var gson: Gson
@Value("\${jwt.secret:}")
val secret: String = ""
/**
* 短期有效期, 默认一天。单位分钟
*/
@Value("\${jwt.expire.normal:${24 * 60}}")
private val normalExpire = ""
/**
* 长期有效期, 默认一年。单位分钟
*/
@Value("\${jwt.expire.remember:${365 * 24 * 60}}")
private val rememberExpire = ""
/**
* 放到header的名称
*/
val header = "token"
/**
* 业务载荷名称
*/
val payload = "user"
/**
* 生成签名,5min后过期
*
* @param user 用户
* @return 加密的token
*/
fun sign(user: Any, remember: Boolean = false): String {
return this.sign(gson.toJson(user), remember)
}
/**
* 生成签名,5min后过期
*
* @param userJson 用户 Json
* @return 加密的token
*/
fun sign(userJson: String, remember: Boolean = false): String {
val milli = this.expireMilliseconds(if (remember) rememberExpire.toLong() else normalExpire.toLong())
val date = Date(System.currentTimeMillis() + milli)
val algorithm: Algorithm = Algorithm.HMAC256(secret)
// 附带username信息
return JWT.create()
.withClaim(this.payload, userJson)
.withIssuedAt(Date())
.withExpiresAt(date)
.sign(algorithm)
}
/**
* 校验token是否正确
*
* @param token 密钥
* @return 是否正确
*/
fun <T> verify(token: String, clazz: Class<T>): T? {
try {
val now = Date()
val algorithm = Algorithm.HMAC256(secret)
val jwt = JWT.decode(token)
val remain = jwt.expiresAt.time - now.time //剩余的时间
val total = jwt.expiresAt.time - jwt.issuedAt.time //总时间
if (remain > 0 && 1.0 * remain / total <= 0.3) //存活时间少于总时间的1/3重新下发
throw TokenCloseExpireException("", JWT.decode(token).getClaim("user").asString())
val result = JWT.require(algorithm).build().verify(token)
val json = result.getClaim(this.payload).asString()
return gson.fromJson(json, clazz)
} catch (ex: Exception) {
this.logger.debug(this, ex, "解析token出错")
throw ex
}
}
/**
* 校验token是否正确
*
* @param token 密钥
* @return 是否正确
*/
fun verify(token: String): String {
try {
val now = Date()
val algorithm = Algorithm.HMAC256(secret)
val jwt = JWT.decode(token)
val remain = jwt.expiresAt.time - now.time //剩余的时间
val total = jwt.expiresAt.time - jwt.issuedAt.time //总时间
if (remain > 0 && 1.0 * remain / total <= 0.3) //存活时间少于总时间的1/3重新下发
throw TokenCloseExpireException("", JWT.decode(token).getClaim("user").asString())
val result = JWT.require(algorithm).build().verify(token)
return result.getClaim(payload).asString()
} catch (ex: Exception) {
this.logger.debug(this, ex, "解析token出错")
throw ex
}
}
/**
* 获取超时毫秒
* @param minutes 分钟数
*/
private fun expireMilliseconds(minutes: Long): Long {
return minutes * 60 * 1000
}
}

View File

@@ -0,0 +1,43 @@
package com.synebula.gaea.app.component.security
import com.synebula.gaea.app.struct.HttpMessage
import com.synebula.gaea.app.struct.exception.TokenCloseExpireException
import com.synebula.gaea.data.message.Status
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import java.io.IOException
import javax.servlet.FilterChain
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
/**
* 登录成功后 走此类进行鉴权操作
*/
class WebAuthorization(authenticationManager: AuthenticationManager, var tokenManager: TokenManager) :
BasicAuthenticationFilter(authenticationManager) {
/**
* 在过滤之前和之后执行的事件
*/
@Throws(IOException::class, ServletException::class)
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
val token = request.getHeader(tokenManager.header)
try {
val user = tokenManager.verify(token)
val authentication =
UsernamePasswordAuthenticationToken(user, null, null)
SecurityContextHolder.getContext().authentication = authentication
super.doFilterInternal(request, response, chain)
} catch (ex: TokenCloseExpireException) {
response.status = Status.Success
response.characterEncoding = "utf-8"
response.contentType = "text/javascript;charset=utf-8"
response.writer.print(HttpMessage(Status.Reauthorize, tokenManager.sign(ex.payload), "重新下发认证消息"))
response.flushBuffer()
}
}
}

View File

@@ -0,0 +1,64 @@
package com.synebula.gaea.app.component.security
import com.synebula.gaea.app.struct.HttpMessage
import com.synebula.gaea.data.message.Status
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.builders.WebSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.stereotype.Component
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
@Component
@EnableWebSecurity
class WebSecurity : WebSecurityConfigurerAdapter() {
@Autowired
lateinit var tokenManager: TokenManager
/**
* 安全配置
*/
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
// 跨域共享
http.cors()
.and().csrf().disable() // 跨域伪造请求限制无效
.authorizeRequests()
.anyRequest().authenticated()// 资源任何人都可访问
.and()
.addFilter(WebAuthorization(authenticationManager(), tokenManager))// 添加JWT鉴权拦截器
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置Session的创建策略为Spring Security永不创建HttpSession 不使用HttpSession来获取SecurityContext
.and()
.exceptionHandling()
.authenticationEntryPoint { _, response, _ ->
response.status = Status.Success
response.characterEncoding = "utf-8"
response.contentType = "text/javascript;charset=utf-8"
response.writer.print(HttpMessage(Status.Unauthorized, "用户未登录,请重新登录后尝试!"))
}
}
@Throws(Exception::class)
override fun configure(web: WebSecurity) {
web.ignoring().antMatchers("/sign/**")
}
/**
* 跨域配置
* @return 基于URL的跨域配置信息
*/
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val source = UrlBasedCorsConfigurationSource()
// 注册跨域配置
source.registerCorsConfiguration("/**", CorsConfiguration().applyPermitDefaultValues())
return source
}
}

View File

@@ -1,7 +1,7 @@
package com.synebula.gaea.app.query package com.synebula.gaea.app.query
import com.synebula.gaea.app.IApplication import com.synebula.gaea.app.IApplication
import com.synebula.gaea.app.component.HttpMessage import com.synebula.gaea.app.struct.HttpMessage
import com.synebula.gaea.app.component.aop.annotation.ExceptionMessage import com.synebula.gaea.app.component.aop.annotation.ExceptionMessage
import com.synebula.gaea.data.message.Status import com.synebula.gaea.data.message.Status
import com.synebula.gaea.query.IQuery import com.synebula.gaea.query.IQuery

View File

@@ -1,4 +1,4 @@
package com.synebula.gaea.app.component.poi.excel package com.synebula.gaea.app.struct
class ExcelData(var title: String = "", class ExcelData(var title: String = "",
var columnNames: List<String> = listOf(), var columnNames: List<String> = listOf(),

View File

@@ -1,4 +1,4 @@
package com.synebula.gaea.app.component package com.synebula.gaea.app.struct
import com.google.gson.Gson import com.google.gson.Gson
import com.synebula.gaea.data.message.DataMessage import com.synebula.gaea.data.message.DataMessage

View File

@@ -0,0 +1,5 @@
package com.synebula.gaea.app.struct.exception
import com.auth0.jwt.exceptions.TokenExpiredException
class TokenCloseExpireException(msg: String, var payload: String) : TokenExpiredException(msg)