概述
弹簧云网关是建在弹簧生态系统顶部的API网关。它建在Spring Boot,Spring WebFlux和Project Reactor的顶部。
在本节中,您将使用Spring Cloud Gateway将请求路由到Servlet API服务。
您将在本文中学到什么:
- OpenID连接身份验证 - 用于用户身份验证。
- 令牌继电器 - 春季云网关API网关充当客户端,并将令牌转发到资源请求。
先决条件:
- Java 8+
- mysql
- redis
OpenID连接身份验证
OpenID Connect定义了基于OAUTH2授权代码流的用户身份验证机制。下图显示了Spring Cloud Gateway和授权服务之间身份验证的完整过程。为了清楚起见,已经省略了一些参数。
建立授权服务
在本节中,我们将使用Spring Authorization Server来构建支持OAUTH2和OPENID CONNECT协议的授权服务。同时,我们将使用RBAC0基本权限模型来控制访问权限。此授权服务还支持GitHub第三方登录作为OAuth2客户端。
相关数据库表结构
我们为本文创建了一个基本的RBAC0权限模型,并提供了OAUTH2授权服务和OAUTH2客户端的持久存储所需的表结构。 oauth2_client_role 表定义了外部系统角色和与本地平台角色的映射关系。可以获得与创建相关表和初始化数据有关的SQL语句。
角色描述
默认情况下,本节中的授权服务提供了两个角色,具有以下角色属性和访问权限:
阅读 | 写 | |
---|---|---|
roun_admin | ||
roun_operation |
Maven依赖性
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
配置
首先,让我们从application.yml
配置开始,在其中指定端口号和mysql连接配置:
server:
port: 8080
spring:
datasource:
druid:
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/oauth2server?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: <<username>> # Modify username
password: <<password>> # Modify password
接下来,我们将创建AuthorizationServerConfig
来为OAuth2和OIDC配置所需的Bean。首先,我们将添加OAuth2客户端信息并将其持续到数据库:
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId("relive-messaging-oidc")
.clientId("relive-client")
.clientSecret("{noop}relive-client")
.clientAuthenticationMethods(s -> {
s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
})
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8070/login/oauth2/code/messaging-gateway-oidc")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope(OidcScopes.EMAIL)
.scope("read")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(false)
.requireProofKey(false)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
.accessTokenTimeToLive(Duration.ofSeconds(30 * 60))
.refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))
.reuseRefreshTokens(true)
.build())
.build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
第二,我们将创建授权过程中所需的持久性容器类:
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
授权服务器需要其令牌的签名密钥,因此让我们生成一个2048-BYTE RSA密钥:
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
static class Jwks {
private Jwks() {
}
public static RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
}
static class KeyGeneratorUtils {
private KeyGeneratorUtils() {
}
static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
接下来,我们将创建用于OAuth2授权的SecurityFilterChain
。 SecurityFilterChain
是Spring Security提供的过滤链。
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
//配置OIDC
authorizationServerConfigurer.oidc(Customizer.withDefaults());
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
return http.requestMatcher(endpointsMatcher)
.authorizeRequests((authorizeRequests) -> {
((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl) authorizeRequests.anyRequest()).authenticated();
}).csrf((csrf) -> {
csrf.ignoringRequestMatchers(new RequestMatcher[]{endpointsMatcher});
}).apply(authorizationServerConfigurer)
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.exceptionHandling(exceptions -> exceptions.
authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
.apply(authorizationServerConfigurer)
.and()
.build();
}
上面,我们配置了OAuth2和OpenID Connect的默认配置,并将身份验证请求重定向到登录页面。同时,我们还启用了Spring Security提供的OAuth2资源服务配置。此配置用于保护OpenID Connect /userInfo 用户信息端点。
启用Spring Security OAUTH2资源服务配置时,我们指定JWT验证,因此我们需要在application.yml
中指定jwk-set-uri
或声明添加JwtDecoder
。在这里,我们使用声明性配置:
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
接下来,我们将自定义访问令牌。在此示例中,我们使用RBAC0权限模型,因此我们将当前用户角色的权限代码添加到access_token
:
@Configuration(proxyBeanMethods = false)
public class AccessTokenCustomizerConfig {
@Autowired
RoleRepository roleRepository;
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
return (context) -> {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
context.getClaims().claims(claim -> {
claim.put("authorities", roleRepository.findByRoleCode(context.getPrincipal().getAuthorities().stream()
.map(GrantedAuthority::getAuthority).findFirst().orElse("ROLE_OPERATION"))
.getPermissions().stream().map(Permission::getPermissionCode).collect(Collectors.toSet()));
});
}
};
}
}
rolerepository是角色表的持久性层对象。在此示例中,我们使用JPA框架,并且将在本文中显示相关代码。如果您不熟悉JPA,则可以使用mybatis。
下面我们将配置授权服务表格身份验证方法。
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(withDefaults())
...
return http.build();
}
接下来,我们将创建一个实现UserDetailsService
的JdbcUserDetailsService
,该UserDetailsService
用于在身份验证过程中查找已登录用户的密码和权限信息。如果您对为什么需要实现UserDetailsService
感兴趣,可以检查UsernamePasswordAuthenticationFilter
-> ProviderManager
-> DaoAuthenticationProvider
的源代码。在DaoAuthenticationProvider
中,用户信息是通过调用UserDetailsservice#loadUserByUsername(字符串用户名)获得的。
@RequiredArgsConstructor
public class JdbcUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
com.relive.entity.User user = userRepository.findUserByUsername(username);
if (ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("user is not found");
}
if (CollectionUtils.isEmpty(user.getRoleList())) {
throw new UsernameNotFoundException("role is not found");
}
Set<SimpleGrantedAuthority> authorities = user.getRoleList().stream().map(Role::getRoleCode)
.map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
我们将其注入春季:
@Bean
UserDetailsService userDetailsService(UserRepository userRepository) {
return new JdbcUserDetailsService(userRepository);
}
尝试访问未经验证的界面时,用户将被定向到登录页面并提示输入其用户名和密码,如下所示:
用户通常需要使用不同组织提供和托管的多个平台。这些用户可能需要为每个平台使用特定(和不同的)凭据。当用户具有许多不同的凭据时,他们通常会忘记登录凭据。
联合身份验证用于使用外部系统对用户进行身份验证。这可以与Google,GitHub或任何其他身份提供商一起使用。在这里,我将使用github进行用户身份验证和数据同步管理。
GitHub身份认证
首先,我们将配置GitHub客户端信息,您只需要更改 clientId 和 clientsecret 即可。其次,我们将使用Spring Security Persistence OAuth2 Client中引入的JdbcClientRegistrationRepository
持久层容器类将GitHub客户端信息存储在数据库中。
@Bean
ClientRegistrationRepository clientRegistrationRepository(JdbcTemplate jdbcTemplate) {
JdbcClientRegistrationRepository jdbcClientRegistrationRepository = new JdbcClientRegistrationRepository(jdbcTemplate);
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("github")
.clientId("123456")
.clientSecret("123456")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
.scope(new String[]{"read:user"})
.authorizationUri("https://github.com/login/oauth/authorize")
.tokenUri("https://github.com/login/oauth/access_token")
.userInfoUri("https://api.github.com/user")
.userNameAttributeName("login")
.clientName("GitHub").build();
jdbcClientRegistrationRepository.save(clientRegistration);
return jdbcClientRegistrationRepository;
}
接下来,我们将实例化OAuth2AuthorizedClientService
和OAuth2AuthorizedClientRepository
:
- oauth2authorizedClientservice :负责在Web请求之间持续oauth2authorizedclient。
- oauth2authorizedClientRepository :用于在请求之间保存和坚持授权客户端。
@Bean
OAuth2AuthorizedClientService authorizedClientService(
JdbcTemplate jdbcTemplate,
ClientRegistrationRepository clientRegistrationRepository) {
return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
}
@Bean
OAuth2AuthorizedClientRepository authorizedClientRepository(
OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
对于使用GitHub登录的每个用户,我们需要分配平台角色来控制他们可以访问的资源。在这里,我们将创建pertiatemappatoAuth2userService类以授予用户角色:
@RequiredArgsConstructor
public class AuthorityMappingOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
private final OAuth2ClientRoleRepository oAuth2ClientRoleRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
DefaultOAuth2User oAuth2User = (DefaultOAuth2User) delegate.loadUser(userRequest);
Map<String, Object> additionalParameters = userRequest.getAdditionalParameters();
Set<String> role = new HashSet<>();
if (additionalParameters.containsKey("authority")) {
role.addAll((Collection<? extends String>) additionalParameters.get("authority"));
}
if (additionalParameters.containsKey("role")) {
role.addAll((Collection<? extends String>) additionalParameters.get("role"));
}
Set<SimpleGrantedAuthority> mappedAuthorities = role.stream()
.map(r -> oAuth2ClientRoleRepository.findByClientRegistrationIdAndRoleCode(userRequest.getClientRegistration().getRegistrationId(), r))
.map(OAuth2ClientRole::getRole).map(Role::getRoleCode).map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
//When no client role is specified, the least privilege ROLE_OPERATION is given by default
if (CollectionUtils.isEmpty(mappedAuthorities)) {
mappedAuthorities = new HashSet<>(
Collections.singletonList(new SimpleGrantedAuthority("ROLE_OPERATION")));
}
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
return new DefaultOAuth2User(mappedAuthorities, oAuth2User.getAttributes(), userNameAttributeName);
}
}
我们可以看到权限信息是从权威和角色属性中获得的,并且映射到此平台的角色属性将通过oauth2clientrolererepository搜索。
注意:
authority
和role
是平台的自定义属性,与OAuth2协议无关,开放ID Connect协议。在生产环境中,您可以与外部系统进行协商,以同意传输权限信息的属性。
OAuth2ClientRoleRepository
是jpa。
对于尚未提前获得的映射角色信息,我们将分配默认的ROLE_OPERATION
最低许可角色。在此示例中,使用GitHub登录的用户也将分配ROLE_OPERATION
角色。
对于成功使用GitHub进行身份验证并首次登录的用户,我们将获取他们的用户信息并将其持续到user
表。在这里,我们将实现AuthenticationSuccessHandler
并添加用户持久性逻辑。
public final class SavedUserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler();
private Consumer<OAuth2User> oauth2UserHandler = (user) -> {
};
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication instanceof OAuth2AuthenticationToken) {
if (authentication.getPrincipal() instanceof OAuth2User) {
this.oauth2UserHandler.accept((OAuth2User) authentication.getPrincipal());
}
}
this.delegate.onAuthenticationSuccess(request, response, authentication);
}
public void setOauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler) {
this.oauth2UserHandler = oauth2UserHandler;
}
}
我们将通过setOauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler)
方法将UserRepositoryOAuth2UserHandler
注入SavedUserAuthenticationSuccessHandler
。 UserRepositoryOAuth2UserHandler
定义了特定的持久性层操作:
@Component
@RequiredArgsConstructor
public final class UserRepositoryOAuth2UserHandler implements Consumer<OAuth2User> {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@Override
public void accept(OAuth2User oAuth2User) {
DefaultOAuth2User defaultOAuth2User = (DefaultOAuth2User) oAuth2User;
if (this.userRepository.findUserByUsername(oAuth2User.getName()) == null) {
User user = new User();
user.setUsername(defaultOAuth2User.getName());
Role role = roleRepository.findByRoleCode(defaultOAuth2User.getAuthorities()
.stream().map(GrantedAuthority::getAuthority).findFirst().orElse("ROLE_OPERATION"));
user.setRoleList(Arrays.asList(role));
userRepository.save(user);
}
}
}
我们获得了defaultOAuth2User.getAuthorities()
映射的角色信息,并使用用户信息将其存储在数据库中。
用户介绍和rolerepository是持久的容器类。
最后,我们将OAuth2登录配置添加到SecurityFilterchain:
@Autowired
UserRepositoryOAuth2UserHandler userHandler;
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.oauth2Login(oauth2login -> {
SavedUserAuthenticationSuccessHandler successHandler = new SavedUserAuthenticationSuccessHandler();
successHandler.setOauth2UserHandler(userHandler);
oauth2login.successHandler(successHandler);
});
...
return http.build();
}
创建一个弹簧云网关应用程序
在本节中,我们将在春季云网关中使用Spring Security OAuth2 Login启用OpenID Connect Authentication并继电器访问令牌到下游服务。
Maven依赖性
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.76.Final</version>
</dependency>
配置
首先,我们将以下属性添加到application.yml
:
server:
port: 8070
servlet:
session:
cookie:
name: GATEWAY-CLIENT
在这里,cookie名称被指定为GATEWAY-CLIENT
,以避免与授权服务相冲突。
通过Spring Cloud Gateway到达资源服务器的路线:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: resource-server
uri: http://127.0.0.1:8090
predicates:
Path=/resource/**
filters:
- TokenRelay
Tokenrelay过滤器提取了存储在用户会话中的访问令牌,并将其添加为授权标题,以在即将推出的请求中。这允许下游服务对请求进行身份验证。
我们将在application.yml
中添加oauth2客户端信息:
spring:
security:
oauth2:
client:
registration:
messaging-gateway-oidc:
provider: gateway-client-provider
client-id: relive-client
client-secret: relive-client
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope:
- openid
- profile
client-name: messaging-gateway-oidc
provider:
gateway-client-provider:
authorization-uri: http://127.0.0.1:8080/oauth2/authorize
token-uri: http://127.0.0.1:8080/oauth2/token
jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
user-info-uri: http://127.0.0.1:8080/userinfo
user-name-attribute: sub
OpenID Connect使用特殊的范围值openid
来控制对UserInfo端点的访问,而其他信息与上一节中授权服务的客户端注册信息参数一致。
Spring Security拦截了未经验证的请求,并在授权服务器上执行身份验证。为简单起见,CSRF被禁用。
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(authorize -> authorize
.anyExchange().authenticated()
)
.oauth2Login(withDefaults())
.cors().disable();
return http.build();
}
}
在Spring Cloud Gateway完成OpenID Connect Authentication后,用户信息和令牌存储在会话中,因此我们添加spring-session-data-redis
以提供REDIS支持的分布式会话功能,并在application.yml
中添加以下配置:
spring:
session:
store-type: redis
redis:
flush-mode: on_save
namespace: gateway:session
redis:
host: localhost
port: 6379
password: 123456
基于上面的示例,我们使用Spring Cloud Gateway驱动身份验证并知道如何对用户进行身份验证,获得令牌(用户同意后),但不要通过Gateway进行身份验证/授权请求(Spring Gateway Cloud不是受众群体目标访问令牌)。这种方法背后的原因是某些服务受到保护,而另一些服务是公开的。即使在一项服务中,有时还需要保护一些端点,而不是每个端点。这就是为什么我将请求身份验证/授权留给特定服务。
当然,从实施角度来看,它并不能阻止我们在Spring Cloud Gateway中执行身份验证/授权,这只是一个选择。
资源服务器
在本节中,我们使用Spring Boot设置简单的资源服务器。该示例为资源服务器提供了两个API接口,并通过Spring Security OAuth2 Resource Server配置保护它们。
Maven依赖性
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.6.7</version>
</dependency>
配置
在application.yml
中添加jwk-set-uri
属性:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://127.0.0.1:8080
jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
server:
port: 8090
创建ResourceServerConfig
类以配置Spring Security Security Module,并使用@EnableMethodSecurity
注释来启用基于注释的安全性。
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer()
.jwt();
return http.build();
}
}
Spring Security Resource Server使用索赔中的scope
和scp
属性默认情况下验证令牌并提取权限信息。
Spring Security
JwtAuthenticationProvider
通过JwtAuthenticationConverter
辅助转换器提取权限信息和其他信息。
但是,在此示例中,内部权限使用当局属性,因此我们使用JwtAuthenticationConverter
手动提取权限。
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
grantedAuthoritiesConverter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
在这里,我们将权限属性指定为authorities
,并完全删除权限前缀。
最后,我们将在示例中创建用于测试的API接口,并使用@PreAuthorize
保护界面,只能通过相应的权限访问:
@RestController
public class ArticleController {
List<String> article = new ArrayList<String>() {{
add("article1");
add("article2");
}};
@PreAuthorize("hasAuthority('read')")
@GetMapping("/resource/article/read")
public Map<String, Object> read(@AuthenticationPrincipal Jwt jwt) {
Map<String, Object> result = new HashMap<>(2);
result.put("principal", jwt.getClaims());
result.put("article", article);
return result;
}
@PreAuthorize("hasAuthority('write')")
@GetMapping("/resource/article/write")
public String write(@RequestParam String name) {
article.add(name);
return "success";
}
}
测试我们的应用程序
我们启动服务后,我们在浏览器中访问http://127.0.0.1:8070/resource/article/read,我们将被重定向到授权服务登录页面,如下所示:
我们输入用户名和密码(管理员/密码)后,我们将获得请求响应信息:
管理员用户的角色是ROLE_ADMIN
,因此我们尝试请求http://127.0.0.1:8070/resource/article/write?name=article3
登录后,我们也访问http://127.0.0.1:8070/resource/article/read,但是这次我们使用github登录,响应信息如下所示:
我们可以看到响应信息表明用户已切换到您的github用户名。
使用GitHub登录的用户默认为ROLE_OPERATION
角色,而ROLE_OPERATION
无法访问http://127.0.0.1:8070/resource/article/write?name=article3。让我们尝试测试它:
结果表明我们的请求被拒绝,带有403状态代码,表明我们没有访问权限。
结论
在本文中,您学会了如何使用弹簧云网关使用OAuth2保护微服务。在示例中,浏览器cookie仅存储会话ID,而JWT访问令牌不暴露于浏览器,而是在服务中内部流动。这样,我们会体验JWT的优势,并使用Cookie-Session来弥补JWT的缺点,例如当我们需要实现强制用户注销功能时。
一如既往,本文中使用的源代码可用on GitHub。
感谢您的阅读! ð