基于spring cloud gateway的简易认证授权

用户的访问基本上都是需要携带 JWT Token 的,gateway-service 在接收到请求之后,会向鉴权服务发送鉴权请求,获得授权之后可以得到当前访问的用户详细信息;如果检测到请求未经授权,那么 gateway-service 直接会返回未授权错误,保护内部服务访问安全。
理论上鉴权也可以通过自定义 gateway filter 来实现,不过我们希望可以尽可能多使用 spring-security 提供的能力,毕竟自己封装需要实现的东西还是有点多的。
Spring Cloud Gateway 只支持搭配 webflux 使用,所以我们后续使用了 @EnableWebFluxSecurity 注解。
我们将 gateway 作为一个 resource server 进行配置,毕竟 spring-security 帮我们做了很多事情,所以需要我们自己配置的代码很少。
 api-server
资源服务器是一个普通的springboot服务,在整个微服务环境中是透明的,可以直接访问
curl http://localhost:8080/hello
Hello World.
1 2 3 4 5 6 7 8 9
   | @RestController public class ApiController {
      @GetMapping("/hello")     public String getRequest() {         return "Hello World.";     } }
 
   | 
 
 auth-server
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
   | <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency>     <groupId>com.nimbusds</groupId>     <artifactId>nimbus-jose-jwt</artifactId>     <version>8.2</version> </dependency>
   | 
 
Application.yml(签名文件在resource目录下)
1 2 3 4 5 6 7
   | server:   port: 8081
 
  management:   endpoints:     web.exposure.include: "*"
   | 
 
认证配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
   | @EnableAuthorizationServer @Configuration @AllArgsConstructor public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
      private final AuthenticationManager authenticationManagerBean;     private final PasswordEncoder passwordEncoder;
      @Override     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {         clients.inMemory()                 .withClient("test-client")                 .secret(passwordEncoder.encode("test-secret"))                 .authorizedGrantTypes("refresh_token", "password")                 .scopes("default-scope");     }
      @Override     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {         endpoints                 .authenticationManager(authenticationManagerBean)                 .accessTokenConverter(accessTokenConverter());     }
      @Override     public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {         security                 .allowFormAuthenticationForClients();     }
      @Bean     public AccessTokenConverter accessTokenConverter() {         JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();         jwtAccessTokenConverter.setKeyPair(keyPair());         return jwtAccessTokenConverter;     }
 
      @Bean     public KeyPair keyPair() {         KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("demojwt.jks"), "keystorepass".toCharArray());         return keyStoreKeyFactory.getKeyPair("jwt", "keypairpass".toCharArray());     } }
   | 
 
安全配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
   | @Configuration @EnableWebSecurity public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
      @Override     protected void configure(HttpSecurity http) throws Exception {         http.authorizeRequests()                 .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()                 .antMatchers("/.well-known/jwks.json").permitAll()                 .anyRequest().authenticated();     }
      @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.userDetailsService(s -> new UserDetails() {             @Override             public Collection<? extends GrantedAuthority> getAuthorities() {                 return AuthorityUtils.createAuthorityList("USER");             }
              @Override             public String getPassword() {                 return passwordEncoder().encode("user-password");             }
              @Override             public String getUsername() {                 return "user-username";             }
              @Override             public boolean isAccountNonExpired() {                 return true;             }
              @Override             public boolean isAccountNonLocked() {                 return true;             }
              @Override             public boolean isCredentialsNonExpired() {                 return true;             }
              @Override             public boolean isEnabled() {                 return true;             }         });     }
      @Override     @Bean     public AuthenticationManager authenticationManagerBean() throws Exception {         return super.authenticationManagerBean();     }
      @Bean     public PasswordEncoder passwordEncoder() {         return new BCryptPasswordEncoder();     }
  }
   | 
 
controller
1 2 3 4 5 6 7 8 9 10 11 12 13
   | @RestController @AllArgsConstructor public class MvcController {
      private final KeyPair keyPair;
      @GetMapping("/.well-known/jwks.json")     public Map<String, Object> getKey() {         RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();         RSAKey key = new RSAKey.Builder(publicKey).build();         return new JWKSet(key).toJSONObject();     } }
   | 
 
 gateway-service
Pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
   | <dependency> 	<groupId>org.springframework.boot</groupId> 	<artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> 	<groupId>org.springframework.boot</groupId> 	<artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> 	<groupId>org.springframework.cloud</groupId> 	<artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
  <dependency> 	<groupId>org.springframework.security</groupId> 	<artifactId>spring-security-config</artifactId> </dependency> <dependency> 	<groupId>org.springframework.security</groupId> 	<artifactId>spring-security-oauth2-resource-server</artifactId> </dependency> <dependency> 	<groupId>org.springframework.security</groupId> 	<artifactId>spring-security-oauth2-client</artifactId> </dependency> <dependency> 	<groupId>org.springframework.security</groupId> 	<artifactId>spring-security-oauth2-jose</artifactId> </dependency>
   | 
 
Application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13
   | server:   port: 8082
 
  spring:   cloud.gateway.routes:     - id: api-service-route       uri: http://localhost:8080       predicates:         - Path=/api/**       filters:         - StripPrefix=1   security.oauth2.resourceserver.jwt.jwk-set-uri: 'http://localhost:8081/.well-known/jwks.json'
   | 
 
因为我们的 authorization server 使用了 jwt token,jwt 很适合在纯 RESTful API 中作为无状态的认证凭证进行使用,搭配 spring security oauth2 的话,简单且好用。当然如果在某些场景下需要撤销某个 jwt token,也可以搭配 redis 进行管理。
我们在上面代码中声明了 gateway-service 作为一个简单的 resource server 并启用了 jwt,jwt token 通过公钥来验证有效性。因此我们需要指定 jwt 鉴权的公钥地址。
ResourceServerConfigurer
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | @EnableWebFluxSecurity public class ResourceServerConfigurer {
      @Bean     public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {         http.authorizeExchange()                 .pathMatchers("/actuator/**").permitAll()                 .anyExchange().authenticated();
          http.oauth2ResourceServer().jwt();
          return http.build();     } }
   | 
 
 测试
依次运行 auth-service,gateway-service,api-service
获取 access token
script1 2 3 4 5 6 7
   | curl -X POST \   http://localhost:8081/oauth/token \   -d grant_type=password \   -d client_id=test-client \   -d client_secret=test-secret \   -d username=user-username \   -d password=user-password
   | 
 
不带 token 访问接口,返回 401 Unauthorized
script1
   | curl -X GET http://localhost:8082/api/hello -sI
   | 
 
带 token 访问接口
script1 2 3
   | curl -X GET \   http://localhost:8082/api/hello \   -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzI0ODY3ODksInVzZXJfbmFtZSI6InVzZXItdXNlcm5hbWUiLCJhdXRob3JpdGllcyI6WyJVU0VSIl0sImp0aSI6IjA3MDZmOWUyLWRlMWYtNDg4ZS1hNTJhLTUzMjdiMjU4ZTI4ZiIsImNsaWVudF9pZCI6InRlc3QtY2xpZW50Iiwic2NvcGUiOlsiZGVmYXVsdC1zY29wZSJdfQ.IqSmO7rH_lFpQ8__ZP1rwSDh5S6kw3EPaT3gs-byW_usN5iM0l0ohEV1OCb1jOUjcMoxnqUM-C3ZEND_tVdGlpscNVXT1avkVGw8Rc2Y49_Ee0OYgLwg97LOFdoHgCIEWKqronk5EdZJES1Zv95MbGSO1o6U_mo8My1-znxNgUwYpGt1sNUsaoCRzIKEQR--67IWe3EMmxo1D0IcPPi0fgKbVD2LR7mOQTqvQMHEF4LplS67mbfUWLFJg9Q4gyH0l3ndgGkCIcofPop9-GlwNCgp52RiGQlp8MCXydHbybr-_g-nXrstxSHQ-1iC0ihIMSueDhEuN7gLe5OELBW1oA'
   | 
 
经过上面这些配置之后,我们就已经实现了一个比较简单的微服务架构下的网关服务了。
 参考
https://mp.weixin.qq.com/s/4v_wwX0SS7jvOwtO8uiDAw
 最后
本文到此结束,感谢阅读。如果您觉得不错,请关注公众号【当我遇上你】,您的支持是我写作的最大动力。