增加 jwt 认证接口
diff --git a/config/application-devel-pg-local.properties b/config/application-devel-pg-local.properties
index 39d4caf..3602345 100644
--- a/config/application-devel-pg-local.properties
+++ b/config/application-devel-pg-local.properties
@@ -16,4 +16,3 @@
 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 d4396a6..c13e4cf 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 0000000..1eccb8a
--- /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 86060cf..20344ab 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<String, Object> params) throws JoseException {
     JwtClaims claims = new JwtClaims();
@@ -31,13 +32,16 @@
     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<String> groups = Arrays.asList("group-one", "other-group", "group-three");
@@ -46,7 +50,7 @@
 
     Map<String, Object> 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 @@
     claims.put("uid", userDetails.getUsername());
     return generateToken(claims);
   }
+
+  public Map<String, List<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
+        .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 faee901..2cb43eb 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 @@
 
   String token;
 
+  String roles;
+
   String loginTimestamp;
 
   public String getId() {
@@ -36,4 +38,12 @@
   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 35f2610..385bff9 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 @@
   @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 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 b5dd0e5..7ff33c6 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.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 @@
     }
 }
 
-@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 fe3c279..cde879a 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.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.HttpServletResponse
 
 @RestController
-@RequestMapping("/auth")
+@RequestMapping("/api/auth")
 class ApiAuthController {
 
     @Autowired
@@ -38,13 +40,13 @@
     lateinit var systemUtil: SystemUtilService
 
     @Autowired
-    lateinit var jwtUtil: JwtTokenUtil
+    lateinit var jwtConfig: JwtConfig
 
     @GetMapping("/gettoken")
     fun loginInit(appid: String): ResponseEntity<Any> {
         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 @@
             ApiClientRedis().apply {
                 id = appid
                 loginTimestamp = now
+                roles = it.roles
                 this.token = HmacUtil.HMACSHA256(token, it.secret)
             }.also {
                 repo.save(it)
@@ -86,15 +89,16 @@
                     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 0000000..26e2df7
--- /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 9714aea..d92da83 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 @@
                 .success())
     }
 
-    @GetMapping("/get")
+    @PostMapping("/get")
     fun queryShop(@RequestBody request: UserParam): ResponseEntity<Any> {
         if (!request.uniqueId.isNullOrEmpty()) {
             val person = useService.findByThirdUniqueIdenty(request.uniqueId!!)