From c519e7916284466eb40b19570f4196f22b5b338f Mon Sep 17 00:00:00 2001 From: Tang Cheng Date: Fri, 19 Apr 2019 16:32:50 +0800 Subject: [PATCH] =?utf8?q?=E5=A2=9E=E5=8A=A0=20jwt=20=E8=AE=A4=E8=AF=81?= =?utf8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- config/application-devel-pg-local.properties | 1 - sql/init_test.sql | 4 +- .../dlpay/framework/core/JwtConfig.java | 26 ++++ .../dlpay/framework/core/JwtTokenUtil.java | 73 ++++++++--- .../framework/domain/ApiClientRedis.java | 10 ++ .../dlpay/framework/domain/TApiClient.java | 11 ++ .../com/supwisdom/dlpay/PayApiApplication.kt | 86 ------------- .../controller/security_controller.kt | 16 ++- .../kotlin/com/supwisdom/dlpay/security.kt | 120 ++++++++++++++++++ .../dlpay/user/controller/user_controller.kt | 2 +- 10 files changed, 238 insertions(+), 111 deletions(-) create mode 100644 src/main/java/com/supwisdom/dlpay/framework/core/JwtConfig.java create mode 100644 src/main/kotlin/com/supwisdom/dlpay/security.kt diff --git a/config/application-devel-pg-local.properties b/config/application-devel-pg-local.properties index 39d4caf9..3602345e 100644 --- a/config/application-devel-pg-local.properties +++ b/config/application-devel-pg-local.properties @@ -16,4 +16,3 @@ redis.database=0 jwt.secret=Zj5taLomEbrM0lk+NMQZbHfSxaDU1wekjT+kiC3YzDw= # timeout seconds jwt.expiration=3600 -jwt.header=payapi diff --git a/sql/init_test.sql b/sql/init_test.sql index d4396a66..c13e4cfe 100644 --- a/sql/init_test.sql +++ b/sql/init_test.sql @@ -1,4 +1,4 @@ -insert into tt_apiclient(appid, secret, status) -values ('100001', 'oUw2NmA09ficiVWD4TUQLDOkPyzQa3VzbjjsW0B2qTk=', 'normal'); +insert into tb_apiclient(appid, secret, status, roles) +values ('100001', 'oUw2NmA09ficiVWD4TUQLDOkPyzQa3VzbjjsW0B2qTk=', 'normal', 'ROLE_THIRD_ADMIN'); commit; \ No newline at end of file diff --git a/src/main/java/com/supwisdom/dlpay/framework/core/JwtConfig.java b/src/main/java/com/supwisdom/dlpay/framework/core/JwtConfig.java new file mode 100644 index 00000000..1eccb8a4 --- /dev/null +++ b/src/main/java/com/supwisdom/dlpay/framework/core/JwtConfig.java @@ -0,0 +1,26 @@ +package com.supwisdom.dlpay.framework.core; + +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"; + + public String getSecret() { + return secret; + } + + public Long getExpiration() { + return expiration; + } + + public String getHeader() { + return header; + } +} diff --git a/src/main/java/com/supwisdom/dlpay/framework/core/JwtTokenUtil.java b/src/main/java/com/supwisdom/dlpay/framework/core/JwtTokenUtil.java index 86060cf7..20344ab7 100644 --- a/src/main/java/com/supwisdom/dlpay/framework/core/JwtTokenUtil.java +++ b/src/main/java/com/supwisdom/dlpay/framework/core/JwtTokenUtil.java @@ -1,29 +1,30 @@ package com.supwisdom.dlpay.framework.core; +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.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; import org.jose4j.lang.JoseException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.Configuration; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; import java.util.HashMap; +import java.util.List; import java.util.Map; -@Configuration -@EnableAutoConfiguration -@Component public class JwtTokenUtil { - @Value("${jwt.secret}") - private String secret; - @Value("${jwt.expiration}") - private Long expiration = 3600L; - @Value("${jwt.header}") - private String header = "supwisdom"; + private JwtConfig jwtConfig; + + public JwtTokenUtil(JwtConfig config) { + this.jwtConfig = config; + } + + public String getHeader() { + return jwtConfig.getHeader(); + } public String generateToken(Map params) throws JoseException { JwtClaims claims = new JwtClaims(); @@ -31,13 +32,16 @@ public class JwtTokenUtil { if (params.get("audience") != null) { claims.setAudience(params.get("audience").toString()); } - claims.setExpirationTimeMinutesInTheFuture(expiration / 60); // time when the token will expire (10 minutes from now) + 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("authorities") != null) { + claims.setClaim("authorities", params.get("authorities")); + } /* 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"); @@ -46,7 +50,7 @@ public class JwtTokenUtil { Map keySpec = new HashMap<>(); keySpec.put("kty", "oct"); - keySpec.put("k", secret); + keySpec.put("k", jwtConfig.getSecret()); JsonWebKey key = JsonWebKey.Factory.newJwk(keySpec); JsonWebSignature jws = new JsonWebSignature(); jws.setPayload(claims.toJson()); @@ -61,4 +65,43 @@ public class JwtTokenUtil { 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 + .setRequireSubject() // the JWT must have a subject claim + .setExpectedIssuer("supwisdom") // whom the JWT needs to have been issued by + .setVerificationKey(key.getKey()) // verify the signature with the public key + .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); + System.out.println("JWT validation succeeded! " + jwtClaims); + return jwtClaims.flattenClaims(); + // InvalidJwtException will be thrown, if the JWT failed processing or validation in anyway. + // Hopefully with meaningful explanations(s) about what went wrong. + +// // Programmatic access to (some) specific reasons for JWT invalidity is also possible +// // should you want different error handling behavior for certain conditions. +// +// // Whether or not the JWT has expired being one common reason for invalidity +// if (e.hasExpired()) +// { +// System.out.println("JWT expired at " + e.getJwtContext().getJwtClaims().getExpirationTime()); +// } +// +// // Or maybe the audience was invalid +// if (e.hasErrorCode(ErrorCodes.AUDIENCE_INVALID)) +// { +// System.out.println("JWT had wrong audience: " + e.getJwtContext().getJwtClaims().getAudience()); +// } + } } diff --git a/src/main/java/com/supwisdom/dlpay/framework/domain/ApiClientRedis.java b/src/main/java/com/supwisdom/dlpay/framework/domain/ApiClientRedis.java index faee9012..2cb43ebc 100644 --- a/src/main/java/com/supwisdom/dlpay/framework/domain/ApiClientRedis.java +++ b/src/main/java/com/supwisdom/dlpay/framework/domain/ApiClientRedis.java @@ -11,6 +11,8 @@ public class ApiClientRedis { String token; + String roles; + String loginTimestamp; public String getId() { @@ -36,4 +38,12 @@ public class ApiClientRedis { public void setLoginTimestamp(String loginTimestamp) { this.loginTimestamp = loginTimestamp; } + + public String getRoles() { + return roles; + } + + public void setRoles(String roles) { + this.roles = roles; + } } diff --git a/src/main/java/com/supwisdom/dlpay/framework/domain/TApiClient.java b/src/main/java/com/supwisdom/dlpay/framework/domain/TApiClient.java index 35f2610f..385bff93 100644 --- a/src/main/java/com/supwisdom/dlpay/framework/domain/TApiClient.java +++ b/src/main/java/com/supwisdom/dlpay/framework/domain/TApiClient.java @@ -18,6 +18,9 @@ public class TApiClient { @Column(name = "status", nullable = false, length = 10) private String status; + @Column(name = "roles", length = 300) + private String roles; + public String getAppid() { return appid; } @@ -41,4 +44,12 @@ public class TApiClient { public void setStatus(String status) { this.status = status; } + + public String getRoles() { + return roles; + } + + public void setRoles(String roles) { + this.roles = roles; + } } diff --git a/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt b/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt index b5dd0e51..7ff33c6c 100644 --- a/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt +++ b/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt @@ -10,25 +10,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.core.annotation.Order 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.repository.configuration.EnableRedisRepositories -import org.springframework.security.authentication.ProviderManager -import org.springframework.security.authentication.dao.DaoAuthenticationProvider -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.userdetails.User -import org.springframework.security.core.userdetails.UserDetailsService -import org.springframework.security.provisioning.InMemoryUserDetailsManager -import org.springframework.security.web.authentication.AuthenticationFailureHandler -import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler -import javax.servlet.Filter @Configuration @@ -62,79 +49,6 @@ class AppConfig { } } -@EnableWebSecurity -class WebSecurityConfig { - @Bean - fun userDetailsService(): UserDetailsService { - val manager = InMemoryUserDetailsManager() - manager.createUser(User.withDefaultPasswordEncoder() - .username("admin") - .password("123456") - .roles("USER").build()) - return manager - } - - @Bean - fun daoProvider(detailsService: UserDetailsService): DaoAuthenticationProvider { - return DaoAuthenticationProvider().also { - it.setUserDetailsService(detailsService) - } - } - - @Bean - fun providerManager(daoProvider: DaoAuthenticationProvider): ProviderManager { - return ProviderManager(listOf(daoProvider)) - } - - - companion object { - @Configuration - @Order(1) - class ApiWebSecurityConfigurationAdapter : WebSecurityConfigurerAdapter() { - - override fun configure(http: HttpSecurity) { - http.authorizeRequests() -// .antMatchers("/login", "/resources/**", "/about", "/common/**").permitAll() -// .antMatchers("/admin/**").hasRole("ADMIN") -// .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") - .antMatchers("/**").permitAll() - .antMatchers("/admin/**").hasRole("ADMIN") - .anyRequest().authenticated() - .and().httpBasic() - .and() - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) -// .oauth2Client() -// .clientRegistrationRepository(clientRegistrationRepository) -// .authorizedClientRepository(this.authorizedClientRepository()) -// .authorizedClientService(this.authorizedClientService()) -// .authorizationCodeGrant() -// .authorizationRequestRepository(this.authorizationRequestRepository()) -// .authorizationRequestResolver(this.authorizationRequestResolver()) -// .accessTokenResponseClient(this.accessTokenResponseClient()) - } - } - - - @Configuration - class MvcWebSecurityConfigurationAdapter : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http.authorizeRequests() - .anyRequest().authenticated() - .and() - .formLogin() - .loginPage("/login") - .loginProcessingUrl("/login/form") - .and() - .logout() - .logoutSuccessUrl("/login") - .invalidateHttpSession(true) - .addLogoutHandler(CookieClearingLogoutHandler()) - } - } - } -} - - @SpringBootApplication class PayApiApplication diff --git a/src/main/kotlin/com/supwisdom/dlpay/framework/controller/security_controller.kt b/src/main/kotlin/com/supwisdom/dlpay/framework/controller/security_controller.kt index fe3c2790..cde879a1 100644 --- a/src/main/kotlin/com/supwisdom/dlpay/framework/controller/security_controller.kt +++ b/src/main/kotlin/com/supwisdom/dlpay/framework/controller/security_controller.kt @@ -1,6 +1,7 @@ package com.supwisdom.dlpay.framework.controller import com.supwisdom.dlpay.framework.ResponseBodyBuilder +import com.supwisdom.dlpay.framework.core.JwtConfig import com.supwisdom.dlpay.framework.core.JwtTokenUtil import com.supwisdom.dlpay.framework.dao.ApiClientDao import com.supwisdom.dlpay.framework.domain.AppClientRedis @@ -12,6 +13,7 @@ import com.supwisdom.dlpay.framework.redisrepo.ApiClientRepository import com.supwisdom.dlpay.framework.service.SystemUtilService import com.supwisdom.dlpay.framework.util.HmacUtil import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.social.connect.web.HttpSessionSessionStrategy import org.springframework.stereotype.Controller @@ -25,7 +27,7 @@ import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @RestController -@RequestMapping("/auth") +@RequestMapping("/api/auth") class ApiAuthController { @Autowired @@ -38,13 +40,13 @@ class ApiAuthController { lateinit var systemUtil: SystemUtilService @Autowired - lateinit var jwtUtil: JwtTokenUtil + lateinit var jwtConfig: JwtConfig @GetMapping("/gettoken") fun loginInit(appid: String): ResponseEntity { apiClient.findById(appid).run { if (!isPresent) { - return ResponseEntity.status(401).build() + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build() } if (get().status != "normal") { return ResponseEntity.ok(ResponseBodyBuilder.create() @@ -57,6 +59,7 @@ class ApiAuthController { ApiClientRedis().apply { id = appid loginTimestamp = now + roles = it.roles this.token = HmacUtil.HMACSHA256(token, it.secret) }.also { repo.save(it) @@ -86,15 +89,16 @@ class ApiAuthController { client.token = "" repo.save(client) } - val token = jwtUtil.generateToken( + val token = JwtTokenUtil(jwtConfig).generateToken( mapOf("uid" to appid, "issuer" to "supwisdom", - "subject" to "payapi")) + "subject" to "payapi", + "authorities" to it.get().roles.split(";"))) ResponseEntity.ok(ResponseBodyBuilder.create() .data("jwt", token) .data("appid", appid) .success()) } else { - ResponseEntity.status(401).build() + ResponseEntity.status(HttpStatus.UNAUTHORIZED).build() } } } diff --git a/src/main/kotlin/com/supwisdom/dlpay/security.kt b/src/main/kotlin/com/supwisdom/dlpay/security.kt new file mode 100644 index 00000000..26e2df7d --- /dev/null +++ b/src/main/kotlin/com/supwisdom/dlpay/security.kt @@ -0,0 +1,120 @@ +package com.supwisdom.dlpay + +import com.supwisdom.dlpay.framework.core.JwtConfig +import com.supwisdom.dlpay.framework.core.JwtTokenUtil +import org.jose4j.jwt.consumer.InvalidJwtException +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.http.HttpStatus +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +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.authority.SimpleGrantedAuthority +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler +import org.springframework.web.filter.OncePerRequestFilter +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + + +class ApiJwtAuthenticationFilter(jwt: JwtTokenUtil) : OncePerRequestFilter() { + private val jwtUtil = jwt + + override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { + request.getHeader(jwtUtil.header)?.let { jwt -> + try { + val claims = jwtUtil.verifyToken(jwt) + val auth = UsernamePasswordAuthenticationToken(claims["uid"], null, + claims["authorities"]?.map { SimpleGrantedAuthority(it as String) }) + SecurityContextHolder.getContext().authentication = auth + } catch (e: InvalidJwtException) { + SecurityContextHolder.clearContext(); + response.sendError(HttpStatus.BAD_REQUEST.value(), e.message) + } + } + filterChain.doFilter(request, response) + } +} + +@EnableWebSecurity +class WebSecurityConfig { + + @Bean + fun userDetailsService(): UserDetailsService { + val manager = InMemoryUserDetailsManager() + manager.createUser(User.withDefaultPasswordEncoder() + .username("admin") + .password("123456") + .roles("USER").build()) + return manager + } + +// @Bean +// fun daoProvider(detailsService: UserDetailsService): DaoAuthenticationProvider { +// return DaoAuthenticationProvider().also { +// it.setUserDetailsService(detailsService) +// } +// } +// +// @Bean +// fun providerManager(daoProvider: DaoAuthenticationProvider): ProviderManager { +// return ProviderManager(listOf(daoProvider)) +// } + + + companion object { + @Configuration + @Order(1) + class ApiWebSecurityConfigurationAdapter : WebSecurityConfigurerAdapter() { + + @Autowired + lateinit var jwtConfig: JwtConfig + + override fun configure(http: HttpSecurity) { + // 设置 API 访问权限管理 + http.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .addFilterAfter(ApiJwtAuthenticationFilter(JwtTokenUtil(jwtConfig)), + UsernamePasswordAuthenticationFilter::class.java) + .authorizeRequests() + .antMatchers("/api/auth/**").permitAll() + .antMatchers("/api/common/**").hasAnyRole("THIRD_COMMON", "THIRD_ADMIN") + .antMatchers("/api/consume/**").hasRole("THIRD_CONSUME") + .antMatchers("/api/deposit/**").hasRole("THIRD_DEPOSIT") + .antMatchers("/api/user/**").hasAnyRole("THIRD_COMMON", "THIRD_ADMIN") + .antMatchers("/api/shop/**").hasRole("THIRD_SHOP") + .anyRequest().authenticated() + // 注册 filter + } + } + + @Configuration + class MvcWebSecurityConfigurationAdapter : WebSecurityConfigurerAdapter() { + + override fun configure(http: HttpSecurity) { + // 设置 Web MVC 应用权限 + http.authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage("/user/login").permitAll() + .and() + .logout() + .logoutUrl("/user/logout") + .logoutSuccessUrl("/user/home") + .invalidateHttpSession(true) + .addLogoutHandler(CookieClearingLogoutHandler()) + } + } + } +} diff --git a/src/main/kotlin/com/supwisdom/dlpay/user/controller/user_controller.kt b/src/main/kotlin/com/supwisdom/dlpay/user/controller/user_controller.kt index 9714aeab..d92da833 100644 --- a/src/main/kotlin/com/supwisdom/dlpay/user/controller/user_controller.kt +++ b/src/main/kotlin/com/supwisdom/dlpay/user/controller/user_controller.kt @@ -43,7 +43,7 @@ class UserController { .success()) } - @GetMapping("/get") + @PostMapping("/get") fun queryShop(@RequestBody request: UserParam): ResponseEntity { if (!request.uniqueId.isNullOrEmpty()) { val person = useService.findByThirdUniqueIdenty(request.uniqueId!!) -- 2.17.1