单点登录学习资料
单点登录原理与简单实现
Spring Security OAuth2:整合jwt
Spring Security OAuth2:SSO单点登录
spring-security-oauth2 系列笔记目录导航
SpringSecurity OAuth2单点登录和登出的实现
基于SpringSecurity OAuth2实现单点登录
@EnableOAuth2Sso
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client // 开启了@EnableOAuth2Client 注解
@EnableConfigurationProperties(OAuth2SsoProperties.class) // 开启了OAuth2SsoProperties
@Import({ OAuth2SsoDefaultConfiguration.class, // @Import注解引入了3个配置类
OAuth2SsoCustomConfiguration.class,
ResourceServerTokenServicesConfiguration.class })
public @interface EnableOAuth2Sso {
}
1. @EnableOAuth2Client
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(OAuth2ClientConfiguration.class) // 导入了这个配置类
public @interface EnableOAuth2Client {
}
OAuth2ClientConfiguration
@Configuration
public class OAuth2ClientConfiguration {
// 引入了OAuth2ClientContextFilter,
// 如果后面过滤器处理过程抛出UserRedirectRequiredException异常, 这个过滤器会让用户重定向
// 而这个过滤器则又会在OAuth2RestOperationsConfiguration配置类中配置成FilterRegistrationBean加入到web容器中
// (当然即便不这样配置, 它本来就可以生效)
@Bean
public OAuth2ClientContextFilter oauth2ClientContextFilter() {
OAuth2ClientContextFilter filter = new OAuth2ClientContextFilter();
return filter;
}
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
protected AccessTokenRequest accessTokenRequest(
// 这个request的解析在: BeanExpressionContext#getObject(key),
// 然后委托给了AbstractRequestAttributesScope#resolveContextualObject(key)
// 它会根据一个key来获取对象, 然后这个scope就交给了ServletRequestAttributes#resolveReference(key)
// 从而拿到了当前的request对象
@Value("#{request.parameterMap}") Map<String, String[]> parameters,
@Value("#{request.getAttribute('currentUri')}") String currentUri)
{
DefaultAccessTokenRequest request = new DefaultAccessTokenRequest(parameters);
request.setCurrentUri(currentUri);
return request;
}
@Configuration
protected static class OAuth2ClientContextConfiguration {
@Resource
@Qualifier("accessTokenRequest")
private AccessTokenRequest accessTokenRequest;
@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
public OAuth2ClientContext oauth2ClientContext() {
return new DefaultOAuth2ClientContext(accessTokenRequest);
}
}
}
OAuth2ClientContextFilter
public class OAuth2ClientContextFilter implements Filter, InitializingBean {
public static final String CURRENT_URI = "currentUri";
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public void afterPropertiesSet() throws Exception {
Assert.notNull(redirectStrategy);
}
public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain chain){
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
request.setAttribute(CURRENT_URI, calculateCurrentUri(request));
try {
chain.doFilter(servletRequest, servletResponse);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// 从异常栈中获取到 UserRedirectRequiredException的异常,
// 如果发现了该类型的异常, 则将用户重定向到单点登录登录页
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer.getFirstThrowableOfType(UserRedirectRequiredException.class, causeChain);
if (redirect != null) {
// 让用户重定向
redirectUser(redirect, request, response);
} else {
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new NestedServletException("Unhandled exception", ex);
}
}
}
protected void redirectUser(UserRedirectRequiredException e,
HttpServletRequest request,
HttpServletResponse response)throws IOException {
// 异常的 redirectUri
String redirectUri = e.getRedirectUri();
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(redirectUri);
// 异常的 requestParams
Map<String, String> requestParams = e.getRequestParams();
for (Map.Entry<String, String> param : requestParams.entrySet()) {
builder.queryParam(param.getKey(), param.getValue());
}
// 异常的 state
if (e.getStateKey() != null) {
builder.queryParam("state", e.getStateKey());
}
// 重定向
this.redirectStrategy.sendRedirect(request, response, builder.build().encode().toUriString());
}
protected String calculateCurrentUri(HttpServletRequest request) throws UnsupportedEncodingException {
ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromRequest(request);
// 将路径请求参数中的+改为%20
String queryString = request.getQueryString();
boolean legalSpaces = queryString != null && queryString.contains("+");
if (legalSpaces) {
builder.replaceQuery(queryString.replace("+", "%20"));
}
UriComponents uri = null;
try {
// 移除code请求参数
uri = builder.replaceQueryParam("code").build(true);
} catch (IllegalArgumentException ex) {
return null;
}
// 恢复 + 号
String query = uri.getQuery();
if (legalSpaces) {
query = query.replace("%20", "+");
}
return ServletUriComponentsBuilder.fromUri(uri.toUri())
.replaceQuery(query).build().toString();
}
}
OAuth2SsoProperties
@ConfigurationProperties(prefix = "security.oauth2.sso")
public class OAuth2SsoProperties {
public static final String DEFAULT_LOGIN_PATH = "/login";
// sso登录地址
private String loginPath = DEFAULT_LOGIN_PATH;
}
2. OAuth2SsoDefaultConfiguration
当@EnableSso标注的类没有继承自WebSecurityConfigurerAdapter时,下面的配置类才会生效,那么下面这个配置类生效意味着什么呢?我们注意到下面这个配置了继承了WebSecurityConfigurerAdapter,那么就会往FilterChainProxy的filterChains属性中添加一个过滤器,这个过滤器经过security常规的配置后,会在其中添加一个OAuth2ClientAuthenticationProcessingFilter 过滤器
@Configuration
@Conditional(NeedsWebSecurityCondition.class) // 使用了@EnableSso注解的配置类没有继承自WebSecurityConfigurerAdapter,生效
public class OAuth2SsoDefaultConfiguration extends WebSecurityConfigurerAdapter {
private final ApplicationContext applicationContext;
// 构造器会自动注入applicationContext
public OAuth2SsoDefaultConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有的请求都需要认证
http.antMatcher("/**").authorizeRequests().anyRequest().authenticated();
// 使用SsoSecurityConfigurer配置HttpSecurity
new SsoSecurityConfigurer(this.applicationContext).configure(http);
}
}
SsoSecurityConfigurer
看看如果我们的@EnableSso没有加到WebSecurityConfigurer的子类上时,security会如何配置?
class SsoSecurityConfigurer {
private ApplicationContext applicationContext;
SsoSecurityConfigurer(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public void configure(HttpSecurity http) throws Exception {
// 获取OAuth2SsoProperties属性配置
OAuth2SsoProperties sso = this.applicationContext.getBean(OAuth2SsoProperties.class);
// http就是HttpSecurity
// HttpSecurity最后会构建出DefaultSecurityFilterChain对象
// 而SecurityConfigurer就是往DefaultSecurityFilterChain添加Filter过滤器的OAuth2ClientAuthenticationConfigurer会添加OAuth2ClientAuthenticationProcessingFilter
// 所以这个过滤器就特别关键了
http.apply(new OAuth2ClientAuthenticationConfigurer(oauth2SsoFilter(sso)));
// 添加认证入口
addAuthenticationEntryPoint(http, sso);
}
private void addAuthenticationEntryPoint(HttpSecurity http, OAuth2SsoProperties sso)throws Exception {
// 获取到处理异常的配置器
ExceptionHandlingConfigurer<HttpSecurity> exceptions = http.exceptionHandling();
// 内容协商策略
ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class);
if (contentNegotiationStrategy == null) {
// 不指定, 则使用请求头协商策略
contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
}
// 匹配一下媒体类型的请求, 当用户未认证时(或者是通过rememberMe认证的但是没有权限访问), 交给该认证入口点处理
// LoginUrlAuthenticationEntryPoint会让用户重定向到指定的url
MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_XHTML_XML,
new MediaType("image", "*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
exceptions.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint(sso.getLoginPath()),
preferredMatcher);
// 如果是ajax请求, 则返回401未授权相应状态码
exceptions.defaultAuthenticationEntryPointFor(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
}
// 这个过滤器比较关键, 它继承自AbstractAuthenticationProcessingFilter,这说明它也是作认证的
// restTemplate属于OAuth2RestTemplate, 它用来根据授权模式及参数获取OAuth2授权的(获取accessToken)
// tokenServices属于ResourceServerTokenServices, 用来根据accessToken获取用户
// 所以这个过滤器的工作流程就是:
// 第一步: 先使用OAuth2RestTemplate获取accessToken访问令牌
// 第二步: 拿到访问令牌后, 使用ResourceServerTokenServices读取访问令牌, 从而获取用户信息, 设置到SecurityContext上下文中
// 从代码上看, 容器中必须要配置这2个bean噢(OAuth2RestTemplate、ResourceServerTokenServices)
private OAuth2ClientAuthenticationProcessingFilter oauth2SsoFilter(OAuth2SsoProperties sso) {
OAuth2RestOperations restTemplate = this.applicationContext
.getBean(UserInfoRestTemplateFactory.class).getUserInfoRestTemplate();
ResourceServerTokenServices tokenServices = this.applicationContext
.getBean(ResourceServerTokenServices.class);
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
sso.getLoginPath());
filter.setRestTemplate(restTemplate);
filter.setTokenServices(tokenServices);
filter.setApplicationEventPublisher(this.applicationContext);
return filter;
}
private static class OAuth2ClientAuthenticationConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {private OAuth2ClientAuthenticationProcessingFilter filter;
OAuth2ClientAuthenticationConfigurer(OAuth2ClientAuthenticationProcessingFilter filter) {
this.filter = filter;
}
@Override
public void configure(HttpSecurity builder) throws Exception {
// 这个OAuth2ClientAuthenticationProcessingFilter 过滤器要添加到AbstractPreAuthenticatedProcessingFilter的前面
// 并且到是到配置的时候, 再去获取的会话认证策略, 并添加到HttpSecurity中
OAuth2ClientAuthenticationProcessingFilter ssoFilter = this.filter;
ssoFilter.setSessionAuthenticationStrategy(builder.getSharedObject(SessionAuthenticationStrategy.class));
builder.addFilterAfter(ssoFilter,AbstractPreAuthenticatedProcessingFilter.class);
}
}
}
这样,我们就看到了,在默认情况下,security其实就是往过滤器中添加了OAuth2ClientAuthenticationProcessingFilter
OAuth2ClientAuthenticationProcessingFilter
public class OAuth2ClientAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
// 获取OAuth2令牌
public OAuth2RestOperations restTemplate;
// 资源服务器根据令OAuth2牌获取OAuth2Authentication的OAuth2认证主体对象
private ResourceServerTokenServices tokenServices;
// 用来提取请求的一些信息,如会话啥的, 记录到认证主体对象中
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
// 认证事件发布器
private ApplicationEventPublisher eventPublisher;
public OAuth2ClientAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl); // 处理登录的地址
setAuthenticationManager(new NoopAuthenticationManager());
setAuthenticationDetailsSource(authenticationDetailsSource);
}
@Override
public void afterPropertiesSet() {
// 必须要设置OAuth2RestTemplate
Assert.state(restTemplate != null, "Supply a rest-template");
super.afterPropertiesSet();
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
// 获取访问令牌
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
try {
// 使用资源服务器令牌服务根据获取到的访问令牌加载出OAuth2Authentication中
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
if (authenticationDetailsSource!=null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
// 发布认证成功事件
publish(new AuthenticationSuccessEvent(result));
return result;
}
catch (InvalidTokenException e) {
// 认证失败处理
BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
}
private void publish(ApplicationEvent event) {
if (eventPublisher!=null) {
eventPublisher.publishEvent(event);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
// 认证成功之后, 再调用一次获取访问令牌(但其实已经缓存过了)
// Nearly a no-op, but if there is a ClientTokenServices then the token will now be stored
restTemplate.getAccessToken();
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
if (failed instanceof AccessTokenRequiredException) {
// Need to force a redirect via the OAuth client filter, so rethrow here
throw failed;
}
else {
// If the exception is not a Spring Security exception this will result in a default error page
super.unsuccessfulAuthentication(request, response, failed);
}
}
}
3. OAuth2SsoCustomConfiguration
这个配置类会在@EnableOAuth2Sso配置在WebSecurityConfiguerer的子类上生效
@Configuration
@Conditional(EnableOAuth2SsoCondition.class)
public class OAuth2SsoCustomConfiguration implements ImportAware, BeanPostProcessor, ApplicationContextAware {
// @EnableOAuth2Sso注解所标注的类
private Class<?> configType;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
// 获取到@EnableOAuth2Sso注解所标注的类,设置给configType
this.configType = ClassUtils.resolveClassName(importMetadata.getClassName(),null);
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 在@EnableOAuth2Sso所标注的WebSecurityConfiguerer的子类这个bean完成初始化后,
// 生成该bean的一个代理,并将代理返回去
// 注意到添加了一个MethodInterceptor拦截器是: SsoSecurityAdapter
// 看看这个拦截器做了什么?
if (this.configType.isAssignableFrom(bean.getClass())
&& bean instanceof WebSecurityConfigurerAdapter) {
ProxyFactory factory = new ProxyFactory();
factory.setTarget(bean);
factory.addAdvice(new SsoSecurityAdapter(this.applicationContext));
bean = factory.getProxy();
}
return bean;
}
private static class SsoSecurityAdapter implements MethodInterceptor {
private SsoSecurityConfigurer configurer;
SsoSecurityAdapter(ApplicationContext applicationContext) {
this.configurer = new SsoSecurityConfigurer(applicationContext);
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 当调用代理对象的init方法时, 会切入这个方法, 并且调用getHttp()
// (因为本来在init方法中就会调用getHttp, 所以在这里调用也没什么关系)
// 关键是看它切入后想干什么? 获取到HttpSecurity后, 使用SsoSecurityConfigurer去继续配置HttpSecurity
// (跟前面的一样,也是添加OAuth2ClientAuthenticationProcessingFilter过滤器)
// 这也就是说, 如果我们的@EnaleOAuth2Sso添加在了WebSecurityConfigurer子类上时, security会使用动态代理的方式, 在配置的最后往HttpSecurity中添加这个过滤器
// (注意:我们如果定了WebSecurityConfigurer子类也是会生成WebSecurity的噢, 并且会生成filter添加到web的代理filter中的)
if (invocation.getMethod().getName().equals("init")) {
Method method = ReflectionUtils
.findMethod(WebSecurityConfigurerAdapter.class, "getHttp");
ReflectionUtils.makeAccessible(method);
HttpSecurity http = (HttpSecurity) ReflectionUtils.invokeMethod(method,
invocation.getThis());
this.configurer.configure(http);
}
return invocation.proceed();
}
}
}
4. ResourceServerTokenServicesConfiguration
@Configuration
// 注意: 这仅会在容器中没有配置AuthorizationServerEndpointsConfiguration这个配置类时,当前配置类才会生效
// 当使用@EnableAuthorizationServer注解时,就会引入AuthorizationServerEndpointsConfiguration这个配置类
// 如果使用了这个注解, 那就表明授权配置都配置在当前项目了,那就没必要还发请求获取当前用户信息了
@ConditionalOnMissingBean(AuthorizationServerEndpointsConfiguration.class)
public class ResourceServerTokenServicesConfiguration {
// 方法注入: OAuth2ClientContext
// (这个在最开始的OAuth2ClientConfiguration中就配置了,
// 它是基于会话的:即在同一个会话中, 使用的OAuth2ClientContext都是同一个对象)
// OAuth2ProtectedResourceDetails
// (这个在OAuth2RestOperationsConfiguration中配置的,而这个配置类又是由OAuth2AutoConfiguration自动配置类引入的)
// 即: OAuth2AutoConfiguration->OAuth2RestOperationsConfiguration
// ->SessionScopedConfiguration
// ->OAuth2ProtectedResourceDetailsConfiguration(里面定义了AuthorizationCodeResourceDetails的bean,并且是@Primary的)
@Bean
@ConditionalOnMissingBean
public UserInfoRestTemplateFactory userInfoRestTemplateFactory(
ObjectProvider<List<UserInfoRestTemplateCustomizer>> customizers,
ObjectProvider<OAuth2ProtectedResourceDetails> details,
ObjectProvider<OAuth2ClientContext> oauth2ClientContext) {
return new DefaultUserInfoRestTemplateFactory(customizers, details,oauth2ClientContext);
}
@Configuration
@Conditional(RemoteTokenCondition.class) // 这个条件是当不满足jwt、jwk、jwtKeyStore的情况时,才匹配
protected static class RemoteTokenServicesConfiguration {
@Configuration
@Conditional(TokenInfoCondition.class)
protected static class TokenInfoServicesConfiguration {
// 资源服务器的属性配置类
private final ResourceServerProperties resource;
protected TokenInfoServicesConfiguration(ResourceServerProperties resource) {
this.resource = resource;
}
@Bean
public RemoteTokenServices remoteTokenServices() {
// 创建一个RemoteTokenServices
// 并且设置token-info-uri 用来根据token获取用户信息的url
// 设置客户端id
// 设置客户端密码
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl(this.resource.getTokenInfoUri());
services.setClientId(this.resource.getClientId());
services.setClientSecret(this.resource.getClientSecret());
return services;
}
}
}
@Configuration
@ConditionalOnClass(OAuth2ConnectionFactory.class)
@Conditional(NotTokenInfoCondition.class) // 不满足tokenInfo条件时生效
protected static class SocialTokenServicesConfiguration{
// ... 其实就是配置SpringSocialTokenServices或者是UserInfoTokenServices用来实现ResourceServerTokenServices
}
@Configuration
@ConditionalOnMissingClass("org.springframework.social.connect.support.OAuth2ConnectionFactory")
@Conditional(NotTokenInfoCondition.class) // 不满足tokenInfo条件时生效, 并且没有引入spring-social的依赖
protected static class UserInfoTokenServicesConfiguration{
// ...其实就是配置SpringSocialTokenServices用来实现ResourceServerTokenServices
}
@Configuration
@ConditionalOnMissingClass("org.springframework.social.connect.support.OAuth2ConnectionFactory")
@Conditional(NotTokenInfoCondition.class)
protected static class UserInfoTokenServicesConfiguration{
// ...其实就是配置 UserInfoTokenServices用来实现ResourceServerTokenServices
}
@Configuration
@Conditional(JwkCondition.class) // 配置了jwk时才生效
protected static class JwkTokenStoreConfiguration {
private final ResourceServerProperties resource;
public JwkTokenStoreConfiguration(ResourceServerProperties resource) {
this.resource = resource;
}
@Bean
@ConditionalOnMissingBean(ResourceServerTokenServices.class) // 可被覆盖(如果有自定义的)
public DefaultTokenServices jwkTokenServices(TokenStore jwkTokenStore) {
// 创建DefaultTokenServices,设置tokenStore(来自下面)
DefaultTokenServices services = new DefaultTokenServices();
services.setTokenStore(jwkTokenStore);
return services;
}
@Bean
@ConditionalOnMissingBean(TokenStore.class)
public TokenStore jwkTokenStore() {
// 使用配置的jwk, 定义令牌存储器
return new JwkTokenStore(this.resource.getJwk().getKeySetUri());
}
}
@Configuration
@Conditional(JwtTokenCondition.class) // 配置了jwt相关属性时, 生效
protected static class JwtTokenServicesConfiguration {
private final ResourceServerProperties resource;
private final List<JwtAccessTokenConverterConfigurer> configurers;
private final List<JwtAccessTokenConverterRestTemplateCustomizer> customizers;
// 构造器注入, 资源服务配置类对象、用来配置JwtAccessTokenConverter的配置器、用来配置RestTemplate的配置器
public JwtTokenServicesConfiguration(ResourceServerProperties resource,
ObjectProvider<List<JwtAccessTokenConverterConfigurer>> configurers,
ObjectProvider<List<JwtAccessTokenConverterRestTemplateCustomizer>> customizers) {
this.resource = resource;
this.configurers = configurers.getIfAvailable();
this.customizers = customizers.getIfAvailable();
}
@Bean
@ConditionalOnMissingBean(ResourceServerTokenServices.class) // 可被覆盖(如果有自定义的)
public DefaultTokenServices jwtTokenServices(TokenStore jwtTokenStore) {
// 创建DefaultTokenServices,设置tokenStore(来自下面)
DefaultTokenServices services = new DefaultTokenServices();
services.setTokenStore(jwtTokenStore);
return services;
}
@Bean
@ConditionalOnMissingBean(TokenStore.class)
public TokenStore jwtTokenStore() {
// 创建jwt的令牌存储器(传入jwt令牌增强器,来源下面)
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
// 其实就是jwt令牌转换器(增强器)
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 看有没有配置security.oauth2.resource.jwt.key-value,
// 如果没有配置,则需要从服务器那里去拿
String keyValue = this.resource.getJwt().getKeyValue();
if (!StringUtils.hasText(keyValue)) {
// 从服务器去拿密钥(如果是非对称加密,则公钥以-----BEGIN开头; 否则是对称加密)
keyValue = getKeyFromServer();
}
if (StringUtils.hasText(keyValue) && !keyValue.startsWith("-----BEGIN")) {
converter.setSigningKey(keyValue);
}
if (keyValue != null) {
converter.setVerifierKey(keyValue);
}
if (!CollectionUtils.isEmpty(this.configurers)) {
AnnotationAwareOrderComparator.sort(this.configurers);
// 可使用JwtAccessTokenConverterConfigurer来配置jwt增强器
for (JwtAccessTokenConverterConfigurer configurer : this.configurers) {
configurer.configure(converter);
}
}
return converter;
}
private String getKeyFromServer() {
RestTemplate keyUriRestTemplate = new RestTemplate();
if (!CollectionUtils.isEmpty(this.customizers)) {
for (JwtAccessTokenConverterRestTemplateCustomizer customizer : this.customizers) {
// 用来配置发送请求的RestTemplate
customizer.customize(keyUriRestTemplate);
}
}
HttpHeaders headers = new HttpHeaders();
String username = this.resource.getClientId();
String password = this.resource.getClientSecret();
if (username != null && password != null) {
// 就是basic认证, 需要 Base64(clientId:clientSecret)
byte[] token = Base64.getEncoder().encode((username + ":" + password).getBytes());
// 添加到请求头, 所以授权服务器需要开启basic认证噢
headers.add("Authorization", "Basic " + new String(token));
}
HttpEntity<Void> request = new HttpEntity<>(headers);
// 发送请求到 security.oauth2.resource.key.key-uri 所指向的路径
String url = this.resource.getJwt().getKeyUri();
// 发送请求, 获取key
return (String) keyUriRestTemplate
.exchange(url, HttpMethod.GET, request, Map.class).getBody()
.get("value");
}
}
@Configuration
@Conditional(JwtKeyStoreCondition.class) // 配置了security.oauth2.resource.jwt.key-store才生效
protected class JwtKeyStoreConfiguration implements ApplicationContextAware {
private final ResourceServerProperties resource;
private ApplicationContext context;
@Autowired
public JwtKeyStoreConfiguration(ResourceServerProperties resource) {
this.resource = resource;
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
@Bean
@ConditionalOnMissingBean(ResourceServerTokenServices.class)
public DefaultTokenServices jwtTokenServices(TokenStore jwtTokenStore) {
// 创建DefaultTokenServices,并且设置jwt令牌储存器(来源下面)
DefaultTokenServices services = new DefaultTokenServices();
services.setTokenStore(jwtTokenStore);
return services;
}
@Bean
@ConditionalOnMissingBean(TokenStore.class)
public TokenStore tokenStore() {
// jwt令牌存储器(传入令牌增强器)
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
// 必须同时配置
// security.oauth2.resource.jwt.key-store
// security.oauth2.resource.jwt.key-store-password
// security.oauth2.resource.jwt.key-alias
Assert.notNull(this.resource.getJwt().getKeyStore(), "keyStore cannot be null");
Assert.notNull(this.resource.getJwt().getKeyStorePassword(), "keyStorePassword cannot be null");
Assert.notNull(this.resource.getJwt().getKeyAlias(), "keyAlias cannot be null");
// 令牌增强器(转换器)
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 加载文件、读取密码、设置密钥对
Resource keyStore = this.context.getResource(this.resource.getJwt().getKeyStore());
char[] keyStorePassword = this.resource.getJwt().getKeyStorePassword().toCharArray();
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keyStore, keyStorePassword);
String keyAlias = this.resource.getJwt().getKeyAlias();
// 优先读取: security.oauth2.resource.jwt.key-password作为密码,
// 其次使用: security.oauth2.resource.jwt.key-store-password
char[] keyPassword = Optional.ofNullable(this.resource.getJwt().getKeyPassword()).map(String::toCharArray).orElse(keyStorePassword);
converter.setKeyPair(keyStoreKeyFactory.getKeyPair(keyAlias, keyPassword));
return converter;
}
}
// TokenInfo生效条件
private static class TokenInfoCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth TokenInfo Condition");
Environment environment = context.getEnvironment();
// 未配置 security.oauth2.resource.prefer-token-info, 就是true
Boolean preferTokenInfo = environment.getProperty("security.oauth2.resource.prefer-token-info", Boolean.class);
if (preferTokenInfo == null) {
preferTokenInfo = environment.resolvePlaceholders("${OAUTH2_RESOURCE_PREFERTOKENINFO:true}").equals("true");
}
// 获取 token-info-uri 和 user-info-uri 配置
String tokenInfoUri = environment.getProperty("security.oauth2.resource.token-info-uri");
String userInfoUri = environment.getProperty("security.oauth2.resource.user-info-uri");
// 如果 token-info-uri 和 user-info-uri 这2个都没配置, 那就匹配
if (!StringUtils.hasLength(userInfoUri)&& !StringUtils.hasLength(tokenInfoUri)) {
return ConditionOutcome.match(message.didNotFind("user-info-uri property").atAll());
}
// 如果配置了 token-info-uri , 并且 preferTokenInfo未true, 那也匹配
if (StringUtils.hasLength(tokenInfoUri) && preferTokenInfo) {
return ConditionOutcome.match(message.foundExactly("preferred token-info-uri property"));
}
// 其它情况都不匹配
return ConditionOutcome.noMatch(message.didNotFind("token info").atAll());
}
}
// jwt令牌生效条件
private static class JwtTokenCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth JWT Condition");
Environment environment = context.getEnvironment();
// jwt.key-value 和 jwt.key-uri 配置
String keyValue = environment.getProperty("security.oauth2.resource.jwt.key-value");
String keyUri = environment.getProperty("security.oauth2.resource.jwt.key-uri");
// 配置了任何一个, 则匹配
if (StringUtils.hasText(keyValue) || StringUtils.hasText(keyUri)) {
return ConditionOutcome.match(message.foundExactly("provided public key"));
}
// 都没配置, 则不匹配
return ConditionOutcome.noMatch(message.didNotFind("provided public key").atAll());
}
}
// jwk生效条件
private static class JwkCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth JWK Condition");
Environment environment = context.getEnvironment();
// 配置了 security.oauth2.resource.jwk.key-set-uri 才会生效, 否则不生效
String keyUri = environment.getProperty("security.oauth2.resource.jwk.key-set-uri");
if (StringUtils.hasText(keyUri)) {
return ConditionOutcome.match(message.foundExactly("provided jwk key set URI"));
}
return ConditionOutcome.noMatch(message.didNotFind("key jwk set URI not provided").atAll());
}
}
// jwtKeyStore生效条件
private static class JwtKeyStoreCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth JWT KeyStore Condition");
Environment environment = context.getEnvironment();
// 配置了 security.oauth2.resource.jwt.key-store 才会生效, 否则不生效
String keyStore = environment.getProperty("security.oauth2.resource.jwt.key-store");
if (StringUtils.hasText(keyStore)) {
return ConditionOutcome
.match(message.foundExactly("provided key store location"));
}
return ConditionOutcome
.noMatch(message.didNotFind("key store location not provided").atAll());
}
}
// 对前面的TokenInfo生效条件取反
private static class NotTokenInfoCondition extends SpringBootCondition {
private TokenInfoCondition tokenInfoCondition = new TokenInfoCondition();
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,AnnotatedTypeMetadata metadata) {
return ConditionOutcomeinverse(this.tokenInfoCondition.getMatchOutcome(context, metadata));
}
}
// RemoteToken生效条件: 当jwtToken、jwk、jwtKeyStore条件都不生效时, 才会生效
private static class RemoteTokenCondition extends NoneNestedConditions {
RemoteTokenCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@Conditional(JwtTokenCondition.class)
static class HasJwtConfiguration {
}
@Conditional(JwkCondition.class)
static class HasJwkConfiguration {
}
@Conditional(JwtKeyStoreCondition.class)
static class HasKeyStoreConfiguration {
}
}
}
5.OAuth2AutoConfiguration
@Configuration
@ConditionalOnClass({ OAuth2AccessToken.class, WebMvcConfigurer.class })
@Import({
OAuth2AuthorizationServerConfiguration.class,
OAuth2MethodSecurityConfiguration.class,
OAuth2ResourceServerConfiguration.class,
OAuth2RestOperationsConfiguration.class
})
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
public class OAuth2AutoConfiguration {
private final OAuth2ClientProperties credentials;
public OAuth2AutoConfiguration(OAuth2ClientProperties credentials) {
this.credentials = credentials;
}
@Bean
public ResourceServerProperties resourceServerProperties() {
return new ResourceServerProperties(this.credentials.getClientId(),this.credentials.getClientSecret());
}
}