增加OAuth模块
diff --git a/oauth/build.gradle b/oauth/build.gradle
new file mode 100644
index 0000000..17d4252
--- /dev/null
+++ b/oauth/build.gradle
@@ -0,0 +1,90 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot'
+ id 'org.jetbrains.kotlin.jvm'
+ id 'org.jetbrains.kotlin.plugin.jpa' version '1.3.31'
+ id 'org.jetbrains.kotlin.plugin.spring'
+ id "com.palantir.git-version"
+ id 'com.palantir.docker'
+}
+
+apply plugin: 'java'
+apply plugin: 'io.spring.dependency-management'
+
+def version = gitVersion()
+def details = versionDetails()
+
+group = rootProject.group
+version = '1'
+sourceCompatibility = jdkVersion
+def startClass = 'com.supwisdom.oauth.OAuthApplication'
+
+println("Build version: $version")
+
+bootJar {
+ mainClassName = startClass
+ manifest {
+ attributes('oauth-Version': version)
+ }
+}
+
+jar {
+ baseName = 'oauth'
+}
+
+
+dependencies {
+
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.boot:spring-boot-starter-cache'
+ implementation 'org.springframework.boot:spring-boot-autoconfigure'
+ implementation 'org.springframework.security:spring-security-oauth2-jose'
+ implementation 'org.springframework.security:spring-security-oauth2-client'
+ implementation 'org.springframework.security:spring-security-oauth2-jose'
+ implementation 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.1.5.RELEASE'
+ implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ implementation 'org.springframework.session:spring-session-data-redis'
+ implementation 'commons-codec:commons-codec:1.12'
+ implementation 'com.jcabi:jcabi-manifests:1.1'
+ implementation 'org.slf4j:slf4j-parent:1.7.26'
+ implementation 'org.jetbrains.kotlin:kotlin-reflect'
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
+ implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
+ implementation group: 'com.sun.jersey', name: 'jersey-client', version: '1.19'
+ implementation group: 'javax.servlet', name: 'jstl', version: '1.2'
+ implementation group: 'taglibs', name: 'standard', version: '1.1.2'
+ implementation group: 'commons-codec', name: 'commons-codec', version: '1.6'
+ implementation 'org.dom4j:dom4j:2.1.1'
+ implementation 'commons-beanutils:commons-beanutils:1.9.3'
+ implementation 'commons-net:commons-net:3.6'
+ implementation 'org.postgresql:postgresql:42.2.5'
+ implementation 'net.javacrumbs.shedlock:shedlock-spring:2.5.0'
+ implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:2.5.0'
+ implementation 'org.bitbucket.b_c:jose4j:0.6.5'
+
+ implementation project(':payapi-common')
+
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'io.rest-assured:rest-assured:3.3.0'
+ testImplementation 'io.rest-assured:spring-mock-mvc:3.3.0'
+ testImplementation 'org.hamcrest:hamcrest:2.1'
+
+}
+
+compileKotlin {
+ kotlinOptions {
+ freeCompilerArgs = ['-Xjsr305=strict']
+ jvmTarget = jdkVersion
+ }
+}
+
+compileTestKotlin {
+ kotlinOptions {
+ freeCompilerArgs = ['-Xjsr305=strict']
+ jvmTarget = jdkVersion
+ }
+}
+
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/OAuthApplication.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/OAuthApplication.kt
new file mode 100644
index 0000000..674639b
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/OAuthApplication.kt
@@ -0,0 +1,129 @@
+package com.supwisdom.dlpay
+
+import io.lettuce.core.ReadFrom
+import net.javacrumbs.shedlock.core.LockProvider
+import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.SpringApplication
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties
+import org.springframework.boot.builder.SpringApplicationBuilder
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.boot.web.servlet.ServletComponentScan
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer
+import org.springframework.cache.annotation.EnableCaching
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.data.redis.connection.RedisConnectionFactory
+import org.springframework.data.redis.connection.RedisPassword
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration
+import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
+import org.springframework.data.redis.core.RedisTemplate
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
+import org.springframework.data.redis.serializer.StringRedisSerializer
+import org.springframework.http.client.SimpleClientHttpRequestFactory
+import org.springframework.scheduling.annotation.EnableScheduling
+import org.springframework.stereotype.Component
+import org.springframework.web.client.RestTemplate
+import java.net.InetSocketAddress
+import java.net.Proxy
+
+
+@Configuration
+@EnableRedisRepositories
+class AppConfig {
+
+ @Autowired
+ private lateinit var redis: RedisProperties
+
+ @Bean
+ fun redisConnectionFactory(): RedisConnectionFactory {
+ val clientConfig = LettuceClientConfiguration.builder()
+ .readFrom(ReadFrom.SLAVE_PREFERRED)
+ .build()
+ val serverConfig = RedisStandaloneConfiguration(redis.host, redis.port)
+ if (redis.password.isNotEmpty()) {
+ serverConfig.password = RedisPassword.of(redis.password)
+ }
+ serverConfig.database = redis.database
+ return LettuceConnectionFactory(serverConfig, clientConfig)
+ }
+
+ @Bean
+ fun lockProvider(connectionFactory: RedisConnectionFactory): LockProvider {
+ return RedisLockProvider(connectionFactory, "prod")
+ }
+}
+
+@Configuration
+class HttpSessionConfig {
+ @Bean
+ fun sessionRedisTemplate(
+ connectionFactory: RedisConnectionFactory): RedisTemplate<Any, Any> {
+ val template = RedisTemplate<Any, Any>()
+ template.keySerializer = StringRedisSerializer()
+ template.hashKeySerializer = StringRedisSerializer()
+
+ template.setDefaultSerializer(GenericJackson2JsonRedisSerializer())
+ template.setConnectionFactory(connectionFactory)
+ return template
+ }
+}
+
+@Configuration
+class RestTemplateConfig {
+ @Component
+ @ConfigurationProperties("resttemplate.proxy")
+ class RestTemplateProxyProperties {
+ @Value("\${type:}")
+ lateinit var type: String
+ @Value("\${host:}")
+ lateinit var host: String
+ @Value("\${port:0}")
+ var port: Int = 0
+ }
+
+ @Bean
+ fun simpleClientHttpRequestFactory(proxyProperties: RestTemplateProxyProperties):
+ SimpleClientHttpRequestFactory {
+ val factory = SimpleClientHttpRequestFactory()
+ factory.setConnectTimeout(15000)
+ factory.setReadTimeout(5000)
+ if (proxyProperties.type.isNotEmpty()) {
+ val proxyType = when (proxyProperties.type) {
+ "http" -> Proxy.Type.HTTP
+ "socks5" -> Proxy.Type.SOCKS
+ else -> Proxy.Type.DIRECT
+ }
+ if (proxyType != Proxy.Type.DIRECT) {
+ factory.setProxy(Proxy(proxyType,
+ InetSocketAddress(proxyProperties.host, proxyProperties.port)))
+ }
+ }
+ return factory
+ }
+
+ @Bean
+ fun restTemplate(factory: SimpleClientHttpRequestFactory): RestTemplate {
+ return RestTemplate(factory)
+ }
+}
+
+
+@SpringBootApplication
+@EnableScheduling
+@EnableCaching
+@ServletComponentScan
+class OAuthApplication : SpringBootServletInitializer() {
+
+ override fun configure(builder: SpringApplicationBuilder): SpringApplicationBuilder {
+ return builder.sources(OAuthApplication::class.java)
+ }
+}
+
+fun main(args: Array<String>) {
+ SpringApplication.run(OAuthApplication::class.java, * args)
+}
\ No newline at end of file
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/dao/ApiClientDao.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/dao/ApiClientDao.kt
new file mode 100644
index 0000000..cf0bdbe
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/dao/ApiClientDao.kt
@@ -0,0 +1,19 @@
+package com.supwisdom.oauth.dao
+
+import com.supwisdom.oauth.domain.TApiClient
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.Pageable
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+import org.springframework.transaction.annotation.Propagation
+import org.springframework.transaction.annotation.Transactional
+
+@Repository
+interface ApiClientDao : JpaRepository<TApiClient, String> {
+
+ @Transactional(propagation = Propagation.REQUIRED, readOnly = true, rollbackFor = [Exception::class])
+ fun findByAppid(appid: String): TApiClient
+
+ @Transactional(propagation = Propagation.REQUIRED, readOnly = true, rollbackFor = [Exception::class])
+ fun findByAppidContaining(appid: String, pageable: Pageable): Page<TApiClient>
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/dao/OAuthUserDao.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/dao/OAuthUserDao.kt
new file mode 100644
index 0000000..4355fcf
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/dao/OAuthUserDao.kt
@@ -0,0 +1,12 @@
+package com.supwisdom.oauth.dao
+
+import com.supwisdom.oauth.domain.OAuthUser
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface OAuthUserDao : JpaRepository<OAuthUser, String> {
+ fun findByLoginid(loginid: String): OAuthUser?
+}
+
+
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/domain/OAuthUser.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/domain/OAuthUser.kt
new file mode 100644
index 0000000..481c46a
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/domain/OAuthUser.kt
@@ -0,0 +1,177 @@
+package com.supwisdom.oauth.domain
+
+import org.hibernate.annotations.GenericGenerator
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.userdetails.UserDetails
+
+import javax.persistence.*
+import javax.validation.constraints.NotNull
+
+@Entity
+@Table(name = "TB_OAUTH_USER", indexes = [Index(name = "oauth_user_loginid_idx", columnList = "loginid", unique = true)])
+class OAuthUser : UserDetails {
+
+ override fun getAuthorities(): Collection<GrantedAuthority>? {
+ return this.auths
+ }
+
+ override fun isEnabled(): Boolean {
+ return "normal" != this.status
+ }
+
+ override fun getUsername(): String {
+ return this.loginid
+ }
+
+ override fun isCredentialsNonExpired(): Boolean {
+ return true
+ }
+
+ override fun getPassword(): String {
+ return this.loginpwd
+ }
+
+ override fun isAccountNonExpired(): Boolean {
+ return true
+ }
+
+ override fun isAccountNonLocked(): Boolean {
+ return "lock" != this.status
+ }
+
+ @Transient
+ var auths: Collection<GrantedAuthority>? = null
+
+ @Id
+ @GenericGenerator(name = "idGenerator", strategy = "uuid")
+ @GeneratedValue(generator = "idGenerator")
+ @Column(name = "uid", nullable = false, length = 32)
+ @NotNull
+ var uid: String = ""
+
+ @Column(name = "loginid", length = 64)
+ @NotNull
+ var loginid: String = ""
+
+ @Column(name = "loginpwd", length = 64)
+ @NotNull
+ var loginpwd: String = ""
+
+ @Column(name = "status", length = 10)
+ @NotNull
+ var status: String? = null
+
+ /**
+ * 最后登录时间
+ * */
+ @Column(name = "lastlogin", length = 16)
+ var lastlogin: String? = null
+
+ /**
+ * 最后登录手机类型
+ * */
+ @Column(name = "lastloginplatform", length = 100)
+ var lastloginplatform: String? = null
+
+ /**
+ * 支付密码
+ * */
+ @Column(name = "paypwd", length = 64)
+ var paypwd: String? = null
+
+ /**
+ * 登录密码错误次数
+ * */
+ @Column(name = "loginpwderror", length = 4)
+ var loginpwderror: Int = 0
+
+ /**
+ * 登录密码错误时间
+ * */
+ @Column(name = "loginpwderrortime", length = 16)
+ var loginpwderrortime: Long? = 0
+
+ /**
+ * 支付密码错误次数
+ * */
+ @Column(name = "paypwderror", length = 4)
+ var paypwderror: Int = 0
+
+ /**
+ * 支付密码错误时间
+ * */
+ @Column(name = "paypwderrortime", length = 16)
+ var paypwderrortime: Long? = 0
+
+ /**
+ * 关联tb_person
+ * */
+ @Column(name = "userid", length = 32)
+ var userid: String? = null
+
+ /**
+ * jti
+ * */
+ @Column(name = "jti", length = 64)
+ var jti: String? = null
+
+ fun checkLoginpwdtime():Int{
+ if (this.loginpwderror >= 5 && (System.currentTimeMillis() - this.loginpwderrortime!!) < 1000 * 60 * 30) {
+ return -1
+ } else if (this.loginpwderror >= 5 && (System.currentTimeMillis() - this.loginpwderrortime!!) > 1000 * 60 * 30) {
+ //更新时间
+ this.loginpwderror = 0
+ this.loginpwderrortime = null
+ return 1
+ }
+ return 0
+ }
+
+ fun updateLoginpwderror(ok: Boolean): Boolean {
+ return if (ok) {
+ if (this.loginpwderror > 0) {
+ this.loginpwderror = 0
+ this.loginpwderrortime = null
+ true
+ } else {
+ false
+ }
+ } else {
+ if (this.loginpwderror == 0) {
+ this.loginpwderrortime = System.currentTimeMillis()
+ }
+ this.loginpwderror += 1
+ true
+ }
+ }
+
+ fun checkPaypwdtime():Int{
+ if (this.paypwderror >= 5 && (System.currentTimeMillis() - this.paypwderrortime!!) < 1000 * 60 * 30) {
+ return -1
+ } else if (this.paypwderror >= 5 && (System.currentTimeMillis() - this.paypwderrortime!!) > 1000 * 60 * 30) {
+ //更新时间
+ this.paypwderror = 0
+ this.paypwderrortime = null
+ return 1
+ }
+ return 0
+ }
+
+ fun updatePaypwderror(ok: Boolean): Boolean {
+ return if (ok) {
+ if (this.paypwderror > 0) {
+ this.paypwderror = 0
+ this.paypwderrortime = null
+ true
+ } else {
+ false
+ }
+ } else {
+ if (this.paypwderror == 0) {
+ this.paypwderrortime = System.currentTimeMillis()
+ }
+ this.paypwderror += 1
+ true
+ }
+ }
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/domain/TApiClient.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/domain/TApiClient.kt
new file mode 100644
index 0000000..404e64a
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/domain/TApiClient.kt
@@ -0,0 +1,48 @@
+package com.supwisdom.oauth.domain
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+
+import javax.persistence.*
+import javax.validation.constraints.NotNull
+
+@Entity
+@Table(name = "TB_APICLIENT", indexes = [Index(name = "apiclient_idx", columnList = "appid, tenantid", unique = true)])
+class TApiClient {
+ @Id
+ @SequenceGenerator(name = "apiclient_id", sequenceName = "SEQ_APICLIENT", allocationSize = 1, initialValue = 10)
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "apiclient_id")
+ @Column(name = "id")
+ @NotNull
+ var id: Int? = null
+
+ @Column(name = "appid", length = 20)
+ @NotNull
+ var appid: String? = null
+
+ @Column(name = "secret", length = 64)
+ @NotNull
+ var secret: String? = null
+ set(secret) {
+ field = secret
+ this.bcryptSecret = BCryptPasswordEncoder().encode(secret)
+ }
+
+ @Column(name = "status", length = 10)
+ @NotNull
+ var status: String? = null
+
+ @Column(name = "roles", length = 300)
+ var roles: String? = null
+
+ @Column(name = "BCRYPT_SECRET", length = 64)
+ @NotNull
+ var bcryptSecret: String? = null
+ private set
+
+ @Column(name = "THIRDURL", length = 200)
+ var thirdurl: String? = null
+
+ @Column(name = "tenantid", length = 20)
+ @NotNull
+ var tenantId: String? = null
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/oauth.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/oauth.kt
new file mode 100644
index 0000000..16e27ba
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/oauth.kt
@@ -0,0 +1,140 @@
+package com.supwisdom.oauth
+
+import com.supwisdom.oauth.dao.ApiClientDao
+import com.supwisdom.oauth.util.Constants
+import com.supwisdom.oauth.util.JwtConfig
+import com.supwisdom.oauth.util.PasswordBCryptConfig
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.data.redis.connection.RedisConnectionFactory
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer
+import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
+import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer
+import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer
+import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer
+import org.springframework.security.oauth2.provider.ClientDetails
+import org.springframework.security.oauth2.provider.ClientDetailsService
+import org.springframework.security.oauth2.provider.OAuth2Authentication
+import org.springframework.security.oauth2.provider.client.BaseClientDetails
+import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore
+import org.springframework.web.bind.annotation.RequestHeader
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+import java.security.Principal
+import java.util.*
+import java.security.SecureRandom
+
+@RestController
+class UserInforController {
+ @Autowired
+ lateinit var jwtConfig: JwtConfig
+ @Autowired
+ private lateinit var redisConnectionFactory: RedisConnectionFactory
+
+ @RequestMapping("/userinfor")
+ fun user(@RequestParam("access_token") access_token: String?,
+ @RequestHeader(Constants.HEADER_AUTHORIZATION) auth: String?): ResponseEntity<Any> {
+ if (access_token.isNullOrEmpty() && auth.isNullOrEmpty()) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
+ }
+ var jwt: String
+ if(!auth.isNullOrEmpty()){
+ jwt = auth.substring(jwtConfig.tokenHeader.length)
+ }else{
+ jwt = access_token!!
+ }
+ val obj: OAuth2Authentication? = RedisTokenStore(redisConnectionFactory).readAuthentication(jwt) ?: return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
+ val user = obj!!.userAuthentication.principal as UserDetails
+ if(user.username.isNullOrEmpty()){
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
+ }
+ return ResponseEntity.status(HttpStatus.OK).body("""{"name":"${user.username}"}""")
+ }
+}
+
+class OAuthDetailService : ClientDetailsService {
+ @Autowired
+ private lateinit var apiClientDao: ApiClientDao
+ override fun loadClientByClientId(clientId: String?): ClientDetails {
+ val details = BaseClientDetails()
+ if (clientId.isNullOrEmpty()) {
+ return details
+ }
+ details.clientId = clientId
+ apiClientDao.findByAppid(clientId)?.let {
+ details.setAuthorizedGrantTypes(Arrays.asList("password","authorization_code","refresh_token"))
+ details.setScope(Arrays.asList("read"))
+ details.setResourceIds(Arrays.asList("oauth2-resource"))
+ val authorities = HashSet<GrantedAuthority>()
+ authorities.add(SimpleGrantedAuthority("ROLE_THIRD_ADMIN"))
+ details.authorities = authorities
+ details.setAutoApproveScopes(Arrays.asList("true"))
+ details.clientSecret = it.bcryptSecret
+ details.accessTokenValiditySeconds = 3600
+ details.refreshTokenValiditySeconds=43200
+ if(!it.thirdurl.isNullOrEmpty()){
+ val redir = HashSet<String>()
+ when {
+ it.thirdurl!!.contains(",") -> redir.addAll(it.thirdurl!!.split(","))
+ it.thirdurl!!.contains(";") -> redir.addAll(it.thirdurl!!.split(";"))
+ else -> redir.add(it.thirdurl!!)
+ }
+ details.registeredRedirectUri = redir
+ }
+
+ }
+ return details
+ }
+}
+
+
+@Configuration
+class OAuth2Config {
+
+ @Configuration
+ @EnableAuthorizationServer
+
+ class AuthorizationServerConfigure : AuthorizationServerConfigurerAdapter() {
+
+ @Autowired
+ private lateinit var redisConnectionFactory: RedisConnectionFactory
+
+ @Autowired
+ private lateinit var authenticationManager: AuthenticationManager
+
+ @Autowired
+ lateinit var passwordBCryptConfig: PasswordBCryptConfig
+
+ override fun configure(security: AuthorizationServerSecurityConfigurer?) {
+ security?.allowFormAuthenticationForClients()
+ security?.passwordEncoder(pwdEncoder())
+ }
+ @Bean
+ fun pwdEncoder(): BCryptPasswordEncoder {
+ return if (passwordBCryptConfig.seed.isBlank()) {
+ BCryptPasswordEncoder()
+ } else {
+ BCryptPasswordEncoder(passwordBCryptConfig.length,
+ SecureRandom(passwordBCryptConfig.seed.toByteArray()))
+ }
+ }
+
+ override fun configure(clients: ClientDetailsServiceConfigurer?) {
+ clients?.withClientDetails(OAuthDetailService())
+ }
+
+ override fun configure(endpoints: AuthorizationServerEndpointsConfigurer?) {
+ endpoints?.tokenStore(RedisTokenStore(redisConnectionFactory))
+ ?.authenticationManager(authenticationManager)
+ }
+ }
+}
\ No newline at end of file
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/security.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/security.kt
new file mode 100644
index 0000000..f70bb02
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/security.kt
@@ -0,0 +1,167 @@
+package com.supwisdom.dlpay
+
+import com.supwisdom.oauth.domain.OAuthUser
+import com.supwisdom.oauth.service.OAuthUserService
+import com.supwisdom.oauth.util.*
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authentication.BadCredentialsException
+import org.springframework.security.authentication.LockedException
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+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.security.core.Authentication
+import org.springframework.security.core.AuthenticationException
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
+import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher
+import org.springframework.stereotype.Component
+import java.io.IOException
+import java.security.SecureRandom
+import javax.servlet.ServletException
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+import javax.sql.DataSource
+
+@Component("authLoginSuccessHandler")
+class AuthLoginSuccessHandler : SimpleUrlAuthenticationSuccessHandler() {
+ @Autowired
+ lateinit var userService: OAuthUserService
+
+ @Override
+ override fun onAuthenticationSuccess(request: HttpServletRequest, response: HttpServletResponse, authentication: Authentication) {
+ val temp = authentication.principal as OAuthUser
+ val user = userService.getByUid(temp.uid)
+ if (user != null) {
+ if (user.loginpwderror > 0) {
+ user.loginpwderror = 0
+ user.loginpwderrortime = null
+ }
+ user.lastlogin = DateUtil.getNow()
+ userService.saveUser(user)
+ super.onAuthenticationSuccess(request, response, authentication)
+ } else {
+ throw UserLoginFailException("登录错误")
+ }
+ }
+}
+@Component("authLoginFailHandler")
+class AuthLoginFailHandler : SimpleUrlAuthenticationFailureHandler() {
+ @Autowired
+ lateinit var userService: OAuthUserService
+
+ @Throws(IOException::class, ServletException::class)
+ override fun onAuthenticationFailure(request: HttpServletRequest,
+ response: HttpServletResponse, exception: AuthenticationException) {
+ val errmsg = when (exception) {
+ is BadCredentialsException -> "手机号或密码错误"
+ is LockedException -> "账户被锁定"
+ else -> exception.message!!
+ }
+ val temp = request.getParameter("username")
+ if(!temp.isNullOrEmpty()) {
+ userService.findByLoginid(temp)?.let {
+ if (it.loginpwderror == 0) {
+ it.loginpwderror = 0
+ it.loginpwderrortime = System.currentTimeMillis()
+ }
+ it.loginpwderror += 1
+ userService.saveUser(it)
+ }
+ }
+ setDefaultFailureUrl("/login")
+ super.onAuthenticationFailure(request, response, ValidateCodeException(errmsg))
+ }
+}
+
+
+
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+class WebSecurityConfig {
+
+ companion object {
+
+ @Configuration
+ class MvcWebSecurityConfigurationAdapter : WebSecurityConfigurerAdapter() {
+ @Autowired
+ lateinit var dataSource: DataSource
+ @Autowired
+ lateinit var validateCodeSecurityConfig: ValidateCodeSecurityConfig
+ @Autowired
+ lateinit var authLoginFailHandler: AuthLoginFailHandler
+ @Autowired
+ lateinit var authLoginSuccessHandler: AuthLoginSuccessHandler
+ @Autowired
+ lateinit var passwordBCryptConfig: PasswordBCryptConfig
+ @Autowired
+ lateinit var userDetailsService: OAuthUserService
+
+ override fun configure(auth: AuthenticationManagerBuilder) {
+ auth.authenticationProvider(authenticationProvider())
+ }
+
+ @Bean
+ fun authenticationProvider(): DaoAuthenticationProvider {
+ return DaoAuthenticationProvider().apply {
+ setUserDetailsService(userDetailsService)
+ setPasswordEncoder(passwordEncoder())
+ }
+ }
+
+ @Bean
+ fun passwordEncoder(): BCryptPasswordEncoder {
+ return if (passwordBCryptConfig.seed.isBlank()) {
+ BCryptPasswordEncoder()
+ } else {
+ BCryptPasswordEncoder(passwordBCryptConfig.length,
+ SecureRandom(passwordBCryptConfig.seed.toByteArray()))
+ }
+ }
+
+ @Bean
+ fun jdbcTokenImplement(): JdbcTokenRepositoryImpl {
+ return JdbcTokenRepositoryImpl().also {
+ it.dataSource = dataSource
+ }
+ }
+
+ override fun configure(http: HttpSecurity) {
+ // 设置 Web MVC 应用权限
+ http.apply(validateCodeSecurityConfig)
+ .and()
+ .authorizeRequests()
+ .antMatchers("/login", "/login/form", "/userinfor").permitAll()
+ .antMatchers("/static/**").permitAll()
+ .antMatchers("/code/image").permitAll()
+ .antMatchers("/**").hasAnyRole("USER", "ADMIN")
+ .anyRequest().authenticated()
+ .and()
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
+ .and()
+ .formLogin()
+ .loginPage("/login")
+ .loginProcessingUrl("/login/form")
+ .failureHandler(authLoginFailHandler)
+ .successHandler(authLoginSuccessHandler)
+ .defaultSuccessUrl("/", false)
+ .and()
+ .logout()
+ .logoutRequestMatcher(AntPathRequestMatcher("/logout"))
+ .logoutSuccessUrl("/login")
+ .deleteCookies("JSESSIONID")
+ .invalidateHttpSession(true)
+ .and().csrf().ignoringAntMatchers("oauth/**")
+
+ }
+ }
+ }
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/service/OAuthUserService.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/service/OAuthUserService.kt
new file mode 100644
index 0000000..5df80ef
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/service/OAuthUserService.kt
@@ -0,0 +1,12 @@
+package com.supwisdom.oauth.service
+
+import com.supwisdom.oauth.domain.OAuthUser
+import org.springframework.security.core.userdetails.UserDetailsService
+
+interface OAuthUserService:UserDetailsService{
+ fun getByUid(uid: String): OAuthUser?
+
+ fun findByLoginid(loginid: String): OAuthUser?
+
+ fun saveUser(oAuthUser: OAuthUser):OAuthUser
+}
\ No newline at end of file
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/service/impl/OAuthUserServiceImpl.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/service/impl/OAuthUserServiceImpl.kt
new file mode 100644
index 0000000..47364bd
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/service/impl/OAuthUserServiceImpl.kt
@@ -0,0 +1,60 @@
+package com.supwisdom.oauth.service.impl
+
+import com.supwisdom.oauth.dao.OAuthUserDao
+import com.supwisdom.oauth.domain.OAuthUser
+import com.supwisdom.oauth.service.OAuthUserService
+import com.supwisdom.oauth.util.UserLoginFailException
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.authority.AuthorityUtils
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.stereotype.Service
+
+@Service
+class OAuthUserServiceImpl :OAuthUserService {
+ companion object {
+ const val TIME_INTERVAL = 1000 * 6 * 30
+ }
+ @Autowired
+ lateinit var oAuthUserDao: OAuthUserDao
+
+ override fun loadUserByUsername(username: String?): UserDetails {
+ if (username.isNullOrEmpty()) {
+ throw UsernameNotFoundException("用户不存在")
+ }
+ val temp = oAuthUserDao.findByLoginid(username)
+ if (temp != null) {
+ if (temp.loginpwd.isEmpty()) {
+ throw UserLoginFailException("用户注册后未设置登录密码,请找回密码或重新注册")
+ }
+ if (temp.loginpwderror >= 5 && (System.currentTimeMillis() - temp.loginpwderrortime!!) < TIME_INTERVAL) {
+ throw UserLoginFailException("密码错误次数过多,请30分钟后再试")
+ } else if (temp.loginpwderror >= 5 && (System.currentTimeMillis() - temp.loginpwderrortime!!) > TIME_INTERVAL) {
+ //更新时间
+ temp.loginpwderror = 0
+ temp.loginpwderrortime = null
+ oAuthUserDao.save(temp)
+ }
+ val authorities: Collection<GrantedAuthority> = AuthorityUtils.createAuthorityList("ROLE_USER")
+ temp.auths = authorities
+ } else {
+ throw UsernameNotFoundException("用户不存在")
+ }
+ return temp
+ }
+
+ override fun getByUid(uid: String): OAuthUser? {
+ return oAuthUserDao.findById(uid).let {
+ if (it.isPresent) it.get() else null
+ }
+ }
+
+ override fun findByLoginid(loginid: String): OAuthUser? {
+ return oAuthUserDao.findByLoginid(loginid)
+ }
+
+ override fun saveUser(oAuthUser: OAuthUser): OAuthUser {
+ return oAuthUserDao.save(oAuthUser)
+ }
+}
\ No newline at end of file
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/util/Constants.java b/oauth/src/main/kotlin/com/supwisdom/oauth/util/Constants.java
new file mode 100644
index 0000000..7cf989f
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/util/Constants.java
@@ -0,0 +1,15 @@
+package com.supwisdom.oauth.util;
+
+public class Constants {
+ // HTTP HEADER define
+ public static final String HEADER_TETANTID = "X-TENANT-ID";
+
+ // define
+ public static final String JWT_CLAIM_TENANTID = "tenantId";
+ public static final String JWT_CLAIM_UID = "uid";
+ public static final String JWT_CLAIM_AUTHORITIES = "authorities";
+ // 根商户ID
+ public static final Integer ROOT_SHOP_FID = 1;
+
+ public static final String HEADER_AUTHORIZATION = "Authorization";
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/util/DateUtil.java b/oauth/src/main/kotlin/com/supwisdom/oauth/util/DateUtil.java
new file mode 100644
index 0000000..5b355c6
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/util/DateUtil.java
@@ -0,0 +1,361 @@
+package com.supwisdom.oauth.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Timestamp;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+public class DateUtil {
+ private static final Logger logger = LoggerFactory.getLogger(DateUtil.class);
+ public static final String DATE_FMT = "yyyyMMdd";
+ public static final String TIME_FMT = "HHmmss";
+ public static final String DATETIME_FMT = "yyyyMMddHHmmss";
+
+ /**
+ * Description: 返回一个当前时间 @return String 格式:yyyyMMddHHmmss @exception Modify
+ * History:
+ */
+ public static String getNow() {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+ return sdf.format(new Date());
+ }
+
+
+ /**
+ * Description: 根据类型返回一个当前时间 @param partten String @return String 格式:partten
+ */
+ public static String getNow(String partten) {
+ SimpleDateFormat sdf = new SimpleDateFormat(partten);
+ return sdf.format(new Date());
+ }
+ /*
+ *
+ * */
+ public static String getNowInterDay(int intervalday) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+ Date d = new Date();
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(d.getTime());
+ calendar.add(Calendar.DATE, intervalday);
+ return sdf.format(calendar.getTime());
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Description: 得到一个特殊的时间 @param startTime String 格式:yyyyMMddHHmmss @param
+ * interval int 秒 @return String 格式:partten @exception Modify History:
+ */
+ public static String getNewTime(String startTime, int interval) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+ Date d = sdf.parse(startTime);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(d.getTime());
+ calendar.add(Calendar.SECOND, interval);
+ return sdf.format(calendar.getTime());
+ } catch (ParseException e) {
+ return startTime;
+ }
+ }
+
+ /**
+ * Description: 得到一个特殊的时间 @param startTime String 格式:partten @param
+ * interval int 秒 @return String 格式:partten @exception Modify History:
+ */
+ public static String getNewTime(String startTime, int interval, String partten) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat(partten);
+ Date d = sdf.parse(startTime);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(d.getTime());
+ calendar.add(Calendar.SECOND, interval);
+ return sdf.format(calendar.getTime());
+ } catch (ParseException e) {
+ return startTime;
+ }
+ }
+
+ public static String getNewDay(String startDay, int intervalday) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+ Date d = sdf.parse(startDay);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(d.getTime());
+ calendar.add(Calendar.DATE, intervalday);
+ return sdf.format(calendar.getTime());
+ } catch (ParseException e) {
+ return startDay;
+ }
+ }
+
+ /**
+ * 得到两个日期相差的天数 格式 yyyyMMdd @return diffdays = secondDay - firstDay
+ */
+ public static long getIntervalDay(String firstDay, String secondDay) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+ Date f = sdf.parse(firstDay);
+ Date s = sdf.parse(secondDay);
+ long time = s.getTime() - f.getTime();
+ return time / (24 * 60 * 60 * 1000);
+ } catch (ParseException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Description: 比较两个时间字符串的前后关系 @param firstTime String 格式:yyyyMMddHHmmss
+ *
+ * @param secondTime String 格式: yyyyMMddHHmmss @return int |
+ * firstTime=second int=0 | firstTime>secondTime int>0 |
+ * firstTime<secondTime int<0 @exception Modify History:
+ */
+ public static int compareDatetime(String firstTime, String secondTime) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+ Date f = sdf.parse(firstTime);
+ Date s = sdf.parse(secondTime);
+ return f.compareTo(s);
+ } catch (ParseException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Description: 比较两个时间字符串的前后关系 @param firstTime String 格式:pattern
+ *
+ * @param secondTime String 格式: yyyyMMddHHmmss @return int |
+ * firstTime=second int=0 | firstTime>secondTime int>0 |
+ * firstTime<secondTime int<0 @exception Modify History:
+ */
+ public static int compareDatetime(String firstTime, String secondTime, String pattern) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+ Date f = sdf.parse(firstTime);
+ Date s = sdf.parse(secondTime);
+ return f.compareTo(s);
+ } catch (ParseException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Description: 比较两个时间字符串的时间差 @param firstTime String 格式:yyyyMMddHHmmss
+ *
+ * @param secondTime String 格式: yyyyMMddHHmmss @param second int 格式 @return
+ * int | firstTime+seconds=secondTime int=0 | firstTime+seconds>secondTime
+ * int>0 | firstTime+seconds<secondTime int<0 @exception Modify History:
+ */
+ public static int compareDatetime(String firstTime, String secondTime, int seconds) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+ Date f = sdf.parse(firstTime);
+ Date s = sdf.parse(secondTime);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(f.getTime());
+ calendar.add(Calendar.SECOND, seconds);
+ Date temp = calendar.getTime();
+ return temp.compareTo(s);
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Description: 对time重新格式化
+ */
+ public static String reformatDatetime(String time, String fromPattern, String toPattern) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat(fromPattern);
+ Date d = sdf.parse(time);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(d.getTime());
+ sdf = new SimpleDateFormat(toPattern);
+ return sdf.format(calendar.getTime());
+ } catch (Exception e) {
+ e.printStackTrace();
+ return time;
+ }
+ }
+
+ /**
+ * 获得两个字符串日期之间的时间差(单位毫秒) 格式 yyyyMMddHHmmss
+ */
+ public static long getInterval(String startTime, String endTime) {
+ long duration = 0;
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+ duration = sdf.parse(endTime).getTime() - sdf.parse(startTime).getTime();
+ } catch (ParseException e) {
+ logger.error("Hi guys,there is an error when you try to parse the date string");
+ }
+ return duration;
+ }
+
+ /**
+ * 获得两个字符串日期之间的时间差(单位毫秒)
+ */
+ public static long getIntervalTime(String startTime, String endTime, String pattern) {
+ long duration = 0;
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+ duration = sdf.parse(endTime).getTime() - sdf.parse(startTime).getTime();
+ } catch (ParseException e) {
+ logger.error("Hi guys,there is an error when you try to parse the date string");
+ }
+ return duration;
+ }
+
+ /**
+ * 转换成日期格式
+ * 短格式:20140401 -> 2014-04-01
+ * 中格式:201404011200 -> 2014-04-01 12:00
+ * 长格式:20140401123025 -> 2014-04-01 12:30:25
+ **/
+ public static String parseToDateFormat(String str) {
+ switch (str.length()) {
+ case 8:
+ str = str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6, 8);
+ break;
+ case 12:
+ str = str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6, 8) + " " + str.substring(8, 10) + ":" + str.substring(10, 12);
+ break;
+ case 14:
+ str = str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6, 8) + " " + str.substring(8, 10) + ":" + str.substring(10, 12) + ":" + str.substring(12, 14);
+ break;
+ default:
+ break;
+ }
+ return str;
+ }
+
+ /**
+ * 解日期格式
+ * 短格式:2014-04-01 -> 20140401
+ * 中格式:2014-04-01 12:00 -> 201404011200
+ * 长格式:2014-04-01 12:30:25 -> 20140401123025
+ **/
+ public static String unParseToDateFormat(String str) {
+ return str.replaceAll("-", "").replaceAll(" ", "").replaceAll(":", "");
+ }
+
+ /**
+ * 检验时间格式
+ */
+ public static boolean checkDatetimeValid(String datetime, String pattern) {
+ if (null == datetime) return false;
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+ Date d = sdf.parse(datetime);
+ return datetime.trim().equals(sdf.format(d));
+ } catch (Exception e) {
+ }
+ return false;
+ }
+
+ /**
+ * 获取指定日期是星期几 格式 yyyyMMdd
+ * MON|TUE|WED|THU|FRI|SAT|SUN
+ * 1 2 3 4 5 6 7
+ */
+ public static int getWeekday(String datestr) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+ Calendar calendar = Calendar.getInstance();
+ boolean isFirstSunday = (calendar.getFirstDayOfWeek() == Calendar.SUNDAY); //一周第一天是否为星期天
+ Date d = sdf.parse(datestr);
+ calendar.setTimeInMillis(d.getTime());
+ int weekDay = calendar.get(calendar.DAY_OF_WEEK);
+ if (isFirstSunday) {
+ weekDay = weekDay - 1;
+ if (weekDay == 0) {
+ weekDay = 7;
+ }
+ }
+ return weekDay;
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ /**
+ * 获取指定日期
+ */
+ public static Date getSpecifyDate(String datestr, String pattern) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+ Date result = sdf.parse(datestr);
+ return result;
+ } catch (Exception e) {
+ return new Date();
+ }
+ }
+
+ public static Integer getLastDayOfMonth(Integer year, Integer month) {
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.YEAR, year);
+ cal.set(Calendar.MONTH, month - 1);
+ cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DATE));
+ String str = new SimpleDateFormat("yyyyMMdd ").format(cal.getTime()).toString();
+ Integer result = Integer.parseInt(str.substring(0, 4) + str.substring(4, 6) + str.substring(6, 8));
+ return result;
+ }
+
+ private static Date set(Date date, int calendarField, int amount) {
+ Calendar c = Calendar.getInstance();
+ c.setLenient(false);
+ c.setTime(date);
+ c.add(calendarField, amount);
+ return c.getTime();
+ }
+
+
+ public static Date setMinutes(Date date, int amount) {
+ return set(date, Calendar.MINUTE, amount);
+ }
+
+
+ public static long getNowSecond() {
+ Calendar calendar = Calendar.getInstance();
+ return calendar.getTimeInMillis() / 1000;
+ }
+
+
+ public static String getUTCTime(Long timeInMillisSecond) {
+ Calendar time = Calendar.getInstance();
+ SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
+ fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
+ time.setTimeInMillis(timeInMillisSecond);
+ return fmt.format(time.getTime());
+ }
+
+ public static String getUTCTime() {
+ return getUTCTime(System.currentTimeMillis());
+ }
+
+ public static int compareDay(Timestamp d1, Timestamp d2) {
+ Calendar cd1 = Calendar.getInstance();
+ cd1.setTimeInMillis(d1.getTime());
+ Calendar cd2 = Calendar.getInstance();
+ cd2.setTimeInMillis(d2.getTime());
+
+ if (cd1.get(Calendar.YEAR) != cd2.get(Calendar.YEAR)) {
+ return cd1.compareTo(cd2);
+ }
+
+ return Integer.compare(cd1.get(Calendar.DAY_OF_YEAR), cd2.get(Calendar.DAY_OF_YEAR));
+ }
+
+ public static Boolean sameDay(Timestamp d1, Timestamp d2) {
+ return (compareDay(d1, d2) == 0);
+ }
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/util/JwtConfig.java b/oauth/src/main/kotlin/com/supwisdom/oauth/util/JwtConfig.java
new file mode 100644
index 0000000..3bf4921
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/util/JwtConfig.java
@@ -0,0 +1,36 @@
+package com.supwisdom.oauth.util;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class JwtConfig {
+ @Value("${jwt.secret}")
+ private String secret;
+ @Value("${jwt.expiration:3600}")
+ private Long expiration = 3600L;
+ @Value("${jwt.header:Authorization}")
+ private String header = "Authorization";
+ @Value("${jwt.token_header:Bearer }")
+ private String tokenHeader = "Bearer";
+
+ public String getSecret() {
+ return secret;
+ }
+
+ public Long getExpiration() {
+ return expiration;
+ }
+
+ public String getHeader() {
+ return header;
+ }
+
+ public String getTokenHeader() {
+ return tokenHeader;
+ }
+
+ public void setExpiration(Long expiration) {
+ this.expiration = expiration;
+ }
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/util/JwtToken.java b/oauth/src/main/kotlin/com/supwisdom/oauth/util/JwtToken.java
new file mode 100644
index 0000000..8d17542
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/util/JwtToken.java
@@ -0,0 +1,39 @@
+package com.supwisdom.oauth.util;
+
+import org.jose4j.jwt.NumericDate;
+
+public class JwtToken {
+ private String jti;
+ private NumericDate expiration;
+ private String jwtToken;
+
+ public JwtToken(String jti, String jwtToken, NumericDate exp) {
+ this.jti = jti;
+ this.jwtToken = jwtToken;
+ this.expiration = exp;
+ }
+
+ public String getJti() {
+ return jti;
+ }
+
+ public void setJti(String jti) {
+ this.jti = jti;
+ }
+
+ public String getJwtToken() {
+ return jwtToken;
+ }
+
+ public void setJwtToken(String jwtToken) {
+ this.jwtToken = jwtToken;
+ }
+
+ public NumericDate getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(NumericDate expiration) {
+ this.expiration = expiration;
+ }
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/util/JwtTokenUtil.java b/oauth/src/main/kotlin/com/supwisdom/oauth/util/JwtTokenUtil.java
new file mode 100644
index 0000000..9304dcd
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/util/JwtTokenUtil.java
@@ -0,0 +1,94 @@
+package com.supwisdom.oauth.util;
+
+import org.jose4j.jwa.AlgorithmConstraints;
+import org.jose4j.jwk.JsonWebKey;
+import org.jose4j.jws.AlgorithmIdentifiers;
+import org.jose4j.jws.JsonWebSignature;
+import org.jose4j.jwt.JwtClaims;
+import org.jose4j.jwt.MalformedClaimException;
+import org.jose4j.jwt.consumer.InvalidJwtException;
+import org.jose4j.jwt.consumer.JwtConsumer;
+import org.jose4j.jwt.consumer.JwtConsumerBuilder;
+import org.jose4j.lang.JoseException;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class JwtTokenUtil {
+ private JwtConfig jwtConfig;
+
+ public JwtTokenUtil(JwtConfig config) {
+ this.jwtConfig = config;
+ }
+
+ public String getHeader() {
+ return jwtConfig.getHeader();
+ }
+
+ public JwtToken generateToken(Map<String, Object> params) throws JoseException, MalformedClaimException {
+ JwtClaims claims = new JwtClaims();
+ claims.setIssuer(params.get("issuer").toString()); // who creates the token and signs it
+ if (params.get("audience") != null) {
+ claims.setAudience(params.get("audience").toString());
+ }
+ claims.setExpirationTimeMinutesInTheFuture(jwtConfig.getExpiration() / 60); // time when the token will expire (10 minutes from now)
+ claims.setGeneratedJwtId();
+ claims.setIssuedAtToNow(); // when the token was issued/created (now)
+ claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago)
+ if (params.get("subject") != null) {
+ claims.setSubject(params.get("subject").toString()); // the subject/principal is whom the token is about
+ }
+ if (params.get(Constants.JWT_CLAIM_AUTHORITIES) != null) {
+ claims.setClaim(Constants.JWT_CLAIM_AUTHORITIES, params.get(Constants.JWT_CLAIM_AUTHORITIES));
+ }
+ if (params.get(Constants.JWT_CLAIM_UID) != null) {
+ claims.setClaim(Constants.JWT_CLAIM_UID, params.get(Constants.JWT_CLAIM_UID));
+ }
+ if (params.get(Constants.JWT_CLAIM_TENANTID) != null) {
+ claims.setClaim(Constants.JWT_CLAIM_TENANTID, params.get(Constants.JWT_CLAIM_TENANTID));
+ }
+ /*
+ claims.setClaim("email", "mail@example.com"); // additional claims/attributes about the subject can be added
+ List<String> groups = Arrays.asList("group-one", "other-group", "group-three");
+ claims.setStringListClaim("groups", groups); // multi-valued claims work too and will end up as a JSON array
+ */
+
+ Map<String, Object> keySpec = new HashMap<>();
+ keySpec.put("kty", "oct");
+ keySpec.put("k", jwtConfig.getSecret());
+ JsonWebKey key = JsonWebKey.Factory.newJwk(keySpec);
+ JsonWebSignature jws = new JsonWebSignature();
+ jws.setPayload(claims.toJson());
+ jws.setKey(key.getKey());
+ jws.setKeyIdHeaderValue(key.getKeyId());
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
+ return new JwtToken(claims.getJwtId(), jws.getCompactSerialization(), claims.getExpirationTime());
+ }
+
+ public JwtToken generateToken(UserDetails userDetails) throws JoseException, MalformedClaimException {
+ Map<String, Object> claims = new HashMap<>();
+ claims.put("uid", userDetails.getUsername());
+ return generateToken(claims);
+ }
+
+ public Map<String, Object> verifyToken(String token) throws JoseException, InvalidJwtException {
+ Map<String, Object> keySpec = new HashMap<>();
+ keySpec.put("kty", "oct");
+ keySpec.put("k", jwtConfig.getSecret());
+ JsonWebKey key = JsonWebKey.Factory.newJwk(keySpec);
+ JwtConsumer jwtConsumer = new JwtConsumerBuilder()
+ .setRequireExpirationTime() // the JWT must have an expiration time
+ .setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
+ .setVerificationKey(key.getKey()) // verify the signature with the public key
+ .setSkipDefaultAudienceValidation()
+ .setJwsAlgorithmConstraints( // only allow the expected signature algorithm(s) in the given context
+ new AlgorithmConstraints(org.jose4j.jwa.AlgorithmConstraints.ConstraintType.WHITELIST, // which is only RS256 here
+ AlgorithmIdentifiers.HMAC_SHA256))
+ .build(); // create the JwtConsumer instance
+
+ // Validate the JWT and process it to the Claims
+ JwtClaims jwtClaims = jwtConsumer.processToClaims(token);
+ return jwtClaims.getClaimsMap();
+ }
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/util/PasswordBCryptConfig.java b/oauth/src/main/kotlin/com/supwisdom/oauth/util/PasswordBCryptConfig.java
new file mode 100644
index 0000000..55d2c03
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/util/PasswordBCryptConfig.java
@@ -0,0 +1,28 @@
+package com.supwisdom.oauth.util;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class PasswordBCryptConfig {
+ @Value("${auth.password.bcrypt.length:10}")
+ private Integer length;
+ @Value("${auth.password.bcrypt.seed}")
+ private String seed = "";
+
+ public Integer getLength() {
+ return length;
+ }
+
+ public void setLength(Integer length) {
+ this.length = length;
+ }
+
+ public String getSeed() {
+ return seed;
+ }
+
+ public void setSeed(String seed) {
+ this.seed = seed;
+ }
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/util/UserLoginFailException.kt b/oauth/src/main/kotlin/com/supwisdom/oauth/util/UserLoginFailException.kt
new file mode 100644
index 0000000..ca7317a
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/util/UserLoginFailException.kt
@@ -0,0 +1,7 @@
+package com.supwisdom.oauth.util
+
+import org.springframework.security.core.AuthenticationException
+
+class UserLoginFailException(msg: String) : AuthenticationException(msg) {
+ private val serialVersionUID = 1170189980006964105L
+}
\ No newline at end of file
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/util/ValidateCodeException.java b/oauth/src/main/kotlin/com/supwisdom/oauth/util/ValidateCodeException.java
new file mode 100755
index 0000000..c719096
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/util/ValidateCodeException.java
@@ -0,0 +1,23 @@
+/**
+ *
+ */
+package com.supwisdom.oauth.util;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * @author lenovo
+ *
+ */
+public class ValidateCodeException extends AuthenticationException {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1170189980006964105L;
+
+
+ public ValidateCodeException(String msg) {
+ super(msg);
+ }
+}
diff --git a/oauth/src/main/kotlin/com/supwisdom/oauth/util/ValidateCodeSecurityConfig.java b/oauth/src/main/kotlin/com/supwisdom/oauth/util/ValidateCodeSecurityConfig.java
new file mode 100644
index 0000000..9f0118c
--- /dev/null
+++ b/oauth/src/main/kotlin/com/supwisdom/oauth/util/ValidateCodeSecurityConfig.java
@@ -0,0 +1,23 @@
+package com.supwisdom.oauth.util;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.Filter;
+
+@Component("validateCodeSecurityConfig")
+public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {
+
+ @Autowired
+ private Filter validateCodeFilter;
+
+ @Override
+ public void configure(HttpSecurity http) throws Exception {
+ http.addFilterBefore(validateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class);
+ }
+
+}
\ No newline at end of file
diff --git a/oauth/src/main/resources/templates/login.html b/oauth/src/main/resources/templates/login.html
new file mode 100644
index 0000000..ac69dd1
--- /dev/null
+++ b/oauth/src/main/resources/templates/login.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+<head>
+ <title>统一身份认证</title>
+ <meta name="_csrf" th:content="${_csrf.token}"/>
+ <!-- default header name is X-CSRF-TOKEN -->
+ <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
+</head>
+<body>
+<div class="login-wrapper">
+ <div class=" login-body">
+ <div class="layui-card">
+ <div class="layui-card-header">
+ <i class="layui-icon layui-icon-engine"></i> 统一身份认证
+ </div>
+ <div class="layui-card-body layui-form layui-form-pane">
+ <p style="color:red;padding: 0 0 10px 0;" th:if="${session['SPRING_SECURITY_LAST_EXCEPTION']!=null and session['SPRING_SECURITY_LAST_EXCEPTION'].message!=null}" th:text="${session['SPRING_SECURITY_LAST_EXCEPTION'].message}"></p>
+ <form th:action="@{/login/form}" method="post">
+ <div class="layui-form-item">
+ <label class="layui-form-label"><i class="layui-icon layui-icon-username"></i></label>
+ <div class="layui-input-block">
+ <input name="username" type="text" lay-verify="required" placeholder="账号"
+ class="layui-input">
+ <input type="hidden"
+ th:name="${_csrf.parameterName}"
+ th:value="${_csrf.token}"/>
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <label class="layui-form-label"><i class="layui-icon layui-icon-password"></i></label>
+ <div class="layui-input-block">
+ <input name="password" type="password" lay-verify="required" placeholder="密码"
+ class="layui-input">
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <label class="layui-form-label"><i class="layui-icon layui-icon-vercode"></i></label>
+ <div class="layui-input-block">
+ <div class="layui-row inline-block">
+ <div class="layui-col-xs7">
+ <input name="verifyCodeActual" type="text" placeholder="验证码"
+ class="layui-input">
+ </div>
+ <div class="layui-col-xs5" style="padding-left: 10px;">
+ <img width="100px" height="35px" class="login-captcha" th:src="@{/code/image}">
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="layui-form-item">
+ <!--<a href="javascript:;" class="layui-link">帐号注册</a>-->
+ <a href="javascript:;" class="layui-link pull-right">忘记密码?</a>
+ </div>
+ <div class="layui-form-item">
+ <button type="submit" lay-filter="login-submit" class="layui-btn layui-btn-fluid" lay-submit>登 录</button>
+ </div>
+ </form>
+ <!--<div class="layui-form-item login-other">-->
+ <!--<label>第三方登录</label>-->
+ <!--<a href="javascript:;"><i class="layui-icon layui-icon-login-qq"></i></a>-->
+ <!--<a href="javascript:;"><i class="layui-icon layui-icon-login-wechat"></i></a>-->
+ <!--<a href="javascript:;"><i class="layui-icon layui-icon-login-weibo"></i></a>-->
+ <!--</div>-->
+ </div>
+ </div>
+ </div>
+
+ <div class="login-footer">
+ <p>© 2019 <a href="javascript:;" target="_blank">上海树维信息科技有限公司 版权所有</a></p>
+ <!--<p>-->
+ <!--<span><a href="javascript:;" target="_blank">前往github</a></span>-->
+ <!--<span><a href="https://gitee.com/andyzy/zy-admin.git" target="_blank">前往gitee</a></span>-->
+ <!--<span><a href="javascript:;" target="_blank">文档说明</a></span>-->
+ <!--</p>-->
+ </div>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/util/Constants.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/util/Constants.java
index 0bf11d8..a58a7f5 100644
--- a/payapi/src/main/java/com/supwisdom/dlpay/framework/util/Constants.java
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/util/Constants.java
@@ -11,4 +11,6 @@
public static final String JWT_CLAIM_AUTHORITIES = "authorities";
// 根商户ID
public static final Integer ROOT_SHOP_FID = 1;
+
+ public static final String HEADER_AUTHORIZATION = "Authorization";
}
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/framework/controller/security_controller.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/framework/controller/security_controller.kt
index 8ca8d92..db0f82d 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/framework/controller/security_controller.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/framework/controller/security_controller.kt
@@ -21,12 +21,18 @@
import com.supwisdom.dlpay.framework.util.*
import com.supwisdom.dlpay.system.service.FunctionService
import mu.KotlinLogging
+import org.jose4j.jwt.ReservedClaimNames
import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
+import org.springframework.security.core.Authentication
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper
+import org.springframework.security.oauth2.provider.OAuth2Authentication
+import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler
import org.springframework.social.connect.web.HttpSessionSessionStrategy
import org.springframework.stereotype.Controller
@@ -204,11 +210,29 @@
@RestController
class UserInforController {
+ @Autowired
+ lateinit var jwtConfig: JwtConfig
+ @Autowired
+ private lateinit var redisConnectionFactory: RedisConnectionFactory
- @RequestMapping("/api/userinfor")
- fun user(user: Principal): Principal {
- System.out.println(user)
- return user
+ @RequestMapping("/userinfor")
+ fun user(@RequestParam("access_token") access_token: String?,
+ @RequestHeader(Constants.HEADER_AUTHORIZATION) auth: String?): ResponseEntity<Any> {
+ if (access_token.isNullOrEmpty() && auth.isNullOrEmpty()) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
+ }
+ var jwt: String
+ if(!auth.isNullOrEmpty()){
+ jwt = auth.substring(jwtConfig.tokenHeader.length)
+ }else{
+ jwt = access_token!!
+ }
+ val obj: OAuth2Authentication? = RedisTokenStore(redisConnectionFactory).readAuthentication(jwt) ?: return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
+ val user = obj!!.userAuthentication.principal as UserDetails
+ if(user.username.isNullOrEmpty()){
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
+ }
+ return ResponseEntity.status(HttpStatus.OK).body("""{"name":"${user.username}"}""")
}
}
@@ -220,6 +244,7 @@
@Autowired
lateinit var commonService: CommonService
+
private val logger = KotlinLogging.logger {}
@GetMapping("/login")
@@ -260,6 +285,7 @@
return "index"
}
+
}
@Controller
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/oauth.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/oauth.kt
index a249193..d007c7e 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/oauth.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/oauth.kt
@@ -1,13 +1,16 @@
package com.supwisdom.dlpay
+import com.supwisdom.dlpay.framework.core.PasswordBCryptConfig
import com.supwisdom.dlpay.system.service.ParamService
import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer
@@ -23,8 +26,7 @@
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore
import java.util.*
import org.springframework.security.oauth2.provider.token.TokenStore
-
-
+import java.security.SecureRandom
class OAuthDetailService : ClientDetailsService {
@@ -47,9 +49,16 @@
details.clientSecret = it.bcryptSecret
details.accessTokenValiditySeconds = 3600
details.refreshTokenValiditySeconds=43200
- val redir = HashSet<String>()
- redir.add("http://localhost:8091/wt/sso/login")
- details.registeredRedirectUri = redir
+ if(!it.thirdurl.isNullOrEmpty()){
+ val redir = HashSet<String>()
+ when {
+ it.thirdurl.contains(",") -> redir.addAll(it.thirdurl.split(","))
+ it.thirdurl.contains(";") -> redir.addAll(it.thirdurl.split(";"))
+ else -> redir.add(it.thirdurl)
+ }
+ details.registeredRedirectUri = redir
+ }
+
}
return details
}
@@ -70,9 +79,21 @@
@Autowired
private lateinit var authenticationManager: AuthenticationManager
+ @Autowired
+ lateinit var passwordBCryptConfig: PasswordBCryptConfig
override fun configure(security: AuthorizationServerSecurityConfigurer?) {
security?.allowFormAuthenticationForClients()
+ security?.passwordEncoder(pwdEncoder())
+ }
+ @Bean
+ fun pwdEncoder(): BCryptPasswordEncoder {
+ return if (passwordBCryptConfig.seed.isBlank()) {
+ BCryptPasswordEncoder()
+ } else {
+ BCryptPasswordEncoder(passwordBCryptConfig.length,
+ SecureRandom(passwordBCryptConfig.seed.toByteArray()))
+ }
}
override fun configure(clients: ClientDetailsServiceConfigurer?) {
diff --git a/settings.gradle b/settings.gradle
index 42be481..35b2274 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,3 @@
rootProject.name = 'payapi'
-include 'payapi', 'payapi-sdk', 'payapi-common', 'ynrcc-agent'
+include 'payapi', 'payapi-sdk', 'payapi-common', 'ynrcc-agent','oauth'
+