乐趣区

关于java:微服务权限终极解决方案Spring-Cloud-Gateway-Oauth2-实现统一认证和鉴权

SpringBoot 实战电商我的项目 mall(35k+star)地址:https://github.com/macrozheng/mall

摘要

最近发现了一个很好的微服务权限解决方案,能够通过认证服务进行对立认证,而后通过网关来对立校验认证和鉴权。此计划为目前最新计划,仅反对 Spring Boot 2.2.0、Spring Cloud Hoxton 以上版本,本文将具体介绍该计划的实现,心愿对大家有所帮忙!

前置常识

咱们将采纳 Nacos 作为注册核心,Gateway 作为网关,应用nimbus-jose-jwtJWT 库操作 JWT 令牌,对这些技术不理解的敌人能够看下上面的文章。

  • Spring Cloud Gateway:新一代 API 网关服务
  • Spring Cloud Alibaba:Nacos 作为注册核心和配置核心应用
  • 据说你的 JWT 库用起来特地扭,举荐这款贼好用的!

利用架构

咱们现实的解决方案应该是这样的,认证服务负责认证,网关负责校验认证和鉴权,其余 API 服务负责解决本人的业务逻辑。平安相干的逻辑只存在于认证服务和网关服务中,其余服务只是单纯地提供服务而没有任何平安相干逻辑。

相干服务划分:

  • micro-oauth2-gateway:网关服务,负责申请转发和鉴权性能,整合 Spring Security+Oauth2;
  • micro-oauth2-auth:Oauth2 认证服务,负责对登录用户进行认证,整合 Spring Security+Oauth2;
  • micro-oauth2-api:受爱护的 API 服务,用户鉴权通过后能够拜访该服务,不整合 Spring Security+Oauth2。

计划实现

上面介绍下这套解决方案的具体实现,顺次搭建认证服务、网关服务和 API 服务。

micro-oauth2-auth

咱们首先来搭建认证服务,它将作为 Oauth2 的认证服务应用,并且网关服务的鉴权性能也须要依赖它。

  • pom.xml 中增加相干依赖,次要是 Spring Security、Oauth2、JWT、Redis 相干依赖;
<dependencies>
    <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.16</version>
    </dependency>
    <!-- redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>
  • application.yml 中增加相干配置,次要是 Nacos 和 Redis 相干配置;
server:
  port: 9401
spring:
  profiles:
    active: dev
  application:
    name: micro-oauth2-auth
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
  redis:
    database: 0
    port: 6379
    host: localhost
    password: 
management:
  endpoints:
    web:
      exposure:
        include: "*"
  • 应用 keytool 生成 RSA 证书 jwt.jks,复制到resource 目录下,在 JDK 的 bin 目录下应用如下命令即可;
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
  • 创立 UserServiceImpl 类实现 Spring Security 的 UserDetailsService 接口,用于加载用户信息;
/**
 * 用户治理业务类
 * Created by macro on 2020/6/19.
 */
@Service
public class UserServiceImpl implements UserDetailsService {

    private List<UserDTO> userList;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostConstruct
    public void initData() {String password = passwordEncoder.encode("123456");
        userList = new ArrayList<>();
        userList.add(new UserDTO(1L,"macro", password,1, CollUtil.toList("ADMIN")));
        userList.add(new UserDTO(2L,"andy", password,1, CollUtil.toList("TEST")));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {List<UserDTO> findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList());
        if (CollUtil.isEmpty(findUserList)) {throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
        }
        SecurityUser securityUser = new SecurityUser(findUserList.get(0));
        if (!securityUser.isEnabled()) {throw new DisabledException(MessageConstant.ACCOUNT_DISABLED);
        } else if (!securityUser.isAccountNonLocked()) {throw new LockedException(MessageConstant.ACCOUNT_LOCKED);
        } else if (!securityUser.isAccountNonExpired()) {throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED);
        } else if (!securityUser.isCredentialsNonExpired()) {throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED);
        }
        return securityUser;
    }

}
  • 增加认证服务相干配置 Oauth2ServerConfig,须要配置加载用户信息的服务UserServiceImpl 及 RSA 的钥匙对KeyPair
