# oauth2客户端

# 原生配置

public final class HttpSecurity extends
		AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
		implements SecurityBuilder<DefaultSecurityFilterChain>,
		HttpSecurityBuilder<HttpSecurity> {
            
    /*xxx: 实现oauth2操作*/
	public OAuth2ClientConfigurer<HttpSecurity> oauth2Client() throws Exception {
		OAuth2ClientConfigurer<HttpSecurity> configurer = getOrApply(new OAuth2ClientConfigurer<>());
		this.postProcess(configurer);
		return configurer;
	}
            
}
/*xxx: oauth2客户端配置  (对应于服务提供商)*/
	/*xxx: 该配置器配置的oauth2流程, 认证完毕后会获取到 accessToken, 同时会将 获得授权的 client存储到上下文中, 以便后续使用*/
public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>> extends
	AbstractHttpConfigurer<OAuth2ClientConfigurer<B>, B> {
 	   
    /*xxx: 标准的oauth2流程,采用 授权码模式实现  */
	public class AuthorizationCodeGrantConfigurer {
     	/*xxx: 授权码模式 在 springSecurity的虚拟过滤器链上增加了两个过滤器,分别是:
		    1.OAuth2AuthorizationRequestRedirectFilter  2.OAuth2AuthorizationCodeGrantFilter  它们在过滤链上的位置已经由官方固定*/
		private void configure(B builder) {
			//xxx: 过滤链 加入 OAuth2AuthorizationRequestRedirectFilter, 其作用是为 oauth2进行登录跳转。 判断当前为oauth2登录的条件有:1.抛出了特定的oauth2异常;2.携带了指定的参数或者路径
			OAuth2AuthorizationRequestRedirectFilter authorizationRequestRedirectFilter = createAuthorizationRequestRedirectFilter(builder);
			builder.addFilter(postProcess(authorizationRequestRedirectFilter));

			//xxx: 过滤链加入 OAuth2AuthorizationCodeGrantFilter, 作用是对当前的授权码进行认证,本质上是获取 accessToken,并将其存储在上下文中
			OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = createAuthorizationCodeGrantFilter(builder);
			builder.addFilter(postProcess(authorizationCodeGrantFilter));
		}   
    }
}

# 授权请求重定向-过滤器逻辑

/*xxx: oauth2请求授权重定向过滤器*/
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
 	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
     	/*xxx: 首先解析 当前的请求,是不是 oauth2授权请求 */
			OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
			if (authorizationRequest != null) {
				/*xxx: 如果当前是 oauth2授权请求, 则直接重定向到授权地址,不再继续后续的流程 */
				this.sendRedirectForAuthorization(request, response, authorizationRequest);
				return;
			}
        
        	try {
			filterChain.doFilter(request, response);
		} catch (Exception ex) {
             /*xxx: 如果在后续的处理流程中, 报了 客户端需要授权 异常*/
					OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request, authzEx.getClientRegistrationId());
                
                	/*xxx: 则重定向到 授权地址 */
					this.sendRedirectForAuthorization(request, response, authorizationRequest);
					/*xxx: 在重定向之前,需要使用 requestCache 把当前的请求路径进行存储,方便授权流程完成后,进行恢复 */
					/*xxx: requestCache 默认由 session提供实现 */
					this.requestCache.saveRequest(request, response);
            }
    }
    
    /*xxx: 重定向到授权地址  */
	private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
												OAuth2AuthorizationRequest authorizationRequest) throws IOException, ServletException {

		/*xxx: 如果是授权码模式,则需要先 将授权请求与 state对应关系记录一下 (state是oauth2流程带的一个随机数,服务提供商会原样返回)*/
		if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
			this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
		}
		this.authorizationRedirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());
	}
}

/*xxx: oauth2授权请求解析器 */
public interface OAuth2AuthorizationRequestResolver {
 	OAuth2AuthorizationRequest resolve(HttpServletRequest request);
    OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId);
}

public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
    
    private final AntPathRequestMatcher authorizationRequestMatcher;
    
    public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository,
														String authorizationRequestBaseUri) {
     	this.clientRegistrationRepository = clientRegistrationRepository;
		/*xxx: registrationId参数匹配, 路径模式匹配 */
		this.authorizationRequestMatcher = new AntPathRequestMatcher(
				authorizationRequestBaseUri + "/{registrationId}");   
    }
    
     	@Override
	public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
		/*xxx: 从当前请求中,获取注册id  信息*/
		String registrationId = this.resolveRegistrationId(request);
		String redirectUriAction = getAction(request, "login");
		return resolve(request, registrationId, redirectUriAction);
	}  
    
    private String resolveRegistrationId(HttpServletRequest request) {
		/*xxx: 当前请求如果携带了 registrationId参数,则 获取registrationId 参数 */
		if (this.authorizationRequestMatcher.matches(request)) {
			return this.authorizationRequestMatcher
					.extractUriTemplateVariables(request).get("registrationId");
		}
		return null;
	}
    
}

# 原生授权异常的抛出时机

# 解析Oauth2客户端

**原生代码逻辑由reactive提供实现,**此处不再跟进。

# 解析Oauth2客户端参数

/*xxx: oauth2参数解析器*/
public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMethodArgumentResolver {
 	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> parameterType = parameter.getParameterType();
		/*xxx: 参数类型为 OAuth2AuthorizedClient, 并且带有 @RegisteredOAuth2AuthorizedClient注解*/
		return (OAuth2AuthorizedClient.class.isAssignableFrom(parameterType) &&
				(AnnotatedElementUtils.findMergedAnnotation(
						parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class) != null));
	}
    
    public Object resolveArgument(MethodParameter parameter,
									@Nullable ModelAndViewContainer mavContainer,
									NativeWebRequest webRequest,
									@Nullable WebDataBinderFactory binderFactory) throws Exception {
        //xxx: 根据内部策略,获取 registrationId
		String clientRegistrationId = this.resolveClientRegistrationId(parameter);
        
     	/*xxx: 加载 oauth2Client*/
		OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
				clientRegistrationId, principal, servletRequest);
        if (authorizedClient != null) {
			return authorizedClient;
		}
        
        ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
		if (clientRegistration == null) {
			return null;
		}
        
        /*xxx: 如果oauth2客户端已定义,  并且   授权模式为授权码模式时,抛出 需要授权异常 */
		if ("authorization_code".equals(clientRegistration.getAuthorizationGrantType())) {
			throw new ClientAuthorizationRequiredException(clientRegistrationId);
		}
        
    }
}

# Oauth2客户端变更

**由reactive提供实现,**此处不再深入;

# 令牌颁发-过滤器逻辑(授权回调)

/*xxx: oauth2授权码授权过滤器 */
public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
    
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
		throws ServletException, IOException {
        
        //xxx: 是否是授权响应, 如果是,则阻断后续过滤器
		if (matchesAuthorizationResponse(request)) {
			/*xxx: 处理授权响应*/
			processAuthorizationResponse(request, response);
			return;
		}

		filterChain.doFilter(request, response);
    }
    
    private boolean matchesAuthorizationResponse(HttpServletRequest request) {
     	/*xxx: 当前请求是否为 授权结果响应回调*/
		/*xxx: 判断请求参数中 是否同时有: code以及state,或者 error以及state */
		if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
			return false;
		}
        
        /*xxx: 根据state从当前上下文中, 获取 授权请求*/
		OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
        
        /*xxx: 比对当前请求地址, 进行合法性校验*/
		if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
				Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
				Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
				Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
				Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
				Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
			return true;
		}
		return false;
        
    }
    
    private void processAuthorizationResponse(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
     	/*xxx: 从缓存中获取授权请求*/
		OAuth2AuthorizationRequest authorizationRequest =
				this.authorizationRequestRepository.removeAuthorizationRequest(request, response);   
        
        /*xxx: 根据回调参数,转化为 授权响应 */
		OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(request.getParameterMap(), redirectUri);
        
        /*xxx: 组装oauth2 授权码认证令牌*/
		OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
			clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
        
        //xxx: 对授权码认证令牌进行认证,主要是验证 state的合法性
        authenticationResult = (OAuth2AuthorizationCodeAuthenticationToken)
				this.authenticationManager.authenticate(authenticationRequest);
        
        //xxx: 认证成功后,组装客户端,并将之进行缓存 
		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
			authenticationResult.getClientRegistration(),
			principalName,
			authenticationResult.getAccessToken(),
			authenticationResult.getRefreshToken());

		/*xxx: 已授权后,将该客户端信息保存起来 */
		this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response);
        
        //xxx: 获取授权请求的重定向地址
		String redirectUrl = authorizationRequest.getRedirectUri();
        
        /*xxx: 认证成功后, 跳转到 oauth2的重定向地址。 这个重定向地址,由本地配置*/
		this.redirectStrategy.sendRedirect(request, response, redirectUrl);
    }
}

# oauth2第三方授权登录

# 原生配置

