|
@@ -0,0 +1,289 @@
|
|
|
+package jit.xms.auth.multifactor.service
|
|
|
+
|
|
|
+import gaf3.core.data.PageParam
|
|
|
+import gaf3.core.exception.BusinessError
|
|
|
+import io.jsonwebtoken.ExpiredJwtException
|
|
|
+import io.jsonwebtoken.Jwts
|
|
|
+import jit.xms.auth.api.domain.*
|
|
|
+import jit.xms.auth.api.support.config.MultiFactorConfigure
|
|
|
+import jit.xms.auth.multifactor.AuthConfigure
|
|
|
+import jit.xms.auth.multifactor.domain.AuthForm
|
|
|
+import jit.xms.auth.multifactor.domain.LoginForm
|
|
|
+import jit.xms.auth.util.support.proxy.AuthGatewayService
|
|
|
+import jit.xms.core.services.app.infos.entity.XmsAppInfo
|
|
|
+import jit.xms.core.services.bff.service.BffAppAcctService
|
|
|
+import jit.xms.core.services.user.accts.service.UserAcctService
|
|
|
+import jit.xms.core.services.user.creds.entity.XmsUserCred
|
|
|
+import jit.xms.core.services.user.creds.service.UserCredService
|
|
|
+import jit.xms.core.services.user.infos.service.UserInfoService
|
|
|
+import jit.xms.core.util.PasswordUtil
|
|
|
+import org.bouncycastle.util.encoders.Hex
|
|
|
+import org.slf4j.Logger
|
|
|
+import org.slf4j.LoggerFactory
|
|
|
+import org.springframework.beans.factory.annotation.Autowired
|
|
|
+import org.springframework.beans.factory.annotation.Qualifier
|
|
|
+import org.springframework.stereotype.Service
|
|
|
+import org.springframework.util.Assert.isTrue
|
|
|
+import org.springframework.util.Assert.notEmpty
|
|
|
+import org.springframework.validation.annotation.Validated
|
|
|
+import java.security.Key
|
|
|
+import java.time.Instant
|
|
|
+import java.util.*
|
|
|
+import javax.validation.Valid
|
|
|
+
|
|
|
+
|
|
|
+@Service
|
|
|
+@Validated
|
|
|
+class AuthService(@Qualifier("jwtSigningKey") val jwtSigningKey: Key,
|
|
|
+ @Autowired val provConfig: MultiFactorConfigure, @Autowired val authConfig: AuthConfigure,
|
|
|
+ val acctService: UserAcctService, val credService: UserCredService,
|
|
|
+ val userService: UserInfoService, val appService: BffAppAcctService,
|
|
|
+ val proxyService: AuthGatewayService) {
|
|
|
+
|
|
|
+ fun login(@Valid form: LoginForm): AuthTicket {
|
|
|
+ // 检查账号密码
|
|
|
+ val acct = acctService.findByAcct(form.username!!)
|
|
|
+ // if (acct.unwrapPass != form.password) {
|
|
|
+ if (!PasswordUtil.verify(form.password, acct.unwrapPass)) {
|
|
|
+ throw BusinessError(BusinessError.ERR_BAD_PASSWORD, "账号密码校验错误")
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询用户信息
|
|
|
+ val userInfo: UserInfo = with(userService.findById(acct.userId!!)) {
|
|
|
+ UserInfo(userId = userId, name = name, sex = sex, title = title,
|
|
|
+ sfzh = sfzh, bzkh = bzkh, jrzjh = jrzjh)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询账号应用
|
|
|
+ val apps = appService.findByAcctId(XmsAppInfo(), acct.acctId!!, PageParam.of(0, 100)).data?.map { p ->
|
|
|
+ AppInfo(appId = p.appId!!, appName = p.app?.name ?: "")
|
|
|
+ }?.toTypedArray()
|
|
|
+
|
|
|
+ // 生成Ticket
|
|
|
+ val ticket = createTicket(acct.userId!!, acct.account!!)
|
|
|
+
|
|
|
+ // 解析loginTypes
|
|
|
+ val loginTypes = acct.loginMode?.split(",")?.toTypedArray()
|
|
|
+
|
|
|
+ return AuthTicket(ticket = ticket, loginTypes = loginTypes, userInfo = userInfo, appInfos = apps
|
|
|
+ ?: emptyArray())
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 单一认证
|
|
|
+ */
|
|
|
+ fun auth(@Valid form: AuthForm, ticket: String? = null): AuthToken {
|
|
|
+ // 认证策略检查
|
|
|
+ if (ticket == null && !provConfig.oneToMany) {
|
|
|
+ throw BusinessError(ERR_PROV_NOT_SUPPORT, "不支持一对多比对模式")
|
|
|
+ }
|
|
|
+
|
|
|
+ val authInfo = ticket?.let {
|
|
|
+ val (account: String, userId: String) = parseTicket(ticket)
|
|
|
+ match1(form, userId, account)
|
|
|
+ } ?: matchN(form)
|
|
|
+ val token = createToken(authInfo)
|
|
|
+ return AuthToken(authInfo, token)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组合认证
|
|
|
+ */
|
|
|
+ fun combine(@Valid forms: Array<AuthForm>, ticket: String? = null): AuthToken {
|
|
|
+ notEmpty(forms, "凭证数据不能为空")
|
|
|
+ isTrue(forms.size > 1, "组合认证必须提供两种以上凭证数据")
|
|
|
+
|
|
|
+ // 第一认证
|
|
|
+ val token = auth(forms[0], ticket)
|
|
|
+
|
|
|
+ // 循环认证
|
|
|
+ val allMatched = forms.drop(1).all {
|
|
|
+ try {
|
|
|
+ this.match1(it, token.info.userId, null)
|
|
|
+ true
|
|
|
+ } catch (e: Throwable) {
|
|
|
+ if (log.isDebugEnabled)
|
|
|
+ log.warn(e.message, e)
|
|
|
+ else
|
|
|
+ log.warn(e.message)
|
|
|
+ false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!allMatched) {
|
|
|
+ throw BusinessError(ERR_COMBINE_MATCH, "组合认证失败,请求中存在不匹配的凭证")
|
|
|
+ }
|
|
|
+
|
|
|
+ return token
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 一对一比对认证
|
|
|
+ */
|
|
|
+ fun match1(@Valid form: AuthForm, userId: String, account: String?): AuthInfo {
|
|
|
+ // 查询用户信息
|
|
|
+ val user = userService.findById(userId)
|
|
|
+ val creds = credService.find(with(XmsUserCred()) {
|
|
|
+ this.userId = userId
|
|
|
+ type = form.type
|
|
|
+ this
|
|
|
+ }, PageParam.of(0, 100)).data?.map { p -> p.data }?.toTypedArray() ?: emptyArray<String>()
|
|
|
+ if (creds.isEmpty()) throw BusinessError(ERR_CRED_NOT_REGISTER, "用户尚未注册该类型的凭证数据[${form.type}]")
|
|
|
+
|
|
|
+ // 比对生物特征
|
|
|
+ val prov = provConfig.runCatching { getProvider(form.type!!) }.getOrElse {
|
|
|
+ throw BusinessError(ERR_PROV_NOT_SUPPORT, "获取凭证Provider失败[${form.type}]", it)
|
|
|
+ }
|
|
|
+ // 解密凭证数据
|
|
|
+ val reqData = unwrap(hex2base64(form.data!!, provConfig.isProvEncrypted(form.type!!)), provConfig.isProvEncrypted(form.type!!)).replace("\\", "")
|
|
|
+ val credData = creds.mapNotNull {
|
|
|
+// unwrap(it!!, provConfig.isProvEncryptedDb(form.type!!))
|
|
|
+ unwrap(hex2base64(it!!, provConfig.isProvEncryptedDb(form.type!!)), provConfig.isProvEncryptedDb(form.type!!)).replace("\\", "")
|
|
|
+ }.toTypedArray()
|
|
|
+ val result = runCatching {
|
|
|
+ if (creds.size > 1) {
|
|
|
+ prov.authN(reqData, credData, null)
|
|
|
+ } else {
|
|
|
+ prov.auth(reqData, credData[0], null)
|
|
|
+ }
|
|
|
+ }.getOrElse {
|
|
|
+ log.debug("调用Provider比对接口失败", it)
|
|
|
+ throw BusinessError(ERR_CRED_NOT_MATCH, "SPI接口调用失败", it.message)
|
|
|
+ }
|
|
|
+ if (!result) {
|
|
|
+ throw BusinessError(ERR_CRED_NOT_MATCH, "凭证数据校验失败")
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成认证结果
|
|
|
+ return AuthInfo(userId = user.userId!!, name = user.name!!, account = account, cred = form.type!!)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 一对多比对认证
|
|
|
+ */
|
|
|
+ fun matchN(@Valid form: AuthForm): AuthInfo {
|
|
|
+ // 查询凭证信息
|
|
|
+ var skip = 0
|
|
|
+ var total = 0
|
|
|
+ val limit = 100
|
|
|
+ var cred: XmsUserCred? = null
|
|
|
+ val filter = with(XmsUserCred()) {
|
|
|
+ type = form.type
|
|
|
+ this
|
|
|
+ }
|
|
|
+ do {
|
|
|
+ val rs = credService.find(filter, PageParam.of(skip, limit))
|
|
|
+ if (rs.data?.isEmpty() == true) continue
|
|
|
+ val prov = provConfig.runCatching { getProvider(form.type!!) }.getOrElse {
|
|
|
+ throw BusinessError(ERR_PROV_NOT_SUPPORT, "获取凭证Provider失败[${form.type}]", it)
|
|
|
+ }
|
|
|
+ val reqData = unwrap(hex2base64(form.data!!, provConfig.isProvEncrypted(form.type!!)), provConfig.isProvEncrypted(form.type!!)).replace("\\", "")
|
|
|
+ val opt = rs.data?.parallelStream()?.filter { p ->
|
|
|
+ // 比对生物特征
|
|
|
+ // val prov = provConfig.getProvider(form.type!!)
|
|
|
+ prov.runCatching {
|
|
|
+ val credData = unwrap(hex2base64(p.data!!, provConfig.isProvEncryptedDb(p.type!!)), provConfig.isProvEncryptedDb(p.type!!)).replace("\\", "")
|
|
|
+// log.debug("credData:" + base642hex(credData, provConfig.isProvEncryptedDb(form.type!!)))
|
|
|
+ auth(base642hex(reqData, provConfig.isProvEncrypted(form.type!!)), base642hex(credData, provConfig.isProvEncryptedDb(p.type!!)), null)
|
|
|
+ }.getOrElse {
|
|
|
+ log.debug("调用Provider比对接口失败", it)
|
|
|
+ throw BusinessError(ERR_CRED_NOT_MATCH, "SPI接口调用失败", it.message)
|
|
|
+ }
|
|
|
+
|
|
|
+ }?.findAny()
|
|
|
+ if (opt?.isPresent == true) {
|
|
|
+ cred = opt.get()
|
|
|
+ break
|
|
|
+ }
|
|
|
+ skip += limit
|
|
|
+ total = rs.total
|
|
|
+ } while (skip < total)
|
|
|
+ if (cred == null) {
|
|
|
+ throw BusinessError(ERR_CRED_NOT_FIND, "凭证数据校验失败")
|
|
|
+ }
|
|
|
+
|
|
|
+ val user = cred.userId?.let { userService.findById(it) } ?: throw BusinessError(ERR_USER_NOT_EXIST, "用户信息不存在")
|
|
|
+
|
|
|
+ // 生成认证结果
|
|
|
+ return AuthInfo(userId = user.userId!!, name = user.name!!, cred = form.type!!)
|
|
|
+ }
|
|
|
+
|
|
|
+ fun createTicket(userId: String, account: String): String {
|
|
|
+ return Jwts.builder()
|
|
|
+ .setSubject(account)
|
|
|
+ .setIssuer("xms-ticket")
|
|
|
+ .setExpiration(Date.from(Instant.now().plusSeconds(authConfig.jwtValiditySec)))
|
|
|
+ .claim("userId", userId)
|
|
|
+ .signWith(jwtSigningKey)
|
|
|
+ .compact()
|
|
|
+ }
|
|
|
+
|
|
|
+ fun parseTicket(ticket: String): Array<String> {
|
|
|
+ try {
|
|
|
+ val jws = Jwts.parser().setSigningKey(jwtSigningKey)
|
|
|
+ .parseClaimsJws(ticket)
|
|
|
+
|
|
|
+ val claims = jws.body
|
|
|
+ return arrayOf(claims?.subject!!, (claims["userId"] as String?)!!)
|
|
|
+ } catch (ex: ExpiredJwtException) {
|
|
|
+ throw BusinessError(ERR_TICKET_EXPIRED, "Ticket已过期")
|
|
|
+ } catch (ex: Throwable) {
|
|
|
+ throw BusinessError(ERR_TICKET_INVALID, "Ticket无效")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fun createToken(info: AuthInfo): String {
|
|
|
+ return Jwts.builder()
|
|
|
+ .setSubject(info.userId)
|
|
|
+ .setIssuer(authConfig.jwtIssuer ?: "xms")
|
|
|
+ .setExpiration(Date.from(Instant.now().plusSeconds(authConfig.jwtValiditySec)))
|
|
|
+ .addClaims(mapOf(
|
|
|
+ Pair("name", info.name),
|
|
|
+ Pair("account", info.account),
|
|
|
+ Pair("cred", info.cred)
|
|
|
+ ).filter { p -> p.value != null })
|
|
|
+ .signWith(jwtSigningKey)
|
|
|
+ .compact()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解密数据
|
|
|
+ fun unwrap(enc: String, encrypted: Boolean): String {
|
|
|
+ return if (proxyService.enabled && encrypted) {
|
|
|
+ log.debug("解密凭证数据...")
|
|
|
+ proxyService.runCatching {
|
|
|
+ decrypt(enc)
|
|
|
+ }.getOrElse {
|
|
|
+ log.debug("解密凭证数据失败", it)
|
|
|
+ throw BusinessError(BusinessError.ERR_SERVICE_FAULT, "解密凭证数据失败")
|
|
|
+ }
|
|
|
+ } else enc
|
|
|
+ }
|
|
|
+
|
|
|
+ //将16进制字符串转换为base64编码字符串
|
|
|
+ fun hex2base64(hexString: String, flag: Boolean): String {
|
|
|
+ if (flag) {
|
|
|
+ return Base64.getEncoder().encodeToString(Hex.decodeStrict(hexString, 0, hexString.length))
|
|
|
+ }
|
|
|
+ return hexString
|
|
|
+ }
|
|
|
+
|
|
|
+ //将base64字符串转换为16进制字符串
|
|
|
+ fun base642hex(base64String: String, base64Flag: Boolean): String {
|
|
|
+ if (base64Flag) {
|
|
|
+ val decode = Base64.getDecoder().decode(base64String)
|
|
|
+ return Hex.toHexString(decode, 0, decode.size)
|
|
|
+ }
|
|
|
+ return base64String
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ val log: Logger = LoggerFactory.getLogger(AuthService::class.java)
|
|
|
+ const val ERR_CRED_NOT_MATCH = BusinessError.ERR_BUSINESS - 1
|
|
|
+ const val ERR_CRED_NOT_FIND = BusinessError.ERR_BUSINESS - 2
|
|
|
+ const val ERR_COMBINE_MATCH = BusinessError.ERR_BUSINESS - 3
|
|
|
+ const val ERR_USER_NOT_EXIST = BusinessError.ERR_BUSINESS - 4
|
|
|
+ const val ERR_CRED_NOT_REGISTER = BusinessError.ERR_BUSINESS - 5
|
|
|
+ const val ERR_TICKET_EXPIRED = BusinessError.ERR_BUSINESS - 6
|
|
|
+ const val ERR_TICKET_INVALID = BusinessError.ERR_BUSINESS - 7
|
|
|
+ const val ERR_PROV_NOT_SUPPORT = BusinessError.ERR_BUSINESS - 8
|
|
|
+ }
|
|
|
+}
|