# 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的方案