From: qiaowei Date: Fri, 2 Aug 2019 03:02:44 +0000 (+0800) Subject: 增加OAuth模块 X-Git-Tag: 1.0.1^2~51 X-Git-Url: https://source.supwisdom.com/gerrit/gitweb?a=commitdiff_plain;h=f408cf2052526d908cb35bbdc64796312335ac90;p=epayment%2Ffood_payapi.git 增加OAuth模块 --- diff --git a/oauth/build.gradle b/oauth/build.gradle new file mode 100644 index 00000000..17d42526 --- /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 00000000..674639b4 --- /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 { + val template = RedisTemplate() + 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) { + 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 00000000..cf0bdbe0 --- /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 { + + @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 +} 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 00000000..4355fcff --- /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 { + 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 00000000..481c46af --- /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? { + 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? = 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 00000000..404e64a0 --- /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 00000000..16e27bac --- /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 { + 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() + 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() + 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 00000000..f70bb020 --- /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 00000000..5df80ef6 --- /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 00000000..47364bdc --- /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 = 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 00000000..7cf989fe --- /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 00000000..5b355c6c --- /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 | + * firstTimesecondTime int>0 | + * firstTimesecondTime + * int>0 | firstTime+seconds 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 00000000..3bf4921a --- /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 00000000..8d17542d --- /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 00000000..9304dcde --- /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 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 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 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 claims = new HashMap<>(); + claims.put("uid", userDetails.getUsername()); + return generateToken(claims); + } + + public Map verifyToken(String token) throws JoseException, InvalidJwtException { + Map 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 00000000..55d2c03c --- /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 00000000..ca7317a9 --- /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 00000000..c7190966 --- /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 00000000..9f0118cd --- /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 { + + @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 00000000..ac69dd13 --- /dev/null +++ b/oauth/src/main/resources/templates/login.html @@ -0,0 +1,79 @@ + + + + + 统一身份认证 + + + + + + + + \ 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 0bf11d8f..a58a7f56 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 class Constants { 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 8ca8d926..db0f82de 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.service.SystemUtilService 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 @@ class ValidateCodeController { @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 { + 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 @@ class WebMainController { @Autowired lateinit var commonService: CommonService + private val logger = KotlinLogging.logger {} @GetMapping("/login") @@ -260,6 +285,7 @@ class WebMainController { 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 a2491931..d007c7e5 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.client.ClientCredentialsToke 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 @@ class OAuthDetailService : ClientDetailsService { details.clientSecret = it.bcryptSecret details.accessTokenValiditySeconds = 3600 details.refreshTokenValiditySeconds=43200 - val redir = HashSet() - redir.add("http://localhost:8091/wt/sso/login") - details.registeredRedirectUri = redir + if(!it.thirdurl.isNullOrEmpty()){ + val redir = HashSet() + 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 @@ class OAuth2Config { @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 42be4811..35b2274f 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' +