|
@@ -0,0 +1,184 @@
|
|
|
+package gaf3.core.gateway.filter.factory
|
|
|
+
|
|
|
+import io.jsonwebtoken.ExpiredJwtException
|
|
|
+import io.jsonwebtoken.Jwts
|
|
|
+import io.jsonwebtoken.MalformedJwtException
|
|
|
+import io.jsonwebtoken.SignatureAlgorithm
|
|
|
+import io.jsonwebtoken.impl.crypto.MacSigner
|
|
|
+import io.jsonwebtoken.security.Keys
|
|
|
+import org.slf4j.LoggerFactory
|
|
|
+import org.springframework.beans.factory.annotation.Value
|
|
|
+import org.springframework.cloud.gateway.filter.GatewayFilter
|
|
|
+import org.springframework.cloud.gateway.filter.OrderedGatewayFilter
|
|
|
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
|
|
|
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setResponseStatus
|
|
|
+import org.springframework.http.HttpHeaders
|
|
|
+import org.springframework.http.HttpStatus
|
|
|
+import org.springframework.http.MediaType
|
|
|
+import org.springframework.util.StringUtils
|
|
|
+import java.io.UnsupportedEncodingException
|
|
|
+import java.net.URLEncoder
|
|
|
+import java.util.*
|
|
|
+
|
|
|
+/**
|
|
|
+ * 用户认证Token过滤器,通过Authorization或Cookie读取认证Jwt
|
|
|
+ * @author dyg
|
|
|
+ */
|
|
|
+class JwtParserGatewayFilterFactory : AbstractGatewayFilterFactory<JwtParserGatewayFilterFactory.Config>(Config::class.java) {
|
|
|
+
|
|
|
+ @Value("\${jwt.secret}")
|
|
|
+ private val secret: String? = null
|
|
|
+
|
|
|
+ override fun shortcutFieldOrder(): List<String> {
|
|
|
+ return arrayListOf(ISSUER_KEY, IGNORE_KEY, REDIRECT_KEY, ORDER_KEY)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun apply(config: Config): GatewayFilter {
|
|
|
+ return OrderedGatewayFilter( label@ { exchange, chain ->
|
|
|
+ // 检查是否忽略uri
|
|
|
+ val uri = exchange.request.uri
|
|
|
+ if (config.ignore != null && uri.path.matches(config.ignore!!.toRegex())) {
|
|
|
+ return@label chain.filter(exchange.mutate().build())
|
|
|
+ }
|
|
|
+ val url = exchange.request.uri.toString()
|
|
|
+ val method = exchange.request.methodValue
|
|
|
+
|
|
|
+ val accepts : List<MediaType>? = exchange.request.headers.accept
|
|
|
+ var isHtml = false
|
|
|
+ if (accepts!!.isNotEmpty()) {
|
|
|
+ val accept = exchange.request.headers.accept[0]
|
|
|
+ isHtml = accept.includes(MediaType.TEXT_HTML)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 优先读取Head中的Authorization
|
|
|
+ val values = exchange.request.headers[HEADER_AUTH]
|
|
|
+ var token: String? = if (values != null && values.size > 0) values[0] else null
|
|
|
+ if (token != null && token.startsWith("Bearer ")) {
|
|
|
+ token = token.substring(7)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取cookie中的token
|
|
|
+ if (token == null) {
|
|
|
+ val cookie = exchange.request.cookies.getFirst(COOKIE_TOKEN)
|
|
|
+ token = cookie?.value
|
|
|
+ }
|
|
|
+
|
|
|
+ if (token == null || StringUtils.isEmpty(token) || "null".equals(token, ignoreCase = true)
|
|
|
+ || "undefined".equals(token, ignoreCase = true)) {
|
|
|
+ log.debug("Jwt not found [{} {}]", method, url)
|
|
|
+ token = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (token != null) {
|
|
|
+ try {
|
|
|
+ val key = secret!!.toByteArray().copyOf(32)
|
|
|
+ val parser = Jwts.parser().setSigningKey(key)
|
|
|
+ if(config.issuer != null) {
|
|
|
+ parser.requireIssuer(config.issuer)
|
|
|
+ }
|
|
|
+ val jws = parser.parseClaimsJws(token)
|
|
|
+
|
|
|
+ val claims = jws.body
|
|
|
+ exchange.attributes[JWT_CLAIMS_ATTRIBUTE] = claims
|
|
|
+ log.debug("Jwt claims: {}", claims)
|
|
|
+ val issuer : String? = claims.issuer
|
|
|
+ val subject : String? = claims.subject
|
|
|
+ val tokens = subject!!.split("@".toRegex(), 2).toTypedArray()
|
|
|
+ // val userid = tokens[0]
|
|
|
+ val tenant = if (tokens.size > 1) tokens[1] else null
|
|
|
+
|
|
|
+ val request = exchange.request.mutate().headers { httpHeaders ->
|
|
|
+ httpHeaders.set(HEADER_PLATFORM, issuer)
|
|
|
+ httpHeaders.set(HEADER_USERID, subject)
|
|
|
+ if (tenant != null)
|
|
|
+ httpHeaders.set(HEADER_TENANT, tenant)
|
|
|
+ if (claims.get("role", String::class.java) != null)
|
|
|
+ httpHeaders.set(HEADER_ROLE, claims.get("role", String::class.java))
|
|
|
+ val tags = claims.get("tags", ArrayList::class.java)
|
|
|
+ if (tags != null) {
|
|
|
+ httpHeaders.set(HEADER_TAGS, tags.joinToString(","))
|
|
|
+ }
|
|
|
+ }.build()
|
|
|
+ return@label chain.filter(exchange.mutate().request(request).build())
|
|
|
+ } catch (ex: ExpiredJwtException) {
|
|
|
+ log.warn("Jwt token expired [{} {}]", method, url)
|
|
|
+ } catch (ex: MalformedJwtException) {
|
|
|
+ log.warn("Jwt token is invalid: MalformedJwtException [{} {}]", method, url)
|
|
|
+ if (log.isDebugEnabled) {
|
|
|
+ log.debug("token is: {}", token)
|
|
|
+ ex.printStackTrace()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if (config.redirect != null && isHtml) {
|
|
|
+ // 重定向到授权地址
|
|
|
+ var redirect_uri = config.redirect
|
|
|
+ redirect_uri += when ("?" in redirect_uri!!) {
|
|
|
+ true -> "&"
|
|
|
+ false -> "?"
|
|
|
+ }
|
|
|
+ redirect_uri += try {
|
|
|
+ println(uri.toString())
|
|
|
+ "redirect_uri=" + URLEncoder.encode(uri.toString(), "UTF-8")
|
|
|
+ } catch (e: UnsupportedEncodingException) {
|
|
|
+ log.warn("URL编码错误", e)
|
|
|
+ "redirect_uri=$uri"
|
|
|
+ }
|
|
|
+
|
|
|
+ setResponseStatus(exchange, HttpStatus.TEMPORARY_REDIRECT)
|
|
|
+ val response = exchange.response
|
|
|
+ response.headers.set(HttpHeaders.LOCATION, redirect_uri)
|
|
|
+ } else {
|
|
|
+ setResponseStatus(exchange, HttpStatus.UNAUTHORIZED)
|
|
|
+ }
|
|
|
+
|
|
|
+ exchange.response.setComplete()
|
|
|
+ }, config.order)
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // @Validated
|
|
|
+ class Config {
|
|
|
+ // @NotEmpty
|
|
|
+ var ignore: String? = null
|
|
|
+ // @NotEmpty
|
|
|
+ var redirect: String? = null
|
|
|
+ var order = -1
|
|
|
+ var issuer: String? = null
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+
|
|
|
+ internal val log = LoggerFactory.getLogger(JwtParserGatewayFilterFactory::class.java)
|
|
|
+
|
|
|
+ const val HEADER_AUTH = "Authorization"
|
|
|
+ const val HEADER_TENANT = "X-Tenant"
|
|
|
+ const val HEADER_USERID = "X-Userid"
|
|
|
+ const val HEADER_ROLE = "X-Role"
|
|
|
+ const val HEADER_TAGS = "X-Tags"
|
|
|
+ const val HEADER_PLATFORM = "X-Platform"
|
|
|
+ const val JWT_CLAIMS_ATTRIBUTE = "NafGateway.jwtClaims"
|
|
|
+ const val COOKIE_TOKEN = "token"
|
|
|
+
|
|
|
+ const val IGNORE_KEY = "ignore"
|
|
|
+ const val REDIRECT_KEY = "redirect"
|
|
|
+ const val ORDER_KEY = "order"
|
|
|
+ const val ISSUER_KEY = "issuer"
|
|
|
+
|
|
|
+ fun doInit() {
|
|
|
+ @Suppress("SpellCheckingInspection")
|
|
|
+ val secret = "12345678"
|
|
|
+ val key = secret.toByteArray().copyOf(32)
|
|
|
+ MacSigner(SignatureAlgorithm.HS256, key) // 这个操作比较耗时
|
|
|
+
|
|
|
+ val cal = Calendar.getInstance()
|
|
|
+ cal.add(Calendar.HOUR_OF_DAY, 1)
|
|
|
+ val signed = Jwts.builder().setIssuer("master").setSubject("platform").setExpiration(cal.time)
|
|
|
+ .claim("userid", "admin").claim("name", "管理员").signWith(Keys.hmacShaKeyFor(key)).compact()
|
|
|
+ Jwts.parser().setSigningKey(key).parseClaimsJws(signed)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|