微服務(wù)權(quán)限終極解決方案,Spring Cloud Gateway + Oauth2 實現(xiàn)統(tǒng)一認證和鑒權(quán)!

最近發(fā)現(xiàn)了一個很好的微服務(wù)權(quán)限解決方案,可以通過認證服務(wù)進行統(tǒng)一認證,然后通過網(wǎng)關(guān)來統(tǒng)一校驗認證和鑒權(quán)。此方案為目前最新方案,僅支持Spring Boot 2.2.0、Spring Cloud Hoxton 以上版本,本文將詳細介紹該方案的實現(xiàn),希望對大家有所幫助!

SpringBoot實戰(zhàn)電商項目mall(35k+star)地址:https://github.com/macrozheng/mall

前置知識

我們將采用Nacos作為注冊中心,Gateway作為網(wǎng)關(guān),使用nimbus-jose-jwtJWT庫操作JWT令牌,對這些技術(shù)不了解的朋友可以看下下面的文章。

應(yīng)用架構(gòu)

我們理想的解決方案應(yīng)該是這樣的,認證服務(wù)負責認證,網(wǎng)關(guān)負責校驗認證和鑒權(quán),其他API服務(wù)負責處理自己的業(yè)務(wù)邏輯。安全相關(guān)的邏輯只存在于認證服務(wù)和網(wǎng)關(guān)服務(wù)中,其他服務(wù)只是單純地提供服務(wù)而沒有任何安全相關(guān)邏輯。

相關(guān)服務(wù)劃分:

  • micro-oauth2-gateway:網(wǎng)關(guān)服務(wù),負責請求轉(zhuǎn)發(fā)和鑒權(quán)功能,整合Spring Security+Oauth2;
  • micro-oauth2-auth:Oauth2認證服務(wù),負責對登錄用戶進行認證,整合Spring Security+Oauth2;
  • micro-oauth2-api:受保護的API服務(wù),用戶鑒權(quán)通過后可以訪問該服務(wù),不整合Spring Security+Oauth2。

方案實現(xiàn)

下面介紹下這套解決方案的具體實現(xiàn),依次搭建認證服務(wù)、網(wǎng)關(guān)服務(wù)和API服務(wù)。

micro-oauth2-auth

我們首先來搭建認證服務(wù),它將作為Oauth2的認證服務(wù)使用,并且網(wǎng)關(guān)服務(wù)的鑒權(quán)功能也需要依賴它。

  • pom.xml中添加相關(guān)依賴,主要是Spring Security、Oauth2、JWT、Redis相關(guān)依賴;
<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中添加相關(guān)配置,主要是Nacos和Redis相關(guān)配置;
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,復(fù)制到resource目錄下,在JDK的bin目錄下使用如下命令即可;
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks

  • 創(chuàng)建UserServiceImpl類實現(xiàn)Spring Security的UserDetailsService接口,用于加載用戶信息;
/**
 * 用戶管理業(yè)務(wù)類
 * 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;
    }

}

  • 添加認證服務(wù)相關(guān)配置Oauth2ServerConfig,需要配置加載用戶信息的服務(wù)UserServiceImpl及RSA的鑰匙對KeyPair
/**
 * 認證服務(wù)器配置
 * 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的內(nèi)容增強器
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService) //配置加載用戶信息的服務(wù)
                .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,可以自己實現(xiàn)TokenEnhancer接口;
/**
 * JWT內(nèi)容增強器
 * 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設(shè)置到JWT中
        info.put("id", securityUser.getId());
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}

  • 由于我們的網(wǎng)關(guān)服務(wù)需要RSA的公鑰來驗證簽名是否合法,所以認證服務(wù)需要有個接口把公鑰暴露出來;
/**
 * 獲取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();
    }

}

  • 創(chuàng)建一個資源服務(wù)ResourceServiceImpl,初始化的時候把資源與角色匹配關(guān)系緩存到Redis中,方便網(wǎng)關(guān)服務(wù)進行鑒權(quán)的時候獲取。
/**
 * 資源與角色匹配關(guān)系管理業(yè)務(wù)類
 * 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

接下來我們就可以搭建網(wǎng)關(guān)服務(wù)了,它將作為Oauth2的資源服務(wù)、客戶端服務(wù)使用,對訪問微服務(wù)的請求進行統(tǒng)一的校驗認證和鑒權(quán)操作。

  • pom.xml中添加相關(guān)依賴,主要是Gateway、Oauth2和JWT相關(guān)依賴;
<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中添加相關(guān)配置,主要是路由規(guī)則的配置、Oauth2中RSA公鑰的配置及路由白名單的配置;
server:
  port: 9201
spring:
  profiles:
    active: dev
  application:
    name: micro-oauth2-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes: #配置路由規(guī)則
        - 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 #開啟從注冊中心動態(tài)創(chuàng)建路由的功能
          lower-case-service-id: true #使用小寫服務(wù)名,默認是大寫
  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"

  • 對網(wǎng)關(guān)服務(wù)進行配置安全配置,由于Gateway使用的是WebFlux,所以需要使用@EnableWebFluxSecurity注解開啟;
/**
 * 資源服務(wù)器配置
 * 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)//鑒權(quán)管理器配置
                .and().exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)//處理未授權(quán)
                .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中自定義鑒權(quán)操作需要實現(xiàn)ReactiveAuthorizationManager接口;
/**
 * 鑒權(quán)管理器,用于判斷是否有資源的訪問權(quán)限
 * 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));
    }

}

  • 這里我們還需要實現(xiàn)一個全局過濾器AuthGlobalFilter,當鑒權(quán)通過后將JWT令牌中的用戶信息解析出來,然后存入請求的Header中,這樣后續(xù)服務(wù)就不需要解析JWT令牌了,可以直接從請求的Header中獲取到用戶信息。
/**
 * 將登錄用戶的JWT轉(zhuǎn)化成用戶信息的全局過濾器
 * 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中解析用戶信息并設(shè)置到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服務(wù),它不會集成和實現(xiàn)任何安全相關(guān)邏輯,全靠網(wǎng)關(guān)來保護它。

  • pom.xml中添加相關(guān)依賴,就添加了一個web依賴;
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

  • application.yml添加相關(guān)配置,很常規(guī)的配置;
server:
  port: 9501
spring:
  profiles:
    active: dev
  application:
    name: micro-oauth2-api
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
management:
  endpoints:
    web:
      exposure:
        include: "*"

  • 創(chuàng)建一個測試接口,網(wǎng)關(guān)驗證通過即可訪問;
/**
 * 測試接口
 * Created by macro on 2020/6/19.
 */
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello World.";
    }

}

  • 創(chuàng)建一個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;
    }
}

  • 創(chuàng)建一個獲取當前用戶信息的接口。
/**
 * 獲取登錄用戶信息接口
 * 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();
    }

}

功能演示

接下來我們來演示下微服務(wù)系統(tǒng)中的統(tǒng)一認證鑒權(quán)功能,所有請求均通過網(wǎng)關(guān)訪問。

  • 在此之前先啟動我們的Nacos和Redis服務(wù),然后依次啟動micro-oauth2-auth、micro-oauth2-gatewaymicro-oauth2-api服務(wù);
image
image
image
image
image
image

項目源碼地址

https://github.com/macrozheng/springcloud-learning/tree/master/micro-oauth2

本文 GitHub https://github.com/macrozheng/mall-learning 已經(jīng)收錄,歡迎大家Star!

作者:夢想de星空
鏈接:http://www.itdecent.cn/p/1e974fc91f74

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容