class Test{
    public OAuth2LoginConfigurer<HttpSecurity> oauth2Login() throws Exception {
		return getOrApply(new OAuth2LoginConfigurer<>());
	}
}
/*xxx: oauth2登录入口*/
public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> extends
	AbstractAuthenticationFilterConfigurer<B, OAuth2LoginConfigurer<B>, OAuth2LoginAuthenticationFilter> {
    
    
 	/*xxx: oauth2登录配置器,添加两个过滤器: 1. 授权请求重定向过滤器(与oauth2Client一致)  2.OAuth2LoginAuthenticationFilter oauth2登录认证过滤器*/
	public void configure(B http) throws Exception {

        //xxx: 根据自身策略,决定授权重定向过滤器的来源
		OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter;
        
        http.addFilter(this.postProcess(authorizationRequestFilter));
        
        /*xxx: oauth2登录认证过滤器, 过滤链 默认就有该过滤器 , 此处对其进行个性化配置*/
        OAuth2LoginAuthenticationFilter authenticationFilter = this.getAuthenticationFilter();
    }
}

# 授权重定向-过滤器逻辑

内容同oauth2客户端,略;

# oauth2登录认证过滤器逻辑

/*xxx: oauth2登录认证过滤器 */
public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
 	public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository,
											OAuth2AuthorizedClientRepository authorizedClientRepository,
											String filterProcessesUrl) {
        //xxx: 指定处理登录的地址, 不一定是实际的地址,也可能是 ant模式。  只要能够匹配即可
        super(filterProcessesUrl);
    }
    
    @Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException {
     	/*xxx: 当前请求是否是oauth2回调, 如果不是的话,报错阻断认证流程 */
		if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(request.getParameterMap())) {
			OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
        
        /*xxx: 获取授权请求 */
		OAuth2AuthorizationRequest authorizationRequest =
				this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
        
        /*xxx: 根据注册id,获取 客户端注册信息 */
		ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
        
        /*xxx: 组装 oauth2登录认证令牌。  此时存在 oauth2请求, oauth2响应。  通过Exchange交换机,判断二者的匹配逻辑  */
		OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(
				clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
        
        /*xxx: 对 oauth2登录认证令牌 进行认证*/
		OAuth2LoginAuthenticationToken authenticationResult =
			(OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);
        
        /*xxx: 由 认证成功后的登录认证令牌,组装 oauth2认证令牌*/
		OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(
			authenticationResult.getPrincipal(),
			authenticationResult.getAuthorities(),
			authenticationResult.getClientRegistration().getRegistrationId());
        
        /*xxx: 将授权过的客户端进行保存 */
		this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
		/*xxx: 返回oauth2认证令牌 */
		return oauth2Authentication;
        
    }
}

# oauth2登录认证令牌的授权

/*xxx: oauth2登录认证处理器 */
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
 	   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2LoginAuthenticationToken loginAuthenticationToken =
			(OAuth2LoginAuthenticationToken) authentication;
           
        	/*xxx: 先 组装授权码令牌,并对其进行授权, 其实质,就是判断state是否匹配  */
			authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
					.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
							loginAuthenticationToken.getClientRegistration(),
							loginAuthenticationToken.getAuthorizationExchange()));   
           
           /*xxx: 授权码令牌认证成功后,会获得 accessToken*/
		OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
           
           /*xxx: 根据 accessToken获取用户信息, 模板模式, Oauth2UserService + Oauth2User  */
		/*xxx: 第三方授权登录的本质: 获取了accessToken后,  获取 用户信息 */
		OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
				loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
           
           /*xxx: 根据用户信息,组装 Oauth2登录认证令牌*/
		OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
			loginAuthenticationToken.getClientRegistration(),
			loginAuthenticationToken.getAuthorizationExchange(),
			oauth2User,
			mappedAuthorities,
			accessToken,
			authorizationCodeAuthenticationToken.getRefreshToken());
		authenticationResult.setDetails(loginAuthenticationToken.getDetails());

		return authenticationResult;
       }
}

# oauth2用户信息的获取

public interface OAuth2UserService<R extends OAuth2UserRequest, U extends OAuth2User> {
 	U loadUser(R userRequest) throws OAuth2AuthenticationException;   
}

public interface OAuth2User extends AuthenticatedPrincipal {
    
	Collection<? extends GrantedAuthority> getAuthorities();
    
    Map<String, Object> getAttributes();
}

# oauth2客户端SpringBoot生效条件

@ConditionalOnClass({ EnableWebSecurity.class, ClientRegistration.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@Import({ OAuth2ClientRegistrationRepositoryConfiguration.class,
        OAuth2WebSecurityConfiguration.class })
public class OAuth2ClientAutoConfiguration {
}
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ClientsConfiguredCondition.class)
/*xxx: 当存在至少一个 oauth2.client.registration时,security oauth2的配置才会整体生效*/
class OAuth2ClientRegistrationRepositoryConfiguration {
    
 	@Bean
	@ConditionalOnMissingBean(ClientRegistrationRepository.class)
	InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
		List<ClientRegistration> registrations = new ArrayList<>(
				OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
		return new InMemoryClientRegistrationRepository(registrations);
	}   
}
@ConditionalOnBean(ClientRegistrationRepository.class)
class OAuth2WebSecurityConfiguration {
 	@Bean
	@ConditionalOnMissingBean
	OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
		/*xxx: 读取已授权客户端的服务,默认由 concurrentHashMap实现 */
		return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
	}

	@Bean
	@ConditionalOnMissingBean
	OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
		/*xxx: 已授权oauth2客户端,默认由 concurrentHashMap 或者 session实现 */
		return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnDefaultWebSecurity
	/*xxx: 当没有使用默认过滤器链时,该配置不生效*/
	static class OAuth2SecurityFilterChainConfiguration {

		@Bean
		SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
			http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
			http.oauth2Login(Customizer.withDefaults());
			http.oauth2Client();
			return http.build();
		}

	}   
}

# 自研单点-oauth2服务端

# 端口定义

# 授权端口

**相当于提供授权码 **

通过用户信息实现认证凭证

public interface IAuthorization {

    //授权,返回访问令牌(授权码模式)  或者 返回访问令牌+授权码(简化模式)
    ModelAndView authorize(HttpServletRequest request,Map<String, Object> model,
                           @RequestParam Map<String, String> parameters, SessionStatus sessionStatus);

    //处理用户授权结果
    View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model,
                       SessionStatus sessionStatus, Principal principal);
}
@RequestMapping("/oauth2")
@Slf4j
@SessionAttributes( value = {"authorizationRequest"})
public class AuthorizationController implements IAuthorization {
 	   @RequestMapping("authorize")
    public ModelAndView authorize(HttpServletRequest request, Map<String, Object> model,
                                  @RequestParam Map<String, String> parameters,
                                  SessionStatus sessionStatus) {
        //根据参数构造 授权请求
        AuthorizationRequest authorizationRequest = oauth2RequestFactory.createAuthorizationRequest(parameters);
        
     	/* 获取令牌或者获取授权码,不能同时为空,即springSecurityOauth2需要进行授权的 只有 授权码,以及隐式授权码模式
         密码模式,以及客户端模式,不需要走授权服务 */
        if (!responseTypes.contains("ticket") && !responseTypes.contains("code")) {
            throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
        }
        
        Authentication authentication= SecurityContextHolder.getContext().getAuthentication();
            /*如果当前用户未认证, 则抛错*/
       if (!authentication.isAuthenticated()) {
                throw new InsufficientAuthenticationException(
                        "User must be authenticated with Spring Security before authorization can be completed.");
        }
        
        ClientDetails client = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId());

        String redirectUriParameter = authorizationRequest.getRequestParameters().get("redirect_uri");
        
        if (StringUtils.hasText(redirectUriParameter)) {
                //如果传递了重定向参数,则需要判断是否与重定向的规则吻合,否则不进行后续的授权流程
                String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);

                if (!StringUtils.hasText(resolvedRedirect)) {
                    throw new RedirectMismatchException(
                            "A redirectUri must be either supplied or preconfigured in the ClientDetails");
                }
                authorizationRequest.setRedirectUri(resolvedRedirect);
            } else {
                //这里重定向的是 客户端验证token的地址,并非验证成功后跳转的地址
                if (!client.getRegisteredRedirectUri().isEmpty()) {
                    authorizationRequest.setRedirectUri(client.getRegisteredRedirectUri().stream().findFirst().get());
                } else {
                    throw new RuntimeException("当前应用的重定向参数未配置");
                }

        }
        
        //用户已经进行过授权,则颁发令牌
        if (authorizationRequest.isApproved()) {
                if (responseTypes.contains("ticket")) {
                    return getImplicitGrantResponse(authorizationRequest);
                }

                if (responseTypes.contains("code")) {
                    return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
                            authentication));
                }
         }
    }
    
    private View getAuthorizationCodeResponse(AuthorizationRequest authorizationRequest, Authentication authUser) {
        try {
            return new RedirectView(getSuccessfulRedirect(authorizationRequest, generateCode(authorizationRequest,
                    authUser)),
                    false, true, false);
        } catch (Oauth2Exception e) {
            return new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, false), false, true, false);
        }
    }
    
    
}

