@EnableOAuth2Sso注解

单点登录学习资料

单点登录原理与简单实现
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());
	}

}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单点登录(Single Sign-On, SSO)是一种身份验证机制,允许用户使用一组凭据(例如用户名和密码)在多个应用程序或系统中进行身份验证。在Spring Security中实现SSO的方法有多种。其中一种方法是使用Spring Security OAuth2框架。 在Spring Security OAuth2中,可以使用@EnableOAuth2Sso注解来启用SSO功能。通过在启动类上添加@EnableOAuth2Sso注解,可以使SSO生效。同时,可以在启动类中定义一个/user端点来获取当前认证用户的信息。这个/user端点使用Authentication对象作为参数,表示当前用户的身份认证信息。 另外,可以通过配置认证服务器的认证方式,从数据库中读取用户信息进行认证,而不是在配置文件中配置用户名和密码。这样可以使认证过程更加灵活和可扩展。可以将登录认证方式改为表单认证,以提供更好的用户体验和安全性。 综上所述,使用Spring Security OAuth2框架可以实现单点登录功能,并通过配置认证服务器的认证方式,实现从数据库中读取信息进行认证,提供更好的用户认证体验。123 #### 引用[.reference_title] - *1* *2* *3* [spring-security学习(十五)——单点登录的简单实例](https://blog.csdn.net/liman65727/article/details/119845195)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值