微服務(wù)權(quán)限終極解決方案

摘要

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

前置知識(shí)

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

  • Spring Cloud Gateway:新一代API網(wǎng)關(guān)服務(wù)
  • Spring Cloud Alibaba:Nacos 作為注冊(cè)中心和配置中心使用
  • 聽(tīng)說(shuō)你的JWT庫(kù)用起來(lái)特別扭,推薦這款賊好用的!

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

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

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

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

方案實(shí)現(xiàn)

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

micro-oauth2-auth

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

  • 在pom.xml中添加相關(guān)依賴(lài),主要是Spring Security、Oauth2、JWT、Redis相關(guān)依賴(lài);

<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-></artifactId> </dependency> </dependencies> 復(fù)制代碼

  • 在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: "*" 復(fù)制代碼

  • 使用keytool生成RSA證書(shū)jwt.jks,復(fù)制到resource目錄下,在JDK的bin目錄下使用如下命令即可;

keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks 復(fù)制代碼

  • 創(chuàng)建UserServiceImpl類(lèi)實(shí)現(xiàn)Spring Security的UserDetailsService接口,用于加載用戶(hù)信息;

/** * 用戶(hù)管理業(yè)務(wù)類(lèi) * 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; } } 復(fù)制代碼

  • 添加認(rèn)證服務(wù)相關(guān)配置Oauth2ServerConfig,需要配置加載用戶(hù)信息的服務(wù)UserServiceImpl及RSA的鑰匙對(duì)KeyPair;

/** * 認(rèn)證服務(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)容增強(qiáng)器 endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService) //配置加載用戶(hù)信息的服務(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下的證書(shū)中獲取秘鑰對(duì) KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); } } 復(fù)制代碼

  • 如果你想往JWT中添加自定義信息的話,比如說(shuō)登錄用戶(hù)的ID,可以自己實(shí)現(xiàn)TokenEnhancer接口;

/** * JWT內(nèi)容增強(qiáng)器 * 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<>(); //把用戶(hù)ID設(shè)置到JWT中 info.put("id", securityUser.getId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } } 復(fù)制代碼

  • 由于我們的網(wǎng)關(guān)服務(wù)需要RSA的公鑰來(lái)驗(yàn)證簽名是否合法,所以認(rèn)證服務(wù)需要有個(gè)接口把公鑰暴露出來(lái);

/** * 獲取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(); } } 復(fù)制代碼

  • 不要忘了還需要配置Spring Security,允許獲取公鑰接口的訪問(wèn);

/** * 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(); } } 復(fù)制代碼

  • 創(chuàng)建一個(gè)資源服務(wù)ResourceServiceImpl,初始化的時(shí)候把資源與角色匹配關(guān)系緩存到Redis中,方便網(wǎng)關(guān)服務(wù)進(jìn)行鑒權(quán)的時(shí)候獲取。

/** * 資源與角色匹配關(guān)系管理業(yè)務(wù)類(lèi) * 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); } } 復(fù)制代碼

micro-oauth2-gateway

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

  • 在pom.xml中添加相關(guān)依賴(lài),主要是Gateway、Oauth2和JWT相關(guān)依賴(lài);

<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> 復(fù)制代碼

  • 在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 #開(kāi)啟從注冊(cè)中心動(dòng)態(tài)創(chuàng)建路由的功能 lower-case-service-id: true #使用小寫(xiě)服務(wù)名,默認(rèn)是大寫(xiě) security: oauth2: resourceserver: jwt: jwk-set-uri: 'http://localhost:9401/rsa/publicKey' #配置RSA的公鑰訪問(wèn)地址 redis: database: 0 port: 6379 host: localhost password: secure: ignore: urls: #配置白名單路徑 - "/actuator/**" - "/auth/oauth/token" 復(fù)制代碼

  • 對(duì)網(wǎng)關(guān)服務(wù)進(jìn)行配置安全配置,由于Gateway使用的是WebFlux,所以需要使用@EnableWebFluxSecurity注解開(kāi)啟;

/** * 資源服務(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)//處理未認(rèn)證 .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); } } 復(fù)制代碼

  • 在WebFluxSecurity中自定義鑒權(quán)操作需要實(shí)現(xiàn)ReactiveAuthorizationManager接口;

/** * 鑒權(quán)管理器,用于判斷是否有資源的訪問(wè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中獲取當(dāng)前路徑可訪問(wèn)角色列表 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()); //認(rèn)證通過(guò)且角色匹配的用戶(hù)可訪問(wèn)當(dāng)前路徑 return mono .filter(Authentication::isAuthenticated) .flatMapIterable(Authentication::getAuthorities) .map(GrantedAuthority::getAuthority) .any(authorities::contains) .map(AuthorizationDecision::new) .defaultIfEmpty(new AuthorizationDecision(false)); } } 復(fù)制代碼

  • 這里我們還需要實(shí)現(xiàn)一個(gè)全局過(guò)濾器AuthGlobalFilter,當(dāng)鑒權(quán)通過(guò)后將JWT令牌中的用戶(hù)信息解析出來(lái),然后存入請(qǐng)求的Header中,這樣后續(xù)服務(wù)就不需要解析JWT令牌了,可以直接從請(qǐng)求的Header中獲取到用戶(hù)信息。

/** * 將登錄用戶(hù)的JWT轉(zhuǎn)化成用戶(hù)信息的全局過(guò)濾器 * 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中解析用戶(hù)信息并設(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; } } 復(fù)制代碼

micro-oauth2-api

最后我們搭建一個(gè)API服務(wù),它不會(huì)集成和實(shí)現(xiàn)任何安全相關(guān)邏輯,全靠網(wǎng)關(guān)來(lái)保護(hù)它。

  • 在pom.xml中添加相關(guān)依賴(lài),就添加了一個(gè)web依賴(lài);

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> 復(fù)制代碼

  • 在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: "*" 復(fù)制代碼

  • 創(chuàng)建一個(gè)測(cè)試接口,網(wǎng)關(guān)驗(yàn)證通過(guò)即可訪問(wèn);

/** * 測(cè)試接口 * Created by macro on 2020/6/19. */ @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello World."; } } 復(fù)制代碼

  • 創(chuàng)建一個(gè)LoginUserHolder組件,用于從請(qǐng)求的Header中直接獲取登錄用戶(hù)信息;