# 提供令牌端口

提供令牌的同时,还存在存储令牌的功能.

通过客户端信息(key+secret)实现认证凭证

public interface IProvideToken {
    ResponseEntity<DefaultOauth2AccessToken> getAccessToken(Principal principal,
            Map<String, String> parameters) throws HttpRequestMethodNotSupportedException;

    ResponseEntity<DefaultOauth2AccessToken> postAccessToken(Principal principal,
                                                             Map<String, String> parameters) throws HttpRequestMethodNotSupportedException;
}
@RequestMapping("/oauth2")
public class ProvideTokenController implements IProvideToken {
    
    	@RequestMapping(value="token",method = RequestMethod.POST)
        public ResponseEntity<DefaultOauth2AccessToken> postAccessToken(Principal principal,
                                                                    @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
			if (!(principal instanceof Authentication)) {
            //第三方客户端办法令牌前,需要对该客户端进行验证
            throw new InsufficientAuthenticationException(
                    "There is no client authentication. Try adding an appropriate authentication filter.");
        }
            //前置需要经过一系列的校验,此处略...
            
            //通过令牌颁发器,颁发令牌
        DefaultOauth2AccessToken token = (DefaultOauth2AccessToken) tokenGranter.grant(tokenRequest.getGrantType(),tokenRequest);
        if(token == null){
            throw new UnsupportedGrantTypeException("unsupported grant type: "+tokenRequest.getGrantType());
        }

        return getResponse(token);
        }
    
    private ResponseEntity<DefaultOauth2AccessToken> getResponse(DefaultOauth2AccessToken token) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Cache-Control","no-store");
        headers.set("Pragma","no-cache");
        return new ResponseEntity<DefaultOauth2AccessToken>(token,headers, HttpStatus.OK);
    }
}

# 校验令牌端口

校验令牌的同时,可以提供认证信息.

public interface IVerifyToken {
    Map<String, ?> checkToken(HttpServletRequest request, String value);
}
@RequestMapping("/oauth2")
public class VerifyTokenController implements IVerifyToken {
    
    @RequestMapping(value = "check_token")
    @NoopHttpReturnValueEnhancer
    public Map<String, ?> checkToken(HttpServletRequest request, @RequestParam("token")  String value) {
     	AtmOauth2AccessToken token = resourceServerTokenServices.readAccessToken(value);

        if(token==null){
            throw new InvalidTokenException("ticket was not recognised");
        }

        if(token.isExpired()){
            throw new InvalidTokenException("ticket has expired");
        }
        
        //通过资源令牌服务,获取认证信息
        AtmOauth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
        //此处返回 认证的账号信息即可(还可包括过期时间 信息)
        Map<String,?> response = accessTokenConverter.convertAccessToken(token,authentication);

        return  response;
    }
}

# 客户端校验

提供令牌端口,需要进行客户端授权,目前暂未实现。

后期可考虑在应用层面+参数解析器实现.

# 自研单点-cas服务端

# 端口定义

# TGT令牌的颁发

由于cas流程的特殊性,TGT的颁发由登录成功处理器完成.

public class AtmCasSsoServerLoginSuccessfulHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    
 	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
     	   //颁发身份凭证 tgt
            AtmTicketGrantingTicket tgt = tgtGranter.createOrUpdateTicketGrantingTicket(authentication);
            ticketRegistry.addTicket(tgt);
            Cookie cookie = new Cookie(TGC,
                    Base64.getEncoder().encodeToString(tgt.getId().getBytes(StandardCharsets.UTF_8)));
            cookie.setPath(request.getContextPath());
            response.addCookie(cookie);
        
        //重定向颁发令牌的地址(Controller)
            String authorizeLocation = getAuthorizeLocation(request);
            UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(authorizeLocation);
            Iterator iterator1 = paramsMap.keySet().iterator();
            while (iterator1.hasNext()) {
                String key = (String) iterator1.next();
                builder.queryParam(key, paramsMap.get(key));
            }
            response.sendRedirect(builder.toUriString());
    }
}

# 授权客户端凭证

@RequestMapping("/cas")
public class GrantStController {
    
    @GetMapping("authorize")
    //授权完成后,直接重定向到客户端验证地址
    public void grantSt(HttpServletRequest request, HttpServletResponse response, String client,
                        Authentication authentication) throws IOException {
     	if (StringUtils.isEmpty(client)) {
            throw new RuntimeException("授权失败,client参数缺失");
        }
        
        Cookie[] cookies = request.getCookies();
        String tgtId = "";
        for (Cookie cookie : cookies) {
            if (TGC.equals(cookie.getName())) {
                tgtId = new String(Base64.getDecoder().decode(cookie.getValue()));
                break;
            }
        }
        
        //获取注册的service信息
        AtmRegisteredService atmRegisteredService = serviceRegistry.findServiceByExactServiceName(client);
        
        //通过tgt, 授权 st
        AtmService atmService = new SimpleWebApplicationServiceImpl(client, serviceUrl,
                artifactId);

        AtmServiceTicket serviceTicket = stGranter.grantServiceTicket(tgtId, atmService, authentication);

        ticketRegistry.addTicket(serviceTicket);
        
        //使用 client本身的配置重定向
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder
                .fromHttpUrl(clientUrl+"/ssoClient/certificateValidate/cas");
        uriComponentsBuilder.queryParam(TICKET,st);
        response.sendRedirect(uriComponentsBuilder.toUriString());
    }
    
}

# 校验客户端凭证

@RequestMapping("/cas")
public class TicketValidationController {
    
    @PostMapping("verify")
    @NoopHttpReturnValueEnhancer
    public Map<String, Object> validate(HttpServletRequest request, HttpServletResponse response, String client,
                                        String ticket) {
        if (StringUtils.isEmpty(ticket) || StringUtils.isEmpty(client)) {
            throw new RuntimeException("st凭证缺失!");
        }
        
        //校验令牌,通常情况下,一个ticket只被使用一次,但是也可以通过配置,使得ticket支持多次校验
        Authentication authentication = validateServiceTicket(atmService, ticket);
        
        //常规的cas服务端,是通过写入 assertsion 参数的手段来标识用户信息的。  此处直接采用简单的参数返回
        Map<String, Object> result = new HashMap<>();
        result.put(USERNAME, JSON.parseObject(JSON.toJSONString(authentication.getPrincipal())).getString(USERNAME));
        result.put(DETAILS, authentication.getDetails());
        return result;
    }	
    
    private Authentication validateServiceTicket(AtmService atmService, String serviceTicket) {
        //每次访问后,会刷新 st凭证的情况。   对于过期的 st凭证,会将其进行删除 
        AtmServiceTicket atmServiceTicket = ticketRegistry.getTicket(serviceTicket, AtmServiceTicket.class);
        if (atmServiceTicket == null) {
            throw new RuntimeException("验证失败,令牌不存在.");
        }
        AtmService service = atmServiceTicket.getService();
        if (!atmService.matches(service)) {
            throw new RuntimeException("令牌无效,不是由当前客户端颁发");
        }
        return atmServiceTicket.getTicketGrantingTicket().getAuthentication();

    }
}

# tgt与st的结构

  • 一个用户登录凭证,对应于一个tgt
  • 一个tgt对应多个st,可以称之为签发st. tgt失效,也就意味着它所签发的所有st失效

# 自研单点结构

# 首次登录行为(前端,且为前后端分离架构)

  • 如果当前请求带有客户端信息,该客户端将自动进行授权动作.
  • 如果当前请求没有带客户端信息,则进入到管理界面。包括客户端信息管理,客户端凭证管理等
  • 具体的行为实现,由认证成功处理器实现.
  • 首次登录,默认系统为前后端分离架构,必须带上currentUri参数
public class AtmOauth2SsoServerLoginSuccessfulHandler extends SavedRequestAwareAuthenticationSuccessHandler {
 	@Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
     	//1. 获取当前请求路径 (前后端分离系统,oauth2 必传)
        String currentUri = request.getHeader(CURRENT_URI);   
        
        //构造oauth2授权请求
        UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(currentUri).build();
        
        if (request.getAttribute(CLIENT) != null) {
            //颁发身份凭证(省略)

            request.setAttribute(RESPONSE_TYPE, "code");
            request.setAttribute(CLIENT_ID, request.getAttribute(CLIENT));
            //客户端的当前状态,可指定任意值,认证服务器会原封不动的返回这个值
            request.setAttribute(STATE, "oauth2Sso");
            //表示申请的权限范围,可选项
            request.setAttribute(SCOPE, "sso");
            request.getRequestDispatcher(AtmOauth2ConfigConstants.OAUTH_SERVICE_BASE_NAME + "/" + AUTHORIZE)
                    .forward(request, response);
        } else {
            //重定向到之前的路径
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}
public class AtmCasSsoServerLoginSuccessfulHandler extends SavedRequestAwareAuthenticationSuccessHandler {
 	
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
     	//1. 认证服务器,该参数必传 (由于是前后端分离的缘故)
        String currentUri = request.getHeader(CURRENT_URI);
        
