共计 6889 个字符,预计需要花费 18 分钟才能阅读完成。
一、背景
在前一节咱们学习了 Spring Authorization Server 的应用,此处咱们简略的记录下 Spring 资源服务器的应用。
二、需要
资源服务器提供 2 个资源 ,userInfo
和 hello
。userInfo:
资源是受爱护的资源,须要 user.userInfo
权限才能够拜访。hello:
资源是公开资源,不要权限即可拜访。
三、剖析
1、如何验证资源服务器中拜访的令牌是无效的?
此处只思考 JWT 的令牌。
令牌是 受权服务器颁发 的,且进行了签名操作,因而资源服务器对令牌的验证,就须要受权服务器的 JWK 信息,此处能够配置 JwtDecoder
来实现,并且填写好jwk set uri
。
2、令牌是从申请中那个中央来的?
令牌能够从申请的 request header
或者 request query param
中获取,因而就须要配置 BearerTokenResolver
来实现。
3、令牌中的权限字段,默认会加上 SCOPE_
前缀,想去掉如何操作。
配置 JwtAuthenticationConverter
对象。
4、如果像向 JWT 的 claim 中增加值如何操作?
通过 JwtDecoder#setClaimSetConverter
来操作。此处也能够实现删除 claim 的中内容。
5、如何验证 JWT 是否非法?
通过 JwtDecoder#setJwtValidator
办法来操作。
6、如何设置从受权服务器获取 JWK 的超时工夫?
通过 JwkSetUriJwtDecoderBuilder#restOperations
来操作。
四、资源服务器认证流程
1、申请会被 BearerTokenAuthenticationFilter
拦截器拦挡,并从中解析出 token
进去,如果没有解析进去,则由下一个过滤器解决。解析进去则构建一个 BearerTokenAuthenticationToken
对象。
2、下一步将 HttpServletRequest
传递给 AuthenticationManagerResolver
对象,由它抉择出 AuthenticationManager
对象,而后将 BearerTokenAuthenticationToken
传递给 AuthenticationManager
对象进行认证。AuthenticationManager
对象的实现,取决于咱们的 token 对象是 JWT
还是 opaque token
3、验证失败
- 清空
SecurityContextHolder
对象。 - 交由
AuthenticationFailureHandler
对象解决。
4、验证胜利
- 将
Authentication
对象设置到SecurityContextHolder
中。 - 交由余下的过滤器持续解决。
五、实现资源服务器
1、引入 jar 包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、资源服务器配置
package com.huan.study.resource.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.MappedJwtClaimSetConverter;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collections;
/**
* 资源服务器配置
*
* @author huan.fu 2021/7/16 - 下午 5:00
*/
@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {private static final Logger log = LoggerFactory.getLogger(ResourceServerConfig.class);
@Autowired
private RestTemplateBuilder restTemplateBuilder;
@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()
// 对于 userInfo 这个 api 须要 s
.antMatchers("/userInfo").access("hasAuthority('user.userInfo')")
.and()
.oauth2ResourceServer()
.jwt()
// 解码 jwt 信息
.decoder(jwtDecoder(restTemplateBuilder))
// 将 jwt 信息转换成 JwtAuthenticationToken 对象
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.and()
// 从 request 申请那个中央中获取 token
.bearerTokenResolver(bearerTokenResolver())
// 此时是认证失败
.authenticationEntryPoint((request, response, exception) -> {
// oauth2 认证失败导致的,还有一种可能是非 oauth2 认证失败导致的,比方没有传递 token,然而拜访受权限爱护的办法
if (exception instanceof OAuth2AuthenticationException) {OAuth2AuthenticationException oAuth2AuthenticationException = (OAuth2AuthenticationException) exception;
OAuth2Error error = oAuth2AuthenticationException.getError();
log.info("认证失败, 异样类型:[{}], 异样:[{}]", exception.getClass().getName(), error);
}
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON.toString());
response.getWriter().write("{\"code\":-3,\"message\":\" 您无权限拜访 \"}");
})
// 认证胜利后,无权限拜访
.accessDeniedHandler((request, response, exception) -> {log.info("您无权限拜访, 异样类型:[{}]", exception.getClass().getName());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON.toString());
response.getWriter().write("{\"code\":-4,\"message\":\" 您无权限拜访 \"}");
})
;
}
/**
* 从 request 申请中那个中央获取到 token
*/
private BearerTokenResolver bearerTokenResolver() {DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
// 设置申请头的参数,即从这个申请头中获取到 token
bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.AUTHORIZATION);
bearerTokenResolver.setAllowFormEncodedBodyParameter(false);
// 是否能够从 uri 申请参数中获取 token
bearerTokenResolver.setAllowUriQueryParameter(false);
return bearerTokenResolver;
}
/**
* 从 JWT 的 scope 中获取的权限 勾销 SCOPE_ 的前缀
* 设置从 jwt claim 中那个字段获取权限
* 如果须要同多个字段中获取权限或者是通过 url 申请获取的权限,则须要本人提供 jwtAuthenticationConverter()这个办法的实现
*
* @return JwtAuthenticationConverter
*/
private JwtAuthenticationConverter jwtAuthenticationConverter() {JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
// 去掉 SCOPE_ 的前缀
authoritiesConverter.setAuthorityPrefix("");
// 从 jwt claim 中那个字段获取权限,模式是从 scope 或 scp 字段中获取
authoritiesConverter.setAuthoritiesClaimName("scope");
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
/**
* jwt 的解码器
*
* @return JwtDecoder
*/
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
// 受权服务器 jwk 的信息
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri("http://qq.com:8080/oauth2/jwks")
// 设置获取 jwk 信息的超时工夫
.restOperations(builder.setReadTimeout(Duration.ofSeconds(3))
.setConnectTimeout(Duration.ofSeconds(3))
.build())
.build();
// 对 jwt 进行校验
decoder.setJwtValidator(JwtValidators.createDefault());
// 对 jwt 的 claim 中增加值
decoder.setClaimSetConverter(MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("为 claim 中减少 key", custom -> "值"))
);
return decoder;
}
}
此处对资源服务器进行了很多的自定义操作,因而配置比拟长。
3、资源
1 个受爱护的资源和一个非受爱护的资源。
@RestController
public class UserController {
/**
* 这个是受爱护的资源,须要 user.userInfo 权限才能够拜访。*/
@GetMapping("userInfo")
public Map<String, Object> userInfo(@AuthenticationPrincipal Jwt principal) {return new HashMap<String, Object>(4) {{put("principal", principal);
put("userInfo", "获取用户信息");
}};
}
/**
* 非受权限爱护的资源
*/
@GetMapping("hello")
public String hello() {return "hello 不要须要受爱护的资源";}
}
六、测试
1、拜访非受爱护的资源
能够看到不须要 token 即能够拜访。
2、拜访受爱护的资源
1、先不必 token 拜访,能够看到是回绝的。
2、而后通过受权服务器生成一个 token,受权服务器为上一篇文章应用的受权服务器。
3、通过 token 拜访后,能够返现能够拜访资源了。
4、演示能够向 token 的 claim 中增加值。
5、演示 userInfo
是须要 user.userInfo
权限的。
七、残缺代码
1、受权服务器,为上篇文章中的受权服务器
https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/authorization-server
2、资源服务器
https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/resource-server
八、参考文档
1、https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2resourceserver