/**
 * 认证服务器配置
 * Created by macro on 2020/6/19.
 */
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    private final PasswordEncoder passwordEncoder;
    private final UserServiceImpl userDetailsService;
    private final AuthenticationManager authenticationManager;
    private final JwtTokenEnhancer jwtTokenEnhancer;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory()
                .withClient("client-app")
                .secret(passwordEncoder.encode("123456"))
                .scopes("all")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(86400);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer); 
        delegates.add(accessTokenConverter());
        enhancerChain.setTokenEnhancers(delegates); // 配置 JWT 的内容增强器
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService) // 配置加载用户信息的服务
                .accessTokenConverter(accessTokenConverter())
                .tokenEnhancer(enhancerChain);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.allowFormAuthenticationForClients();
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setKeyPair(keyPair());
        return jwtAccessTokenConverter;
    }

    @Bean
    public KeyPair keyPair() {
        // 从 classpath 下的证书中获取秘钥对
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
    }

}
  • 如果你想往 JWT 中增加自定义信息的话,比如说 登录用户的 ID,能够本人实现 TokenEnhancer 接口;
/**
 * JWT 内容增强器
 * Created by macro on 2020/6/19.
 */
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
        Map<String, Object> info = new HashMap<>();
        // 把用户 ID 设置到 JWT 中
        info.put("id", securityUser.getId());
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}
  • 因为咱们的网关服务须要 RSA 的公钥来验证签名是否非法,所以认证服务须要有个接口把公钥裸露进去;
/**
 * 获取 RSA 公钥接口
 * Created by macro on 2020/6/19.
 */
@RestController
public class KeyPairController {

    @Autowired
    private KeyPair keyPair;

    @GetMapping("/rsa/publicKey")
    public Map<String, Object> getKey() {RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();}

}
  • 不要忘了还须要配置 Spring Security,容许获取公钥接口的拜访;
/**
 * SpringSecurity 配置
 * Created by macro on 2020/6/19.
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                .antMatchers("/rsa/publicKey").permitAll()
                .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();
    }

}
  • 创立一个资源服务ResourceServiceImpl,初始化的时候把资源与角色匹配关系缓存到 Redis 中,不便网关服务进行鉴权的时候获取。
/**
 * 资源与角色匹配关系治理业务类
 * Created by macro on 2020/6/19.
 */
@Service
public class ResourceServiceImpl {

    private Map<String, List<String>> resourceRolesMap;
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @PostConstruct
    public void initData() {resourceRolesMap = new TreeMap<>();
        resourceRolesMap.put("/api/hello", CollUtil.toList("ADMIN"));
        resourceRolesMap.put("/api/user/currentUser", CollUtil.toList("ADMIN", "TEST"));
        redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap);
    }
}

micro-oauth2-gateway

接下来咱们就能够搭建网关服务了,它将作为 Oauth2 的资源服务、客户端服务应用,对拜访微服务的申请进行对立的校验认证和鉴权操作。

  • pom.xml 中增加相干依赖,次要是 Gateway、Oauth2 和 JWT 相干依赖;
<dependencies>
    <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>
    <dependency>
        <groupId>com.nimbusds</groupId>
        <artifactId>nimbus-jose-jwt</artifactId>
        <version>8.16</version>
    </dependency>
</dependencies>
  • application.yml 中增加相干配置,次要是路由规定的配置、Oauth2 中 RSA 公钥的配置及路由白名单的配置;
server:
  port: 9201
spring:
  profiles:
    active: dev
  application:
    name: micro-oauth2-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes: #配置路由规定
        - id: oauth2-api-route
          uri: lb://micro-oauth2-api
          predicates:
            - Path=/api/**
          filters:
            - StripPrefix=1
        - id: oauth2-auth-route
          uri: lb://micro-oauth2-auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
      discovery:
        locator:
          enabled: true #开启从注册核心动态创建路由的性能
          lower-case-service-id: true #应用小写服务名,默认是大写
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: 'http://localhost:9401/rsa/publicKey' #配置 RSA 的公钥拜访地址
  redis:
    database: 0
    port: 6379
    host: localhost
    password: 
secure:
  ignore:
    urls: #配置白名单门路
      - "/actuator/**"
      - "/auth/oauth/token"
  • 对网关服务进行配置平安配置,因为 Gateway 应用的是 WebFlux,所以须要应用@EnableWebFluxSecurity 注解开启;
/**
 * 资源服务器配置
 * Created by macro on 2020/6/19.
 */
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
    private final AuthorizationManager authorizationManager;
    private final IgnoreUrlsConfig ignoreUrlsConfig;
    private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
        http.authorizeExchange()
                .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()// 白名单配置
                .anyExchange().access(authorizationManager)// 鉴权管理器配置
                .and().exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)// 解决未受权
                .authenticationEntryPoint(restAuthenticationEntryPoint)// 解决未认证
                .and().csrf().disable();
        return http.build();}

    @Bean
    public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }

}
  • WebFluxSecurity 中自定义鉴权操作须要实现 ReactiveAuthorizationManager 接口;