        //构造授权请求
        UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(currentUri).build();
        
        //通过前端跳转,前端有感知
        if (request.getAttribute(CLIENT) != null) {
            //重定向颁发令牌的地址(Controller)
            String authorizeLocation = getAuthorizeLocation(request);
            UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(authorizeLocation);
            Iterator iterator1 = paramsMap.keySet().iterator();
            while (iterator1.hasNext()) {
                String key = (String) iterator1.next();
                builder.queryParam(key, paramsMap.get(key));
            }
            response.sendRedirect(builder.toUriString());
        } else {
            //重定向到之前的路径
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}

# 登录失败行为(认证行为)

  • 由于是前后端分离系统,前后端都需要特殊定制.
  • 但是其本质没变,都是跳转到登录页. 只不过,单点模式时,额外附加了client参数(严格来说,它应该是客户端使用的,此处放置在服务端主要是为了兼容)。
  • 涉及到较为复杂的跳转,主要与浏览器的cookie机制有关;
public class AtmSsoAuthenticationEntryPoint extends AtmAuthenticationEntryPoint {
 	    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException, ServletException {
        //clientId存在,则必须验证合法性: 1.应用是否存在  2.如果存在client,判定应用是否限源(客户端servletPath比对,base与domain强制匹配)
        String client = request.getParameter("client");
        String currentUri = request.getParameter("currentUri");
        //此处可以对其进行加密
        String loginPage = this.securityProperties.getBrowser().getRedirect().getLoginPage();
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(loginPage);
        if (StringUtils.hasText(client)) {
            uriComponentsBuilder.queryParam("client", client);
            log.debug("当前应用为ssoSever,将进行client{}的认证", client);
        } else {
            log.error("当前应用为ssoServer,将进入后台管理界面");
        }
        if (StringUtils.hasText(currentUri)) {
            uriComponentsBuilder.queryParam("currentUri", currentUri);
        }
        response.sendRedirect(uriComponentsBuilder.toUriString());
    }
}

# 单点登出行为(登出处理器)

# cas登出处理

public class AtmCasSsoServerLogoutHandler implements LogoutHandler {
 	
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
     	//2. 根据tgt找到所有的service
        AtmTicketGrantingTicket ticketGrantingTicket = ticketRegistry.getTicket(tgtId,
                AtmTicketGrantingTicket.class);
        
        //移除tgt,后续的 签发将失败
        ticketRegistry.deleteTicket(tgtId);
        
        //遍历发出登出通知
        Map<String, AtmService> atmServiceMap = ticketGrantingTicket.getServices();
        Stream<Map<String, AtmService>> streamServices = Stream.of(atmServiceMap);

        streamServices
                .map(Map::entrySet)
                .flatMap(Set::stream)
                .filter(entry -> entry.getValue() instanceof WebApplicationService)
                .map(entry -> {
                    final WebApplicationService service = (WebApplicationService) entry.getValue();
                    log.debug("Handling single logout callback for [{}]", service);
                    return this.singleLogoutServiceMessageHandler.handle(service, entry.getKey());
                })
                .flatMap(Collection::stream)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        //3. 发出登出请求(后端请求在上方已经做了处理, 前端退出似需要结合前端另外再做处理,此处没有做处理)

        //4. 清理凭证
        Cookie cookie = new Cookie(TGC, "");
        response.addCookie(cookie);
        
    }
}

# oauth2登出处理

public class AtmOauth2SsoServerLogoutHandler implements LogoutHandler {
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

        //1. 根据authentication,找到所有的token (authentication一定是一次登录)
        Collection<AtmOauth2AccessToken> tokenCollection = tokenStore.findTokensByClientIdAndUserName("",
                authentication.getName());

        //2. 遍历发出登出请求
        tokenCollection.forEach(token -> {
            String clientId = tokenStore.getClientByAccessToken(token.getValue());
            if (StringUtils.hasText(clientId)) {
                ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
                if (client != null) {
                    WebApplicationService service = new SimpleWebApplicationServiceImpl(client.getClientId(),
                            request.getRequestURI(), "");
                    singleLogoutServiceMessageHandler.handle(service, token.getValue());
                }

            }

        });
    }
}

# 登出消息发送处理器

public interface SingleLogoutServiceMessageHandler {

    Collection<LogoutRequest> handle(WebApplicationService singleLogoutService, String ticketId);
}

public class DefaultSingleLogoutServiceMessageHandler implements SingleLogoutServiceMessageHandler {
 	
    public Collection<LogoutRequest> handle(WebApplicationService singleLogoutService, String ticketId) {
    	//当前客户端是否支持单点登出
        if (!serviceSupportsSingleLogout(registeredService)) {
            log.debug("Service [{}] does not support single logout.", registeredService);
            return new ArrayList<>(0);
        }
        //重定向到客户端登出地址
        return createLogoutRequests(ticketId, singleLogoutService, registeredService, logoutUrls);
    }
    
    private Collection<LogoutRequest> createLogoutRequests(String ticketId,
                                                           WebApplicationService selectedService,
                                                           AtmRegisteredService registeredService,
                                                           Collection<String> logoutUrls) {
        return logoutUrls
                .stream()
                .map(url -> createLogoutRequest(ticketId, selectedService, registeredService, url))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
    
    private LogoutRequest createLogoutRequest( String ticketId,
                                               WebApplicationService selectedService,
                                               AtmRegisteredService registeredService,
                                               String logoutUrl) {
     	//支持后端登出的客户端,才发送请求 
        if (type == AtmRegisteredService.LogoutType.BACK_CHANNEL) {
            if (performBackChannelLogout(logoutRequest)) {
                logoutRequest.setStatus(LogoutRequestStatus.SUCCESS);
            } else {
                logoutRequest.setStatus(LogoutRequestStatus.FAILURE);
                log.warn("Logout message is not sent to [{}]; Continuing processing...", selectedService);
            }
        } else {
            log.debug("Logout operation is not yet attempted for [{}] given logout type is set to [{}]", selectedService, type);
            logoutRequest.setStatus(LogoutRequestStatus.NOT_ATTEMPTED);
        }
        return logoutRequest;   
    }
    
    //发送单点登出的请求
    public boolean performBackChannelLogout( LogoutRequest request) {
        
        //登出请求,遵循xml规范
        String logoutRequest = this.logoutMessageCreator.create(request);
        
     	MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
        paramMap.add(LOGOUT_REQUEST, logoutRequest);   
        
        RestTemplate restTemplate  = new RestTemplate();
        
        restTemplate.postForObject(request.getLogoutUrl(),requestEntity,String.class); 
        
        //应用中不再判定客户端是否登出成功.后续可改为可靠登出 
        return true;
    }
}

# 自研单点客户端

# 登录失败行为(认证行为)

public class SsoClientAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private SsoProperties ssoProperties;

    public SsoClientAuthenticationEntryPoint(SsoProperties ssoProperties) {
        this.ssoProperties = ssoProperties;
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        String path = ssoProperties.getClient().getSsoServerLocation();
        Assert.isTrue(!StringUtils.isEmpty(path), "parameter [secure.oauth2.sso.loginPath] cannot be null!");
        log.debug("using sso login mode,the location is : " + path);
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(path);
        builder.queryParam("client", "atmClient1");
        log.debug("单点登录,进行认证:{}", builder.build().toUriString());
        response.sendRedirect(builder.build().toUriString());
    }

}

# 登录成功行为(辅助登出)

public class SsoClientLoginSuccessHandler implements AuthenticationSuccessHandler {

    public static final String TICKET = "ticket";
    public static final String TOKEN = "token";

    private SessionMappingStorage sessionMappingStorage;


    private List<LoginSuccessHandlerAdapter> handlers;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        //1.加载权限
        if (handlers != null) {
            for (LoginSuccessHandlerAdapter handler : handlers) {
                handler.onAuthenticationSuccess(request,response,authentication);
            }
        }

        //2.记录凭证与session的关系,(正常的单点过程,只可能存在cas或oauth2的一种)
        String certificateKey = request.getParameter(TICKET);
        if (StringUtils.isEmpty(certificateKey)) {
            certificateKey = request.getAttribute(TOKEN) == null ? null : (String) request.getAttribute(TOKEN);
        }
        HttpSession session = request.getSession();
        if (session != null && StringUtils.hasText(certificateKey)) {
            sessionMappingStorage.addSessionById(certificateKey, session);
        }
        PrintWriter printWriter = response.getWriter();
        printWriter.write("sso login success:" + JSON.toJSONString(authentication));
        printWriter.flush();
    }
}

# 单点授权回调(凭证校验过滤器)

# cas单点回调

