0.9.0 增加JWT和Spring Security保证安全
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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++
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user