# 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<List<UserInfoRestTemplateCustomizer>> customizers,
ObjectProvider<OAuth2ProtectedResourceDetails> details,
ObjectProvider<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 日历
假设你有一个应用:
- 用户通过 Google 账号登录(使用
oauth2Login()
) - 登录后需要展示用户的 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<String> response = restTemplate.exchange(
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
HttpMethod.GET,
new HttpEntity<>(headers),
String.class
);
return response.getBody(); // 返回日历事件
}
# 关键机制解析
- 令牌自动注入:
@RegisteredOAuth2AuthorizedClient("google")
注解会自动获取当前登录用户的 Google 访问令牌- 令牌来源:用户登录时通过
oauth2Login()
获取并存储的令牌
- 令牌自动传递:
headers.setBearerAuth(accessToken); // 将令牌放入 Authorization 头
//实际请求相当于
GET /calendar/v3/calendars/primary/events
Authorization: Bearer ya29.a0AfB_byz3... (Google 访问令牌)
- 令牌自动刷新(可选): 如果配置了刷新令牌(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) {
}
}