public class CasSsoClientCredentialValidateFilter implements SsoUrlMatchedFilter, Filter {
    
 	@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
            ServletException {
                
                //有条件过滤器
        if (requestMatcher.matches(httpServletRequest)) {
         	  String ticket = retrieveTicketFromRequest(httpServletRequest);
            
            MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
                //带上客户端认证信息
                paramMap.add(CLIENT, "atmClient1");
                paramMap.add(CLIENT_SECRET, "ssoClient1Secret");
                paramMap.add(TICKET, ticket);
            
            Map resultMap = restTemplate.postForObject(ssoProperties.getClient().getSsoServerLocation() +
                                "/cas/verify",
                        requestEntity,
                        Map.class);
            
            //认证凭证校验成功后,需要返回用户信息
                if (resultMap != null && resultMap.containsKey(USER_NAME)) {
                 	 String userName = (String) resultMap.get(USER_NAME);
                    Authentication authentication = new UsernamePasswordAuthenticationToken(userName, "",
                            Collections.emptyList());
                    //此处可以设置认证的信息,过滤链出栈时,会自动进行维持
                    SecurityContext securityContext = new SecurityContextImpl(authentication);
                    SecurityContextHolder.setContext(securityContext);
                    //成功处理器,要完成几个任务: 1.加载权限 2.进行重定向跳转
                    successHandler.onAuthenticationSuccess(httpServletRequest, httpServletResponse, authentication);   
                }
        }
    }
}

# oauth2单点回调

public class Oauth2SsoClientCredentialValidateFilter implements SsoUrlMatchedFilter, Filter {
    
 	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
            ServletException {
         //有条件过滤器
        if (requestMatcher.matches((HttpServletRequest) request)) {
         	 Map<String, String> map = Oauth2Utils.extractMap((httpServletRequest).getQueryString());
            //此处可以对state进行验证,初版暂未验证
            //这是目前 与 官方提供的 oauth2Client授权登录的  最大区别
            String code = map.get(CODE);
            if (code == null) {
                throw new RuntimeException("用户授权失败");
            }   
            
            MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
            //带上客户端凭证信息
            paramMap.add(CLIENT_ID, "atmClient1");
            paramMap.add(CLIENT_SECRET, "ssoClient1Secret");
            paramMap.add(CODE, code);
            paramMap.add(GRANT_TYPE,"authorization_code");
            
            //暂时使用basic进行客户端认证
            if (this.atmAuthenticationScheme == AtmAuthenticationScheme.HEADER) {
                headers.add(
                        "Authorization",
                        String.format(
                                "Basic %s",
                                new String(Base64Utils.encode(String.format("%s:%s", paramMap.get(CLIENT_ID).get(0),
                                        paramMap.get(CLIENT_SECRET).get(0)).getBytes(StandardCharsets.UTF_8)),
                                        StandardCharsets.UTF_8)));
                paramMap.remove(CLIENT_ID);
                paramMap.remove(CLIENT_SECRET);
            }
            
            //第一步,先获取令牌
            DefaultOauth2AccessToken accessToken =
                    restTemplate.postForObject(ssoProperties.getClient().getSsoServerLocation() +
                                    "/oauth2/token",
                            requestEntity,
                            DefaultOauth2AccessToken.class);
            
            //第二步,根据令牌获取认证信息
            Map map1 = restTemplate.postForObject(ssoProperties.getClient().getSsoServerLocation() +
                            "/oauth2/check_token",
                    requestEntity1,
                    Map.class
            );
            
            if (map1 != null && map1.containsKey(USER_NAME)) {
                String userName = (String) map1.get(USER_NAME);
                Authentication authentication = new UsernamePasswordAuthenticationToken(userName, "",
                        Collections.emptyList());
                //此处可以设置认证的信息,过滤链出栈时,会自动进行维持
                SecurityContext securityContext = new SecurityContextImpl(authentication);
                SecurityContextHolder.setContext(securityContext);
                //该token将与session关联,实现单点登出的功能
                httpServletRequest.setAttribute(TOKEN, accessToken.getValue());
                //成功处理器,要完成几个任务: 1.加载权限 2.进行重定向跳转
                successHandler.onAuthenticationSuccess(httpServletRequest, httpServletResponse, authentication);
            } 
            
        }
    }
}

# 登出行为(登出成功处理器)

  • 登出成功处理器处理相应逻辑
  • 主要是剔除指定的session
public class SsoClientLogoutSuccessHandler implements LogoutSuccessHandler {
 	@Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                Authentication authentication) throws IOException, ServletException {
        if (isBackChannelLogoutRequest(request)) {
            log.trace("Received a back channel logout request");
            destroySession(request);

        } else if (isFrontChannelLogoutRequest(request)) {
            log.trace("Received a front channel logout request");
            destroySession(request);
            // redirection url to the SSO server
            String redirectionUrl = computeRedirectionToServer(request);
            response.sendRedirect(redirectionUrl);
        } else {
            HttpSession httpSession = request.getSession();
            if(httpSession!=null){
                sessionMappingStorage.removeBySessionId(httpSession.getId());
            }
            //直接退出的时候,也需要重定向到服务端
            String redirectionUrl = computeRedirectionToServer(request);
            response.sendRedirect(redirectionUrl);
        }
    }
    
    private boolean isBackChannelLogoutRequest(HttpServletRequest request) {
        return "POST".equals(request.getMethod())
                && !isMultipartRequest(request)
                && !StringUtils.isEmpty(request.getParameter(LOGOUT_REQUEST));
    }
    
    private void destroySession(HttpServletRequest request) throws ServletException {
        String logoutMessage;
        if (isFrontChannelLogoutRequest(request)) {
            //后续需要进行客户端私钥解密
            logoutMessage = request.getParameter(SAML_REQUEST);
        } else {
            logoutMessage =request.getParameter(LOGOUT_REQUEST);
        }

        //解析到登出消息
        String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");

        //获取凭证信息
        if (StringUtils.hasText(token)){
            //通过凭证信息,使相应的 session失效
           HttpSession httpSession= sessionMappingStorage.removeSessionByMappingId(token);
           if(httpSession!=null){
               try {
                   httpSession.invalidate();
               }catch (IllegalStateException e){
                   log.debug("the session has been logged out.");
               }
               request.logout();
           }
        }
    }
}

# 单点客户端协作

# 后端登录

# 登录流程

  • 通过凭证(通常为token)直接进入客户端后台,这个凭证可以是从前端传入,也可以是从上下文中获取.
  • 通过指定过滤器进行登录认证操作
  • 认证成功,顺滑执行后续操作。认证失败则重定向到服务端认证地址.

# 痛点与难点

  • 凭证的有效性对系统登录的可靠性影响较大。
  • 常见的是实时检验凭证,对网络资源造成无效开销(尤其是外网环境的系统)。

# 前端登录

# 登录流程

  • 前端操作,触发认证,重定向单点服务端认证保存前端地址;

  • 前端通过凭证(通常为cookie)进入单点服务端后台首次需要进行认证操作。之后这一步对用户来说无感知。

  • 单点服务端重定向到单点授权回调地址

  • 单点客户端凭证处理器进行登录认证操作

  • 认证成功,重定向到客户端前端地址;认证失败,则重定向到服务端认证地址.

# 痛点与难点

  • cookie存在跨域限制;
  • 客户端需要维护前端状态由于多次重定向,可能会导致前端状态丢失(比如打开的菜单等等)

# 单点登录理论指导

# 内部系统-内部规范

无主动授权流程,即不需要识别客户端;

  • 首选基于SpringSession的分布式会话方案;

  • 基于token实时校验方案;

    • 基于jjwt实现的方案;
    • 基于自定义token实现的方案;

# 外部系统-业界规范

需要主动授权,需要识别客户端;

  • oauth2单点登录的方案

  • 基于cas的方案

# 单点客户端接入实践

# 老版oauth2客户端官方接入改造

# 接入步骤

  • 引入依赖
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>
  • 启动类加入注解
@EnableOAuth2Sso
  • 添加配置文件
security:
  oauth2:
    #    sso:
    #      login-path: /
    client:
      clientId: ips
      clientSecret: ips123456
      scope: all
      user-authorization-uri: http://127.0.0.1:8090/atm/ssoServer/oauth2/authorize
      access-token-uri: http://127.0.0.1:8090/atm/ssoServer/oauth2/token
      clientAuthenticationScheme: header
    resource:
      #jwt的触发: key-value存在,或者key-uri存在, 则使用jwt的方案,对应于 DefaultTokenServices 的 ResourceServerTokenServices实现
      jwt:
        key-value: automannn
      #        key-uri: http://localhost:8080/server/oauth/token_key


      # 对应于UserInfoTokenServices的 ResourceServerTokenServices实现(直接根据token获取用户信息,默认没有,需要oauth2服务器自行扩展)
      #      client-id: user-client
      #      user-info-uri: http://localhost:8080/server/api/user
      #      token-type: Bearer

      #对应与RemoteTokenServices的 ResourceServerTokenServices实现
