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