快速搭建微服务-服务安全
微服务架构下的服务安全是构建微服务系统的一个重要环节。做好服务鉴权是保障数据不泄漏、不被非法操作的关键。
Spring Cloud架构支持OAuth2 + Spring Security的方式进行服务鉴权,只需简单配置即可。同时我们也可以在网关服务里加入自定义的鉴权Filter实现服务鉴权。
采用OAuth2 + Spring Security的方式进行服务鉴权时,如果同时使用了Hystrix断路器,就会出现后台服务之间进行调用时access_token无法在服务间传递的问题,其根本原因是Hystix的默认隔离策略是Thread(即线程隔离),这样就会导致服务间调用时没有将access_token进行传递,导致鉴权失败。此问题的具体解决办法可以在 实用技巧:Hystrix传播ThreadLocal对象(两种方案) 中找到。
本文对OAuth2 + Spring Security和自定义鉴权Filter都进行说明。
OAuth2 + Spring Security方式
采用OAuth2 + Spring Security方式需要区分鉴权服务和资源服务。
api-gateway网关服务
1 2 3 4 5 6 7 8
   | <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
   | 
 
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
   | zuul:   routes:     api-order:       path: /api-order/**       service-id: service-order       sensitiveHeaders: Cookie,Set-Cookie     api-goods:       path: /api-goods/**       service-id: service-goods       sensitiveHeaders: Cookie,Set-Cookie     auth:       path: /auth/**       service-id: auth-server       sensitiveHeaders: Cookie,Set-Cookie
  spring:   application:     name: api-gateway
  security:   oauth2:     client:       access-token-uri: http://localhost:9080/auth/oauth/token       user-authorization-uri: http://localhost:9080/auth/oauth/authorize       client-id: webapp     resource:       user-info-uri: http://localhost:9080/auth/user       prefer-token-info: false
   | 
 
在配置zuul.routes网关路由时,需要注意sensitiveHeaders需要配置Cookie,Set-Cookie,这样才能在请求网关时携带token并在刷新之后返回token。
1 2 3 4 5 6 7 8 9
   | @Configuration @EnableOAuth2Sso public class SecurityConfig extends WebSecurityConfigurerAdapter {
      @Override     protected void configure(HttpSecurity http) throws Exception {         http.csrf().disable();     } }
   | 
 
auth-server鉴权服务
1 2 3 4 5 6 7 8 9 10 11 12 13
   | <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency>     <groupId>org.springframework.security</groupId>     <artifactId>spring-security-jwt</artifactId>     <optional>true</optional> </dependency>
   | 
 
1 2 3 4
   | security:   oauth2:     resource:       filter-order: 3
   | 
 
启动类加上@EnableAuthorizationServer注解声明当前应用为鉴权服务端。
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
   | @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
      @Autowired     private UserDetailsServiceImpl userDetailsService;
      @Bean     public ShaPasswordEncoder passwordEncoder() {         return new ShaPasswordEncoder(256);     }
      @Bean     public DaoAuthenticationProvider authenticationProvider() {         DaoAuthenticationProvider provider = new DaoAuthenticationProvider();         provider.setPasswordEncoder(passwordEncoder());         provider.setUserDetailsService(userDetailsService);         provider.setSaltSource((userDetails -> userDetails.getUsername() + AppConsts.EncryptionConsts.ENCRYPT_EXTRA_SALT));         return provider;     }
      @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.authenticationProvider(authenticationProvider()).userDetailsService(userDetailsService);     }
      @Bean     @Override     public AuthenticationManager authenticationManagerBean() throws Exception {         return super.authenticationManagerBean();     } }
   | 
 
- AuthorizationServerConfig.java
 
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
   | @Configuration public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
      @Autowired     private UserDetailsServiceImpl userDetailsService;
      @Autowired     private JedisConnectionFactory connectionFactory;
      @Autowired     private AuthenticationManager authenticationManager;
      @Bean     public RedisTokenStore tokenStore() {         return new RedisTokenStore(connectionFactory);     }
      @Override     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {         endpoints.authenticationManager(authenticationManager)                 .userDetailsService(userDetailsService)                 .tokenStore(tokenStore());     }
      @Override     public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {         security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");     }
      @Override     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {         clients.inMemory()                 .withClient("android")                 .scopes("app")                 .secret("android")                 .authorizedGrantTypes("password", "authorization_code", "refresh_token")                 .and()                 .withClient("web")                 .scopes("web")                 .authorizedGrantTypes("implicit");     } }
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13
   | @FrameworkEndpoint public class RevokeTokenEndpoint {
      @Autowired     @Qualifier("consumerTokenServices")     private ConsumerTokenServices consumerTokenServices;
      @DeleteMapping("/oauth/token")     @ResponseBody     public String revokeToken(String accessToken) {         return consumerTokenServices.revokeToken(accessToken) ? "注销成功" : "注销失败";     } }
   | 
 
资源服务
1 2 3 4 5 6 7 8
   | <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
   | 
 
1 2 3 4 5 6
   | security:   oauth2:     resource:       id: service-order       user-info-uri: http://localhost:9080/auth/user       prefer-token-info: false
   | 
 
- ResourceServerConfig.java
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
      @Autowired     private ObjectMapper objectMapper;
      @Override     public void configure(HttpSecurity http) throws Exception {         http.csrf().disable()                 .exceptionHandling()                 .authenticationEntryPoint((request, response, authException) -> {                     response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);                     objectMapper.writeValue(response.getWriter(), ResponseEntity.status(HttpStatus.UNAUTHORIZED));                 })                 .and()                 .authorizeRequests()                 .anyRequest().authenticated()                 .and()                 .httpBasic();     } }
   | 
 
自定义鉴权Filter
采用自定义鉴权Filter的方式只需要在网关服务里写一个Filter即可。
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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
   | public class AuthFilter extends ZuulFilter {
      @Autowired     private RestTemplate restTemplate;
      @Autowired     private RedisTemplate redisTemplate;
      @Autowired     private JwtUtils jwtUtils;
      @Override     public String filterType() {         return FilterConstants.PRE_TYPE;     }
      @Override     public int filterOrder() {         return 4;     }
      @Override     public boolean shouldFilter() {         return true;     }
      @Override     public Object run() {         RequestContext context = RequestContext.getCurrentContext();         HttpServletRequest request = context.getRequest();         String method = request.getMethod().toUpperCase();                  String url = request.getRequestURI();                  if (StringUtils.containsAny(url, AppConsts.PathConsts.getSkipPaths())) {             return null;         }         String token = jwtUtils.getToken(request);                  boolean tokenExpired = false;         Claims claims = null;         try {             claims = jwtUtils.getClaimsFromToken(token);         } catch (ExpiredJwtException | SignatureException e) {             tokenExpired = true;         }         if (tokenExpired) {                          TokenRefresh tokenRefresh = (TokenRefresh) redisTemplate.opsForValue().get(AppConsts.TokenConsts.REFRESH_TTL_KEY + token.hashCode());             if (tokenRefresh != null && tokenRefresh.getRefreshExpiredTime().compareTo(new Date()) > 0) {                 context.getResponse().setHeader(AppConsts.TokenConsts.AUTH_HEADER_NAME, jwtUtils.generateToken(tokenRefresh.getUsername(), tokenRefresh.getClientType()));                                  redisTemplate.delete(AppConsts.TokenConsts.REFRESH_TTL_KEY + token.hashCode());                 return null;             }                          context.setSendZuulResponse(false);             context.getResponse().setContentType(AppConsts.WebConsts.TEXT_PLAIN_UTF8_VALUE);             context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());             context.setResponseBody("Token已过期且无法刷新,请重新登录");             return null;         }         if (claims == null) {             context.setSendZuulResponse(false);             context.getResponse().setContentType(AppConsts.WebConsts.TEXT_PLAIN_UTF8_VALUE);             context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());             context.setResponseBody("Token丢失或被非法篡改");             return null;         }                  String username = jwtUtils.getUsernameFromToken(token);         User user = restTemplate.getForObject("http://auth-server/user/" + username, User.class);         if (user == null) {             context.setSendZuulResponse(false);             context.getResponse().setContentType(AppConsts.WebConsts.TEXT_PLAIN_UTF8_VALUE);             context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());             context.setResponseBody("Token中无有效用户信息");             return null;         }         boolean hasAuthority = false;                  Set<Authority> authorities = user.getAuthorities();         for (Authority authority : authorities) {             if (authority.getMethod().name().equals(method) && url.contains(authority.getUrl())) {                 hasAuthority = true;             }         }                  if (hasAuthority) {                          return null;         }                  context.setSendZuulResponse(false);         context.getResponse().setContentType(AppConsts.WebConsts.TEXT_PLAIN_UTF8_VALUE);         context.setResponseStatusCode(HttpStatus.FORBIDDEN.value());         context.setResponseBody("无访问权限");         return null;     } }
  |