#      token-info-uri: http://127.0.0.1:8090/atm/ssoServer/oauth2/check_token
#      client-id: ips
#      client-secret: ips123456
  • 验证
访问 http://127.0.0.1:8081/client1/, 成功获取资源
访问 http://127.0.0.1:8081/client1/user, 能够正常获取用户信息

# 核心原理

  • 过滤器 Oauth2ClientAuthenticationProcessingFilter + Oauth2ClientContextFilter, 具体可看SpringSecurity章节中,关于oauth2过滤链的说明

# 常用改造点

{
	@Bean
	@ConditionalOnMissingBean
	/*xxx: 用户信息请求工厂,负责获取 token,授权码等流程*/
	/*xxx: 初始化时,会将oauth2客户端上下文注入*/
	/*xxx: 同时注入被保护资源详细信息*/
	public UserInfoRestTemplateFactory userInfoRestTemplateFactory(
			ObjectProvider&lt;List&lt;UserInfoRestTemplateCustomizer>> customizers,
			ObjectProvider&lt;OAuth2ProtectedResourceDetails> details,
			ObjectProvider&lt;OAuth2ClientContext> oauth2ClientContext) {
		return new DefaultUserInfoRestTemplateFactory(customizers, details,
				oauth2ClientContext);
	}
	
}

{
    @FunctionalInterface
    public interface UserInfoRestTemplateCustomizer {
    
        /**
         * Customize the rest template before it is initialized.
         * @param template the rest template
         */
        void customize(OAuth2RestTemplate template);
    
    }
}	

{
    @Override
	public OAuth2RestTemplate getUserInfoRestTemplate() {
		if (this.oauth2RestTemplate == null) {
			this.oauth2RestTemplate = createOAuth2RestTemplate(
					this.details == null ? DEFAULT_RESOURCE_DETAILS : this.details);
			this.oauth2RestTemplate.getInterceptors()
					.add(new AcceptJsonRequestInterceptor());
			AuthorizationCodeAccessTokenProvider accessTokenProvider = new AuthorizationCodeAccessTokenProvider();
			accessTokenProvider.setTokenRequestEnhancer(new AcceptJsonRequestEnhancer());
			this.oauth2RestTemplate.setAccessTokenProvider(accessTokenProvider);
			if (!CollectionUtils.isEmpty(this.customizers)) {
				AnnotationAwareOrderComparator.sort(this.customizers);
				for (UserInfoRestTemplateCustomizer customizer : this.customizers) {
					customizer.customize(this.oauth2RestTemplate);
				}
			}
		}
		return this.oauth2RestTemplate;
	}

}
	
# 自定义跳转授权参数(授权码模式时)
public class CustomAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider {
  @Override
  public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request) throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException, OAuth2AccessDeniedException {
    AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails)details;
    if (request.getAuthorizationCode() == null) {
      if (request.getStateKey() == null) {
          //重定向到授权地址获取授权码
        throw this.getRedirectForAuthorization(resource, request);
      }

      //本地获取授权码 (主要用于之前已经进行了授权操作的情况,如果已经授权,则设置授权码,否则跳转授权)
      this.obtainAuthorizationCode(resource, request);
    }

    //根据授权码获取accessToken
    OAuth2AccessToken oAuth2AccessToken = this.retrieveToken(request, resource,
            getParametersForTokenRequest(resource, request), this.getHeadersForTokenRequest(request));

    return oAuth2AccessToken;
  }

  private UserRedirectRequiredException getRedirectForAuthorization(AuthorizationCodeResourceDetails resource, AccessTokenRequest request) {
    TreeMap<String, String> requestParameters = new TreeMap();
    requestParameters.put("response_type", "code");
    requestParameters.put("client_id", resource.getClientId());
    String redirectUri = resource.getRedirectUri(request);
    if (redirectUri != null) {
      requestParameters.put("redirect_uri", redirectUri);
    }

    if (resource.isScoped()) {
      StringBuilder builder = new StringBuilder();
      List<String> scope = resource.getScope();
      if (scope != null) {
        Iterator scopeIt = scope.iterator();

        while(scopeIt.hasNext()) {
          builder.append((String)scopeIt.next());
          if (scopeIt.hasNext()) {
            builder.append(' ');
          }
        }
      }

      requestParameters.put("scope", builder.toString());
    }

    String clientId = resource.getClientId();
    String clientSecret = resource.getClientSecret();

    //配置自定义参数
    String authorization = "Basic " + new String((clientId + ":" + clientSecret).getBytes());
    requestParameters.put("access_token", authorization);

    UserRedirectRequiredException redirectException = new UserRedirectRequiredException(resource.getUserAuthorizationUri(), requestParameters);
    String stateKey = stateKeyGenerator.generateKey(resource);
    redirectException.setStateKey(stateKey);
    request.setStateKey(stateKey);
    redirectException.setStateToPreserve(redirectUri);
    request.setPreservedState(redirectUri);
    return redirectException;
  }
}
  • 通过UserInfoRestTemplateCustomizer, 将自定义的 授权码模式tokenProvider设置到获取token的 restTemplate中
AuthorizationCodeAccessTokenProvider authCodeProvider = new CustomAuthorizationCodeAccessTokenProvider();

authCodeProvider.setStateMandatory(false);
((OAuth2RestTemplate)restTemplate).setAccessTokenProvider(authCodeProvider);

# 自定义根据授权码获取token

public class CustomOAuth2ClientAuthenticationProcessingFilter extends OAuth2ClientAuthenticationProcessingFilter {

  protected OAuth2AccessToken retrieveToken(final AccessTokenRequest request, OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {
    try {
      this.authenticationHandler.authenticateTokenRequest(resource, form, headers);
      this.tokenRequestEnhancer.enhance(request, resource, form, headers);
      final ResponseExtractor<OAuth2AccessToken> delegate = this.getResponseExtractor();
      ResponseExtractor<OAuth2AccessToken> extractor = new ResponseExtractor<OAuth2AccessToken>() {
        public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException {
          if (response.getHeaders().containsKey("Set-Cookie")) {
            request.setCookie(response.getHeaders().getFirst("Set-Cookie"));
          }

          return (OAuth2AccessToken)delegate.extractData(response);
        }
      };
      return (OAuth2AccessToken)this.getRestTemplate().execute(this.getAccessTokenUri(resource, form), this.getHttpMethod(), this.getRequestCallback(resource, form, headers), extractor, form.toSingleValueMap());
    } catch (OAuth2Exception var8) {
      throw new OAuth2AccessDeniedException("Access token denied.", resource, var8);
    } catch (RestClientException var9) {
      throw new OAuth2AccessDeniedException("Error requesting access token.", resource, var9);
    }
  }
}
  • 生效同上

# 自定义获取用户凭证

/*xxx: 单点登录配置器 */
class SsoSecurityConfigurer {
  /*xxx: 添加 OAuth2ClientAuthenticationProcessingFilter,oauth客户端认证过滤器*/
  private OAuth2ClientAuthenticationProcessingFilter oauth2SsoFilter(
          OAuth2SsoProperties sso) {
    /*xxx: 从容器中,获取  userInfoRestTemplateFacotry*/
    OAuth2RestOperations restTemplate = this.applicationContext
            .getBean(UserInfoRestTemplateFactory.class).getUserInfoRestTemplate();
    /*xxx: 从容器中获取 资源服务器令牌服务*/
    ResourceServerTokenServices tokenServices = this.applicationContext
            .getBean(ResourceServerTokenServices.class);
    /*xxx: 实例化 oauth2客户端认证过滤器*/
    OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
            sso.getLoginPath());
    filter.setRestTemplate(restTemplate);
    /*xxx: 设置资源服务器令牌服务*/
    filter.setTokenServices(tokenServices);
    filter.setApplicationEventPublisher(this.applicationContext);
    return filter;
  }
}
  • 直接配置相应的bean即可,不配置的话,有默认的策略,分别是 jwt(defaultTokenServices),userInfoServices,remoteTokenServices
@Slf4j
@Component
public class CustomResourceServerTokenServicesImpl implements ResourceServerTokenServices {

    private final ResourceServerProperties resourceServerProperties;

    public CustomResourceServerTokenServicesImpl(ResourceServerProperties resourceServerProperties) {
        this.resourceServerProperties = resourceServerProperties;
    }

    @Override
    public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

        String userInfoUri = resourceServerProperties.getUserInfoUri();
        userInfoUri+="?access_token="+accessToken;
        RestTemplate restTemplate = new RestTemplate();
        String result = restTemplate.getForObject(userInfoUri, String.class);

        JSONObject bodyJson = JSONObject.parseObject(result);
        if (bodyJson == null || bodyJson.getJSONObject("data") == null) {
            log.error("从统一认证平台获取用户信息失败{}", bodyJson);
            throw new RuntimeException("从统一认证平台获取用户信息失败");
        }