/** * 獲取登錄用戶(hù)信息 * Created by macro on 2020/6/17. */ @Component public class LoginUserHolder { public UserDTO getCurrentUser(){ //從Header中獲取用戶(hù)信息 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; } } 復(fù)制代碼

  • 創(chuàng)建一個(gè)獲取當(dāng)前用戶(hù)信息的接口。

/** * 獲取登錄用戶(hù)信息接口 * 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(); } } 復(fù)制代碼

功能演示

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

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

  • 使用密碼模式獲取JWT令牌,訪問(wèn)地址:http://localhost:9201/auth/oauth/token

  • 使用獲取到的JWT令牌訪問(wèn)需要權(quán)限的接口,訪問(wèn)地址:http://localhost:9201/api/hello

  • 使用獲取到的JWT令牌訪問(wèn)獲取當(dāng)前登錄用戶(hù)信息的接口,訪問(wèn)地址:http://localhost:9201/api/user/currentUser

  • 當(dāng)JWT令牌過(guò)期時(shí),使用refresh_token獲取新的JWT令牌,訪問(wèn)地址:http://localhost:9201/auth/oauth/token

  • 使用沒(méi)有訪問(wèn)權(quán)限的andy賬號(hào)登錄,訪問(wèn)接口時(shí)會(huì)返回如下信息,訪問(wèn)地址:http://localhost:9201/api/hello


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

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