/**
 * 鉴权管理器,用于判断是否有资源的拜访权限
 * Created by macro on 2020/6/19.
 */
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        // 从 Redis 中获取以后门路可拜访角色列表
        URI uri = authorizationContext.getExchange().getRequest().getURI();
        Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath());
        List<String> authorities = Convert.toList(String.class,obj);
        authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());
        // 认证通过且角色匹配的用户可拜访以后门路
        return mono
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .any(authorities::contains)
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
    }

}
  • 这里咱们还须要实现一个全局过滤器AuthGlobalFilter,当鉴权通过后将 JWT 令牌中的用户信息解析进去,而后存入申请的 Header 中,这样后续服务就不须要解析 JWT 令牌了,能够间接从申请的 Header 中获取到用户信息。
/**
 * 将登录用户的 JWT 转化成用户信息的全局过滤器
 * Created by macro on 2020/6/17.
 */
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StrUtil.isEmpty(token)) {return chain.filter(exchange);
        }
        try {
            // 从 token 中解析用户信息并设置到 Header 中去
            String realToken = token.replace("Bearer", "");
            JWSObject jwsObject = JWSObject.parse(realToken);
            String userStr = jwsObject.getPayload().toString();
            LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr);
            ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build();
            exchange = exchange.mutate().request(request).build();} catch (ParseException e) {e.printStackTrace();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {return 0;}
}

micro-oauth2-api

最初咱们搭建一个 API 服务,它不会集成和实现任何平安相干逻辑,全靠网关来爱护它。

  • pom.xml 中增加相干依赖,就增加了一个 web 依赖;
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
  • application.yml 增加相干配置,很惯例的配置;
server:
  port: 9501
spring:
  profiles:
    active: dev
  application:
    name: micro-oauth2-api
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
management:
  endpoints:
    web:
      exposure:
        include: "*"
  • 创立一个测试接口,网关验证通过即可拜访;
/**
 * 测试接口
 * Created by macro on 2020/6/19.
 */
@RestController
public class HelloController {@GetMapping("/hello")
    public String hello() {return "Hello World.";}

}
  • 创立一个 LoginUserHolder 组件,用于从申请的 Header 中间接获取登录用户信息;
/**
 * 获取登录用户信息
 * Created by macro on 2020/6/17.
 */
@Component
public class LoginUserHolder {public UserDTO getCurrentUser(){
        // 从 Header 中获取用户信息
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servletRequestAttributes.getRequest();
        String userStr = request.getHeader("user");
        JSONObject userJsonObject = new JSONObject(userStr);
        UserDTO userDTO = new UserDTO();
        userDTO.setUsername(userJsonObject.getStr("user_name"));
        userDTO.setId(Convert.toLong(userJsonObject.get("id")));
        userDTO.setRoles(Convert.toList(String.class,userJsonObject.get("authorities")));
        return userDTO;
    }
}
  • 创立一个获取以后用户信息的接口。
/**
 * 获取登录用户信息接口
 * Created by macro on 2020/6/19.
 */
@RestController
@RequestMapping("/user")
public class UserController{

    @Autowired
    private LoginUserHolder loginUserHolder;

    @GetMapping("/currentUser")
    public UserDTO currentUser() {return loginUserHolder.getCurrentUser();
    }

}

性能演示

接下来咱们来演示下微服务零碎中的对立认证鉴权性能,所有申请均通过网关拜访。

  • 在此之前先启动咱们的 Nacos 和 Redis 服务,而后顺次启动 micro-oauth2-authmicro-oauth2-gatewaymicro-oauth2-api服务;

  • 应用明码模式获取 JWT 令牌,拜访地址:http://localhost:9201/auth/oauth/token

  • 应用获取到的 JWT 令牌拜访须要权限的接口,拜访地址:http://localhost:9201/api/hello

  • 应用获取到的 JWT 令牌拜访获取以后登录用户信息的接口,拜访地址:http://localhost:9201/api/user/currentUser

  • 当 JWT 令牌过期时,应用 refresh_token 获取新的 JWT 令牌,拜访地址:http://localhost:9201/auth/oauth/token

  • 应用没有拜访权限的 andy 账号登录,拜访接口时会返回如下信息,拜访地址:http://localhost:9201/api/hello

我的项目源码地址

https://github.com/macrozheng…

公众号

mall 我的项目全套学习教程连载中,关注公众号 第一工夫获取。

退出移动版