        JSONObject jsonObject = bodyJson.getJSONObject("data").getJSONObject("thirdUser");
        User user = jsonObject.toJavaObject(User.class);

         //这里将user转为oauth2的authentication对象
        OAuth2Authentication oauth2Authentication = this.extractOauth2Authentication(user);
         return oauth2Authentication;
    }

    @Override
    public OAuth2AccessToken readAccessToken(String accessToken) {
        throw new UnsupportedOperationException("Not supported: read access token");
    }

    //逻辑源于 remoteTokenServices
    public OAuth2Authentication extractOauth2Authentication(User thirdUser) {
        Map<String, String> parameters = new HashMap();
        Set<String> scope = Collections.emptySet();
        Authentication user = this.extractAuthentication(thirdUser);
        String clientId = this.resourceServerProperties.getClientId();
        parameters.put("client_id", clientId);


        Set<String> resourceIds = Collections.emptySet();
        Collection<? extends GrantedAuthority> authorities = Collections.emptySet();;

        OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, (String)null, (Set)null, (Map)null);
        return new OAuth2Authentication(request, user);
    }

    public Authentication extractAuthentication(User thirdUser) {
        if (thirdUser !=null) {
            Object principal = thirdUser.getAccount();
            Collection<? extends GrantedAuthority> authorities = Collections.emptySet();

            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
            authenticationToken.setDetails(thirdUser);
            return authenticationToken;
        } else {
            return null;
        }
    }
}

# 更新默认凭证

  • 由于认证成功后,会发送相应事件,因此可以基于事件扩展
class AbstractAuthenticationProcessingFilter{
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
          throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

    /*xxx: 获取认证信息 */
    authResult = attemptAuthentication(request, response);

    /*xxx: 调用认证成功处理器对结果进行处理*/
    successfulAuthentication(request, response, chain, authResult);
      
  }

  protected void successfulAuthentication(HttpServletRequest request,
                                          HttpServletResponse response, FilterChain chain, Authentication authResult)
          throws IOException, ServletException {
      //登录成功后,默认写入安全上下文
    SecurityContextHolder.getContext().setAuthentication(authResult);

    rememberMeServices.loginSuccess(request, response, authResult);

    // Fire event
    if (this.eventPublisher != null) {
      eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
              authResult, this.getClass()));
    }

    successHandler.onAuthenticationSuccess(request, response, authResult);
  }
}
  • 基于事件,更新安全上下文
@Component
public class Oauth2AuthenticationChangeHandler implements ApplicationListener<InteractiveAuthenticationSuccessEvent> {

    @Autowired
    private AuthenticationService authenticationService;

    @Override
    public void onApplicationEvent(InteractiveAuthenticationSuccessEvent interactiveAuthenticationSuccessEvent) {
        Class<?> generatedBy = interactiveAuthenticationSuccessEvent.getGeneratedBy();
        if (generatedBy == OAuth2ClientAuthenticationProcessingFilter.class){
            
            //转换authentication类型
           OAuth2Authentication auth2Authentication = (OAuth2Authentication) interactiveAuthenticationSuccessEvent.getAuthentication();


            User thirdUser = (User) auth2Authentication.getUserAuthentication().getDetails();
            //方案二, 直接通过三方用户信息转为医保兼容的上下文凭证
            PortalUserDetails portalUserDetails = setPortalUserDetails(thirdUser);
            UsernamePasswordAuthenticationToken authenticationToken= new UsernamePasswordAuthenticationToken(portalUserDetails, null, portalUserDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        }
    }


    private PortalUserDetails setPortalUserDetails(User thirdUser) {
        PortalUserDetails portalUserDetails = new PortalUserDetails();
//        portalUserDetails.setOpterNo((rs.getData()).getOpterNo());
        portalUserDetails.setUserAcct(thirdUser.getAccount());
        portalUserDetails.setUactID(String.valueOf(thirdUser.getUserId()));
        portalUserDetails.setName(thirdUser.getRealName());
        portalUserDetails.setOrgUntID(thirdUser.getDeptId());
        portalUserDetails.setOrgName("默认组织");
        portalUserDetails.setOrgCodg("mzzz");
        //行政区划
        portalUserDetails.setPoolAreaCodg("000000");
        portalUserDetails.setDeptID(thirdUser.getDeptId());
        portalUserDetails.setDeptName("默认组织");
        portalUserDetails.setAdmDvs("000000");
        //父级组织
        portalUserDetails.setPrntOrgID("-1");
        portalUserDetails.setInsuTypePoolAreaMap(new HashMap<>());
        return portalUserDetails;
    }
}

# 新版oauth2客户端官方接入改造(社交登录)

# 接入步骤

  • 引入依赖
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
            <version>5.3.5.RELEASE</version>
        </dependency>
  • 添加配置文件
spring:
  security:
    oauth2:
      client:
        registration:
          natm:  # 自定义的 registrationId
            client-id: ips
            client-secret: ips123456
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: all  # 权限范围
            client-name: natm  # 显示名称
        provider:
          natm:  # 必须与 registrationId 同名
            authorization-uri: http://127.0.0.1:8090/atm/ssoServer/oauth2/authorize
            token-uri: http://127.0.0.1:8090/atm/ssoServer/oauth2/token
            user-info-uri: http://127.0.0.1:8090/atm/ssoServer/api/v1/getUserInfo
            user-name-attribute: username  # 用户名字段

# 生效原理解析

  • 配置类
@Configuration(proxyBeanMethods = false)
//该类位于spring-security-oauth2-client包中
@ConditionalOnBean(ClientRegistrationRepository.class) 
class OAuth2WebSecurityConfiguration {
    
  @Configuration(proxyBeanMethods = false)
  @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
  //未自定义过滤适配器的情况下,才进行配置
  static class OAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
      http.oauth2Login(Customizer.withDefaults());
      http.oauth2Client();
    }

  }
}
  • httpSecurity配置器
public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>> extends
	AbstractHttpConfigurer<OAuth2ClientConfigurer<B>, B> {

  private AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer = new AuthorizationCodeGrantConfigurer();
    
  @Override
  public void configure(B builder) {
    this.authorizationCodeGrantConfigurer.configure(builder);
  }

  public class AuthorizationCodeGrantConfigurer {
    private void configure(B builder) {
        //过滤器1 OAuth2AuthorizationRequestRedirectFilter
      OAuth2AuthorizationRequestRedirectFilter authorizationRequestRedirectFilter = createAuthorizationRequestRedirectFilter(builder);
      builder.addFilter(postProcess(authorizationRequestRedirectFilter));
      //过滤器2 OAuth2AuthorizationCodeGrantFilter
      OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = createAuthorizationCodeGrantFilter(builder);
      builder.addFilter(postProcess(authorizationCodeGrantFilter));
    }
  }
}
public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> extends
	AbstractAuthenticationFilterConfigurer<B, OAuth2LoginConfigurer<B>, OAuth2LoginAuthenticationFilter> {
  @Override
  public void configure(B http) throws Exception {
    String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri;
    if (authorizationRequestBaseUri == null) {
      authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
    }
    OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
            OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), authorizationRequestBaseUri);
    //过滤器1 OAuth2AuthorizationRequestRedirectFilter
    http.addFilter(this.postProcess(authorizationRequestFilter));

    //过滤器2 OAuth2LoginAuthenticationFilter, 由父类模板自动加到过滤链中
    OAuth2LoginAuthenticationFilter authenticationFilter = this.getAuthenticationFilter();

    super.configure(http);
  }
}

# oauth2Login()与oauth2Client()对比

在 Spring Security 中,当用户通过 OAuth 2.0 登录后,oauth2Client() 可以让你的应用自动使用获取到的访问令牌(Access Token)来调用第三方 API(如 Google Calendar)。下面用一个具体场景说明:

# 场景:用户登录后访问 Google 日历

假设你有一个应用:

  1. 用户通过 Google 账号登录(使用 oauth2Login()
  2. 登录后需要展示用户的 Google 日历事件
# 1. 配置 oauth2Login()(用户登录)
@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2  // 启用 OAuth 登录
                .loginPage("/login")
                .userInfoEndpoint()
                    .userService(customOAuth2UserService)
            )
            .authorizeRequests()
                .anyRequest().authenticated();
        
        return http.build();
    }
}

用户点击 "Google 登录" 后,Spring Security 会:

  • 处理 OAuth 2.0 授权流程
  • 获取访问令牌(Access Token)和用户信息
  • 将令牌存储在 OAuth2AuthorizedClient 对象中
# 2. 配置 oauth2Client()(访问 API)
http
    .oauth2Client(oauth2 -> oauth2  // 启用客户端功能
        .authorizedClientService(authorizedClientService)
    );
