在springcloud微服务架构下,如何进行统一的认证、鉴权,一直是大家非常关心的问题,下面对微服务架构下的认证和鉴权继续聊聊,一是自己的再次思考总结,二是希望对小伙伴有所帮助。
1、方案思路
在springcloud微服务中,认证授权服务统一负责认证(用户登录),网关服务统一负责校验认证和鉴权,其他业务服务负责处理自己的业务逻辑。安全相关功能只存在于认证授权服务和网关服务中,其他业务服务只是提供业务处理而没有任何安全相关功能。
2、技术组件
采用Nacos作为注册中心和配置中心;
采用Spring Security + Oauth2作为安全框架;
采用Spring Cloud Gateway作为网关服务;
采用JWT操作令牌;
其中,nacos版本为2.0.4+,基础组件版本如下:
<properties>
<spring-boot.version>2.7.0</spring-boot.version>
<spring-cloud.version>2021.0.4</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
<spring-cloud-starter-oauth2.version>2.2.5.RELEASE</spring-cloud-starter-oauth2.version>
<nimbus-jose-jwt.version>9.23</nimbus-jose-jwt.version>
</properties>
3、服务划分
auth:认证服务,负责对登录用户进行认证,整合Spring Security + Oauth2 + Jwt;
gateway:网关服务,负责对请求进行动态路由、校验认证和鉴权,整合Spring Security + Oauth2 + Jwt;
api:受保护的api服务,用户通过校验认证和鉴权后可以访问该服务,不整合Spring Security + Oauth2 + Jwt;
4、auth服务实例
下面搭建认证服务,将会整合security+oauth2+jwt,并且网关服务的鉴权功能需要依赖认证服务。在该服务中client信息和user信息都是存在于内存中,后续有需要可持久化到数据库中。
在pom.xml文件中添加依赖
<!--Spring Cloud & Alibaba -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在bootstrap.yml文件中添加配置
server:
port: 9401
spring:
profiles:
active: dev
application:
name: 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:
在jdk的bin目录下使用如下命令生成RSA证书jwt.jks,复制到resource目录下
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
添加认证服务相关配置类,需要配置加载用户信息及RSA的密钥对KeyPair
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final CustomUserDetailsService userDetailsService;
private final AuthenticationManager authenticationManager;
private final CustomTokenEnhancer customTokenEnhancer;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.secret(passwordEncoder.encode("1"))
.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(customTokenEnhancer);
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());
}
}
添加Spring Security配置类,允许获取公钥接口的访问
@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();
}
}
由于网关服务需要RSA的公钥来验证签名是否合法,所以认证服务需要有个接口将公钥暴露出来
@RestController
@RequiredArgsConstructor
public class KeyPairController {
private final 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();
}
}
添加一个资源类,初始化的时候把资源与角色匹配关系缓存的redis中,方便网关服务进行鉴权的时候获取
@Component
@RequiredArgsConstructor
public class ResourceService {
private Map<String, List<String>> resourceRolesMap;
private final 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);
}
}
其他相关类不再描述,可参考大佬博文。
5、gateway服务实例
下面搭建网关服务,它将作为oauth2的资源服务、客户端服务使用,对访问业务服务的请求进行统一的校验认证和鉴权,校验通过进行动态路由。
在pom.xml文件中添加依赖
<!--Spring Cloud & Alibaba -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</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>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
在bootstrap.yml文件中添加配置
server:
port: 9201
spring:
profiles:
active: dev
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes: #配置路由路径
- id: oauth2-api-route
uri: lb://api
predicates:
- Path=/api/**
filters:
- StripPrefix=1
- id: oauth2-auth-route
uri: lb://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注解开启
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final RestAccessDeniedHandler restAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
//自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
//对白名单路径,直接移除JWT请求头
http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
http.authorizeExchange()
.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
.anyExchange().access(authorizationManager)//鉴权管理器配置
.and().exceptionHandling()
.accessDeniedHandler(restAccessDeniedHandler)//处理未授权
.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接口
@Component
@RequiredArgsConstructor
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final 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中获取到用户信息
@Slf4j
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@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();
log.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;
}
}
其他相关类不再描述,可参考大佬博文。
6、api服务实例
下面搭建一个api服务,不整合任何安全框架,全靠网关来保护它
在pom.xml文件中添加依赖
<!--Spring Cloud & Alibaba -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在bootstrap.yml文件中添加配置
server:
port: 9601
spring:
profiles:
active: dev
application:
name: msbd-api
cloud:
nacos:
discovery:
server-addr: localhost:8848
添加一个测试接口
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello World.";
}
}
7、演示
下面演示微服务中认证、鉴权功能,所有请求均通过网关访问。需要依次启动nacos服务、redis服务、auth服务、gateway服务、api服务。
nacos服务界面如下
使用密码模式获取token令牌,访问地址:http://localhost:9201/auth/oauth/token
未携带token访问api服务的hello接口,访问地址:http://localhost:9201/api/hello
携带token访问api服务的hello接口,访问地址:http://localhost:9201/api/hello
携带无访问权限的token访问api服务的hello接口,访问地址:http://localhost:9201/api/hello
携带过期token访问api服务的hello接口,访问地址:http://localhost:9201/api/hello
在微服务中,不应该把重复校验认证和鉴权的功能集成到每个业务服务中,应该在认证服务做统一认证,在网关中做统一校验,这样才是优雅的微服务架构的安全解决方案。