diff --git a/build.gradle b/build.gradle index 2076d88..ed8f93f 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ allprojects { subprojects { ext { - version '0.8.0' + version '0.9.0' spring_version = "2.3.0.RELEASE" } diff --git a/src/gaea.app/build.gradle b/src/gaea.app/build.gradle index d748678..6c6bae8 100644 --- a/src/gaea.app/build.gradle +++ b/src/gaea.app/build.gradle @@ -1,13 +1,22 @@ +buildscript { + dependencies { + classpath("org.jetbrains.kotlin:kotlin-allopen:$kotlin_version") + } +} + +apply plugin: 'kotlin-spring' + dependencies { compile project(":src:gaea") 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-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: '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.guava', name: 'guava', version: '30.1.1-jre' - + compile group: 'com.auth0', name: 'java-jwt', version: '3.14.0' } publishing { diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/IApplication.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/IApplication.kt index b5d3251..56bf9fa 100644 --- a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/IApplication.kt +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/IApplication.kt @@ -1,8 +1,10 @@ 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.log.ILogger +import org.springframework.security.core.context.SecurityContextHolder interface IApplication { @@ -47,4 +49,23 @@ interface IApplication { } return msg } + + /** + * 获取用户信息 + * @param clazz 用户信息结构类 + */ + fun sessionUser(clazz: Class): T? { + try { + val authentication = SecurityContextHolder.getContext().authentication.principal.toString() + try { + val gson = Gson() + return gson.fromJson(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 + } } \ No newline at end of file diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/cmd/ICommandApp.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/cmd/ICommandApp.kt index 8022e00..b17a444 100644 --- a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/cmd/ICommandApp.kt +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/cmd/ICommandApp.kt @@ -1,7 +1,7 @@ package com.synebula.gaea.app.cmd 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.data.message.Status import com.synebula.gaea.data.serialization.json.IJsonSerializer diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/aop/AppAspect.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/aop/AppAspect.kt index 45a02cd..5876a28 100644 --- a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/aop/AppAspect.kt +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/aop/AppAspect.kt @@ -2,7 +2,7 @@ package com.synebula.gaea.app.component.aop import com.fasterxml.jackson.databind.ObjectMapper 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.Handler import com.synebula.gaea.app.component.aop.annotation.ModuleName diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/poi/excel/Excel.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/poi/Excel.kt similarity index 91% rename from src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/poi/excel/Excel.kt rename to src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/poi/Excel.kt index 3d0d179..76ab78d 100644 --- a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/poi/excel/Excel.kt +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/poi/Excel.kt @@ -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.HSSFCellStyle 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 java.lang.Exception import java.lang.RuntimeException -import kotlin.math.ceil /** * Excel操作对象 @@ -35,7 +35,7 @@ object Excel { val titleFont = wb.createFont() titleFont.bold = true titleStyle.setFont(titleFont) - this.setBorderStyle(titleStyle, BorderStyle.THIN) + setBorderStyle(titleStyle, BorderStyle.THIN) //声明列对象 // 第三步,在sheet中添加表头第0行,注意老版本poi对Excel的行数列数有限制 @@ -48,7 +48,7 @@ object Excel { cell = row.createCell(col) cell.setCellStyle(titleStyle) cell.setCellValue(data.columnNames[col]) - this.setColumnWidth(data.columnNames[col], col, sheet) + setColumnWidth(data.columnNames[col], col, sheet) } catch (ex: RuntimeException) { throw Exception("创建索引${col}列[${data.columnNames[col]}]时出现异常", ex) } @@ -57,7 +57,7 @@ object Excel { val contentStyle = wb.createCellStyle() contentStyle.alignment = HorizontalAlignment.LEFT// 创建一个修改居左格式 contentStyle.verticalAlignment = VerticalAlignment.CENTER - this.setBorderStyle(contentStyle, BorderStyle.THIN) + setBorderStyle(contentStyle, BorderStyle.THIN) //创建内容 var col = 0 @@ -70,7 +70,7 @@ object Excel { cell = row.createCell(col) cell.setCellStyle(contentStyle) cell.setCellValue(data.data[i][col]) - this.setColumnWidth(data.data[i][col], col, sheet) + setColumnWidth(data.data[i][col], col, sheet) col++ } diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/security/TokenManager.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/security/TokenManager.kt new file mode 100644 index 0000000..a9a4c7d --- /dev/null +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/security/TokenManager.kt @@ -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 verify(token: String, clazz: Class): 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 + } +} + diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/security/WebAuthorization.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/security/WebAuthorization.kt new file mode 100644 index 0000000..1d451d4 --- /dev/null +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/security/WebAuthorization.kt @@ -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() + } + } +} diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/security/WebSecurity.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/security/WebSecurity.kt new file mode 100644 index 0000000..c0a6a43 --- /dev/null +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/security/WebSecurity.kt @@ -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 + } +} diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/query/IQueryApp.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/query/IQueryApp.kt index f3daa0e..1acc70a 100644 --- a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/query/IQueryApp.kt +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/query/IQueryApp.kt @@ -1,7 +1,7 @@ package com.synebula.gaea.app.query 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.data.message.Status import com.synebula.gaea.query.IQuery diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/poi/excel/ExcelData.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/struct/ExcelData.kt similarity index 75% rename from src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/poi/excel/ExcelData.kt rename to src/gaea.app/src/main/kotlin/com/synebula/gaea/app/struct/ExcelData.kt index e00e96f..ba4cf67 100644 --- a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/poi/excel/ExcelData.kt +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/struct/ExcelData.kt @@ -1,4 +1,4 @@ -package com.synebula.gaea.app.component.poi.excel +package com.synebula.gaea.app.struct class ExcelData(var title: String = "", var columnNames: List = listOf(), diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/HttpMessage.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/struct/HttpMessage.kt similarity index 94% rename from src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/HttpMessage.kt rename to src/gaea.app/src/main/kotlin/com/synebula/gaea/app/struct/HttpMessage.kt index 7b24bc6..43e8236 100644 --- a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/component/HttpMessage.kt +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/struct/HttpMessage.kt @@ -1,4 +1,4 @@ -package com.synebula.gaea.app.component +package com.synebula.gaea.app.struct import com.google.gson.Gson import com.synebula.gaea.data.message.DataMessage diff --git a/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/struct/exception/TokenCloseExpireException.kt b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/struct/exception/TokenCloseExpireException.kt new file mode 100644 index 0000000..ffe8a26 --- /dev/null +++ b/src/gaea.app/src/main/kotlin/com/synebula/gaea/app/struct/exception/TokenCloseExpireException.kt @@ -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)