# 3. 在控制器中调用 Google Calendar API
@GetMapping("/calendar")
public String getCalendarEvents(
    @RegisteredOAuth2AuthorizedClient("google") 
    OAuth2AuthorizedClient authorizedClient) { // 自动注入令牌
    
    // 从授权客户端获取访问令牌
    String accessToken = authorizedClient.getAccessToken().getTokenValue();
    
    // 使用令牌调用 Google Calendar API
    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.setBearerAuth(accessToken); // 自动添加 Bearer 令牌
    
    ResponseEntity&lt;String> response = restTemplate.exchange(
        "https://www.googleapis.com/calendar/v3/calendars/primary/events",
        HttpMethod.GET,
        new HttpEntity&lt;>(headers),
        String.class
    );
    
    return response.getBody(); // 返回日历事件
}
# 关键机制解析
  1. 令牌自动注入
  • @RegisteredOAuth2AuthorizedClient("google") 注解会自动获取当前登录用户的 Google 访问令牌
  • 令牌来源:用户登录时通过 oauth2Login() 获取并存储的令牌
  1. 令牌自动传递
   headers.setBearerAuth(accessToken); // 将令牌放入 Authorization 头
   
   //实际请求相当于
   GET /calendar/v3/calendars/primary/events
   Authorization: Bearer ya29.a0AfB_byz3... (Google 访问令牌)
  1. 令牌自动刷新(可选): 如果配置了刷新令牌(Refresh Token),当访问令牌过期时,Spring Security 会自动刷新令牌:
   http.oauth2Client(oauth2 -> oauth2
       .authorizedClientService(authorizedClientService)
       .authorizationCodeGrant()
           .accessTokenResponseClient(accessTokenResponseClient) // 可配置令牌刷新
   );

# oauth2三方登录与三方授权的关键区别(自己理解)

  • 三方登录只能使用某一个供应商登录
  • 但是三方授权可以使用多个供应商进行授权
  • 这也许是官方要使二者共存的原因

# 登录流程

  • 涉及过滤器 OAuth2AuthorizationRequestRedirectFilter、OAuth2LoginAuthenticationFilter
  • 登录过程与 AuthorizationCodeGrantFilter 无关
  • OAuth2AuthorizationRequestRedirectFilter 用于导向授权服务器
  • OAuth2LoginAuthenticationFilter: 根据授权码获取access_token,然后根据access_token获取用户信息,这个过程是通过authenticationProvider完成的
public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*";

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        //核心逻辑在此处完成: 授权码换取access_token,然后根据access_token获取用户信息
        OAuth2LoginAuthenticationToken authenticationResult =
                (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);

        this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
    }
}
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        
        //根据授权码,获取 accessToken
        authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
                .authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
                        loginAuthenticationToken.getClientRegistration(),
                        loginAuthenticationToken.getAuthorizationExchange()));

        //根据 accessToken 获取用户信息
        OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
                loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));

        //最终返回的此认证成功令牌
        OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
                loginAuthenticationToken.getClientRegistration(),
                loginAuthenticationToken.getAuthorizationExchange(),
                oauth2User,
                mappedAuthorities,
                accessToken,
                authorizationCodeAuthenticationToken.getRefreshToken());
        authenticationResult.setDetails(loginAuthenticationToken.getDetails());

        return authenticationResult;
    }
}
public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
        
        ResponseEntity<Map<String, Object>> response = this.restOperations.exchange(request, RESPONSE_TYPE);

        Map<String, Object> userAttributes = response.getBody();

        return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
    }
}

# cas客户端官方接入改造(spring-security-cas)

# 引入依赖

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-cas</artifactId>
            <version>5.3.5.RELEASE</version>
        </dependency>

# 配置

  • springSecurity并未提供cas的开箱即用的配置模块,因此需要自己进行配置
@Configuration
@Profile("cas")
public class CasSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private CasAuthenticationProvider casAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers( "/public/**").permitAll() // 公开URL
                .anyRequest().authenticated() // 其他所有URL需要认证
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint) // 使用CAS入口点
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/public/") // 注销后跳转
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .and()
                .csrf().disable(); // CAS协议可能涉及跨站请求,根据实际情况决定是否禁用CSRF

        // 添加CAS过滤器链
        http.addFilterAt(casFilter(), CasAuthenticationFilter.class);
    }

    // 配置处理ST的过滤器(位于 /login/cas)
    private CasAuthenticationFilter casFilter() throws Exception {
        CasAuthenticationFilter filter = new CasAuthenticationFilter();
        filter.setServiceProperties(serviceProperties());
        filter.setAuthenticationManager(authenticationManager());
        // 如果CAS服务器重定向回来的URL不是默认的/login/cas,需要设置filter.setFilterProcessesUrl("/your-custom-url");
        return filter;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(casAuthenticationProvider);
    }

    @Bean
    //定义应用(服务)在 CAS 服务器上的注册信息。
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        // 应用接收ST的完整URL,必须与CAS服务器注册的服务URL一致
        serviceProperties.setService(getApplicationContext().getEnvironment().getProperty("cas.client.service"));
        // 通常设为false。true表示强制重新登录(即使有SSO会话),用于敏感操作
        serviceProperties.setSendRenew(false);
        return serviceProperties;
    }

    @Bean
    //定义当需要认证时如何开始 CAS 流程
    public AuthenticationEntryPoint authenticationEntryPoint(ServiceProperties serviceProperties) {
        CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
        // CAS服务器的登录URL
        entryPoint.setLoginUrl(getApplicationContext().getEnvironment().getProperty("cas.client.casServerUrlLogin"));
        entryPoint.setServiceProperties(serviceProperties);
        return entryPoint;
    }

    @Bean
    //定义如何验证从 CAS 服务器收到的 Service Ticket (ST)
    public TicketValidator ticketValidator() {
        // CAS服务器的基础URL
        return new Cas30JsonServiceTicketValidator(getApplicationContext().getEnvironment().getProperty("cas.client.casServerUrl")){
            @Override
            protected Assertion parseResponseFromServer(String response) throws TicketValidationException {
                JSONObject jsonObject = JSON.parseObject(response);
                return new AssertionImpl(jsonObject.getString("username"));
            }
        };
    }

    @Bean
    //核心处理器,负责验证 ST、获取用户信息并构建 Authentication 对象
    public CasAuthenticationProvider casAuthenticationProvider(
            ServiceProperties serviceProperties,
            TicketValidator ticketValidator,
            UserDetailsService userDetailsService) {

        CasAuthenticationProvider provider = new CasAuthenticationProvider();
        provider.setServiceProperties(serviceProperties);
        provider.setTicketValidator(ticketValidator);
        // 关键:用于根据CAS返回的用户名加载用户详细信息和权限
        provider.setUserDetailsService(userDetailsService);
        // 设置一个唯一的key(任意字符串),用于区分Provider实例
        provider.setKey(getApplicationContext().getEnvironment().getProperty("cas.client.serviceId"));
        return provider;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        // 示例:使用内存用户存储。实际项目中通常连接数据库或LDAP
        UserDetails user = User.withUsername("admin")
                .password("") // CAS认证,本地密码通常为空或不重要,但字段不能为null
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
}
cas:
  client:
    service: ips
    serviceId: ips
    casServerUrl: http://127.0.0.1:8090/atm/ssoServer
    casServerUrlLogin: http://127.0.0.1:8090/atm/ssoServer/cas/login
    casServerUrlLogout: http://127.0.0.1:8090/atm/ssoServer/logout

# cas流程关键点

  • ssoServer登录时写入tgt到session中
  • http://127.0.0.1:8090/atm/ssoServer/cas/login 并非基于filter实现,而是基于controller实现
  • 服务端校验st时,使用的也是controller
  • 客户端的stValidator使用的自定义方案,用于从服务端解析非标响应;
//客户端validator
class Test{
    @Bean
    //定义如何验证从 CAS 服务器收到的 Service Ticket (ST)
    public TicketValidator ticketValidator() {
        // CAS服务器的基础URL
        return new Cas30JsonServiceTicketValidator(getApplicationContext().getEnvironment().getProperty("cas.client.casServerUrl")){
            @Override
            protected Assertion parseResponseFromServer(String response) throws TicketValidationException {
                JSONObject jsonObject = JSON.parseObject(response);
                return new AssertionImpl(jsonObject.getString("username"));
            }
        };
    }
}
//服务器端-颁发st端口
@RequestMapping(AtmOauth2ConfigConstants.CAS_SERVICE_BASE_NAME)
public class GrantStController {
    @GetMapping(value = {"authorize","login"})
    //授权完成后,直接重定向到客户端验证地址
    public void grantSt(HttpServletRequest request, HttpServletResponse response, String client, String service,
                        Authentication authentication) throws IOException {
        
    }
}
//服务器端-验证st端口
@RequestMapping(value={AtmOauth2ConfigConstants.CAS_SERVICE_BASE_NAME,"/p3"})
@ResponseBody
public class TicketValidationController {
    @RequestMapping(value={"verify","serviceValidate"})
    @NoopHttpReturnValueEnhancer
    public Map<String, Object> validate(HttpServletRequest request, HttpServletResponse response, String client,
                                        String ticket) {
        
    }
}