SpringSecurity源码学习七:OAuth 2.0登录


Spring Security OAuth2 是一个基于 Spring Security 的开源框架,用于实现 OAuth2 认证和授权的功能。OAuth2 是一种授权协议,用于允许用户授权第三方应用程序访问其受保护的资源,而无需共享其凭据。

1. 代码示例

  1. 添加依赖项:
    在您的项目的 Maven 或 Gradle 构建文件中添加以下依赖项:
xml
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-oauth2-client</artifactId>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
  1. 配置应用程序属性:
    在您的 application.properties 或 application.yml 文件中添加以下配置:
yaml
   spring:
     security:
       oauth2:
         client:
           registration:
             wechat:
               client-id: your-client-id
               client-secret: your-client-secret
               client-name: WeChat
               scope: snsapi_login
               redirect-uri: /login/oauth2/code/wechat
           provider:
             wechat:
               authorization-uri: https://open.weixin.qq.com/connect/qrconnect
               token-uri: https://api.weixin.qq.com/sns/oauth2/access_token
               user-info-uri: https://api.weixin.qq.com/sns/userinfo
               user-name-attribute: openid

替换 your-client-id 和 your-client-secret 为您的微信开放平台应用程序的实际值。

  1. 创建登录回调处理程序:
    创建一个类来处理微信登录回调,实现 OAuth2UserService 接口,并覆盖 loadUser() 方法以根据微信用户信息创建用户对象。
@Service
   public class WeChatOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
        @Override
       public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
           // 根据 userRequest 获取微信用户信息
           // 创建用户对象并返回
       }
   }
  1. 配置 Spring Security:
    创建一个类来配置 Spring Security,扩展 WebSecurityConfigurerAdapter 类,并覆盖 configure() 方法以配置安全规则和 OAuth2 登录。
@Configuration
   public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
       private WeChatOAuth2UserService weChatOAuth2UserService;
        @Override
       protected void configure(HttpSecurity http) throws Exception {
           http
               .authorizeRequests()
                   .antMatchers("/login", "/login/oauth2/code/wechat")
                   .permitAll()
                   .anyRequest().authenticated()
                   .and()
               .oauth2Login()
                   .loginPage("/login")
                   .userInfoEndpoint()
                       .userService(weChatOAuth2UserService);
       }
   }

在上述配置中,我们允许 /login 和 /login/oauth2/code/wechat 路径的所有请求,其他请求需要经过身份验证。使用 oauth2Login() 方法启用 OAuth2 登录,并指定登录页面和自定义的 OAuth2UserService 实现。

  1. 创建登录页面:
    创建一个登录页面,例如 login.html ,用于显示微信登录按钮并触发 OAuth2 登录流程。
html
   <html>
   <body>
       <h1>欢迎登录</h1>
       <a href="/login/oauth2/authorization/wechat">微信登录</a>
   </body>
   </html>

在上述代码中,我们使用 /login/oauth2/authorization/wechat 链接来触发微信登录流程。

完成上述步骤后,您的 Spring Boot 应用程序将支持使用微信进行登录。用户访问登录页面并选择微信登录,将被重定向到微信登录页面进行授权。一旦授权成功,用户将被重定向回您的应用程序,并且您的 WeChatOAuth2UserService 将被调用来加载用户信息。根据需要,您可以将用户信息存储在数据库中或执行其他操作。

我们也可以把jwt信息放入到对象OAuth2User中返回给页面,后边请求可以通过自定义过滤器拦截jwt信息校验。

  1. 创建自定义过滤器:
    创建一个类来实现 javax.servlet.Filter 接口,并实现 doFilter() 方法来处理自定义的过滤逻辑。
@Component
public class TokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 获取请求中的 Token
        String token = extractTokenFromRequest((HttpServletRequest) request);
         // 校验 Token
        if (isValidToken(token)) {
            // Token 有效,继续处理请求
            chain.doFilter(request, response);
        } else {
            // Token 无效,返回错误响应
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        }
    }
     private String extractTokenFromRequest(HttpServletRequest request) {
        // 从请求中提取 Token,例如从请求头或请求参数中获取
        // 返回 Token 字符串
    }
     private boolean isValidToken(String token) {
        // 校验 Token 的有效性,例如验证签名、过期时间等
        // 返回校验结果
    }
}
  1. 配置过滤器:
    在 Spring Boot 的配置类中,使用 @Configuration 注解,并使用 @WebFilter 注解来配置自定义过滤器。
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<TokenFilter> tokenFilterRegistration() {
        FilterRegistrationBean<TokenFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new TokenFilter());
        registration.addUrlPatterns("/api/*"); // 配置过滤的路径
        return registration;
    }
}
  1. 配置 Spring Security:
    在 Spring Security 的配置类中,使用 HttpSecurity 对象来配置安全规则,并使用 .addFilterBefore() 方法将自定义过滤器添加到过滤器链中。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .and()
            .addFilterBefore(new TokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

在上述代码中,我们创建了一个名为 TokenFilter 的自定义过滤器,并在 doFilter() 方法中实现了自定义的过滤逻辑,包括从请求中提取 Token 和校验 Token 的有效性。然后,在 FilterConfig 配置类中,使用 @WebFilter 注解将自定义过滤器配置为 Spring Bean,并通过 FilterRegistrationBean 注册到过滤器链中。接下来,在 Spring Security 配置类中,使用 HttpSecurity 对象配置安全规则,并使用 .addFilterBefore() 方法将自定义过滤器添加到过滤器链中。

请注意,上述代码仅提供了基本的配置示例,实际使用中可能需要根据具体需求进行调整和扩展。

2. 源码解析

使用Oauth2做登录的时候,主要涉及到以下两个过滤器:

  1. OAuth2AuthorizationRequestRedirectFilter :重定向过滤器,即当未认证时,重定向到登录页。当我们点击页面上的微信登录的时候,请求会流转到此过滤器。

  2. OAuth2LoginAuthenticationFilter:授权登录过滤器,处理指定的授权登录。当我们微信登录成功后,会回调到我们的服务,此时请求会流转到此过滤器。

2.1 OAuth2AuthorizationRequestRedirectFilter

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		try {
			//构建第三方授权信息请求对象
			OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
			if (authorizationRequest != null) {
				//发起重定向,到第三方
				this.sendRedirectForAuthorization(request, response, authorizationRequest);
				return;
			}
		}

上边是主要逻辑,以我们上边的示例为例,这段逻辑就是从配置文件yml中拿到微信的配置,发起第三方调用。

	@Override
	public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
		//获取registrationId
		String registrationId = this.resolveRegistrationId(request);
		if (registrationId == null) {
			return null;
		}
		//获取action参数,默认值是: login
		String redirectUriAction = getAction(request, "login");
		return resolve(request, registrationId, redirectUriAction);
	}
	private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
			OAuth2AuthorizationRequest authorizationRequest) throws IOException {
		if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
			//保存本次请求相关的信息,以用于三方平台回调时可以再次获取,例如当回调时需要检查state参数是否一致,以保证安全;
			this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
		}
		//调转到第三方登录页面
		this.authorizationRedirectStrategy.sendRedirect(request, response,
				authorizationRequest.getAuthorizationRequestUri());
	}
	@Override
	public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
		String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
		redirectUrl = response.encodeRedirectURL(redirectUrl);
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Redirecting to %s", redirectUrl));
		}
		//重定向到第三方授权登录页面
		response.sendRedirect(redirectUrl);
	}

可以看到,这个主要是拼接参数,重定向到第三方登录页面,比如微信登录页面。

2.2 OAuth2LoginAuthenticationFilter

OAuth2LoginAuthenticationFilter没有重写AbstractAuthenticationProcessingFilter的doFilter方法,我们看抽象类AbstractAuthenticationProcessingFilter的doFilter方法。

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
			//attemptAuthentication是抽象方法,可被子类重写
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				return;
			}
			//成功后会
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// Authentication success
			if (this.continueChainBeforeSuccessfulAuthentication) {
				chain.doFilter(request, response);
			}
			//保存用户信息等
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
			this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, ex);
		}
	}

OAuth2LoginAuthenticationFilter类重写了attemptAuthentication方法。

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		//参数集合
		MultiValueMap<String, String> params = org.springframework.security.oauth2.client.web.OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
		if (!org.springframework.security.oauth2.client.web.OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
			OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		//根据state参数从会话中查询授权登录之前保存的请求对象(请求对象也有state参数),如果找不到则抛出异常:AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE
		OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository
				.removeAuthorizationRequest(request, response);
		if (authorizationRequest == null) {
			OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		//获取ClientRegistration信息,配置文件中的第三方配置信息
		String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
		ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
		if (clientRegistration == null) {
			OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
					"Client Registration not found with Id: " + registrationId, null);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		// @formatter:off
		String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
				.replaceQuery(null)
				.build()
				.toUriString();
		// @formatter:on
		OAuth2AuthorizationResponse authorizationResponse = org.springframework.security.oauth2.client.web.OAuth2AuthorizationResponseUtils.convert(params,
				redirectUri);
		Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
		//构造认证请求,然后使用工厂模式执行认证,这个和用户名密码认证是一样的
		OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
				new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
		authenticationRequest.setDetails(authenticationDetails);
		//认证
		OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
				.getAuthenticationManager().authenticate(authenticationRequest);
		OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(
				authenticationResult.getPrincipal(), authenticationResult.getAuthorities(),
				authenticationResult.getClientRegistration().getRegistrationId());
		oauth2Authentication.setDetails(authenticationDetails);
		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
				authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
				authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());

		this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
		return oauth2Authentication;
	}

这段逻辑前半部分主要做了配置获取,认证请求的构建,主要逻辑是认证。也就是ProviderManager的authenticate()方法。

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		//支持多种认证,遍历所有AuthenticationProvider
		for (org.springframework.security.authentication.AuthenticationProvider provider : getProviders()) {
			//匹配当前的Authentication
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				//执行匹配到的AuthenticationProvider逻辑
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}

核心逻辑是provider.authenticate(authentication),我们继续往下看。具体实现是OAuth2LoginAuthenticationProvider的authenticate()方法。

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken loginAuthenticationToken = (org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken) authentication;
		// Section 3.1.2.1 Authentication Request -
		// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest scope
		// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
		if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes()
				.contains("openid")) {
			// This is an OpenID Connect Authentication Request so return null
			// and let OidcAuthorizationCodeAuthenticationProvider handle it instead
			return null;
		}
		//构建OAuth2AuthorizationCodeAuthenticationToken对象
		org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
		try {
			authorizationCodeAuthenticationToken = (org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
					.authenticate(new org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken(
							loginAuthenticationToken.getClientRegistration(),
							loginAuthenticationToken.getAuthorizationExchange()));
		}
		catch (OAuth2AuthorizationException ex) {
			OAuth2Error oauth2Error = ex.getError();
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}

		//构建OAuth2User对象,拿到用户信息
		OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
		Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
		//可以自己实现loadUser接口,自定义逻辑
		OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
				loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
		Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
				.mapAuthorities(oauth2User.getAuthorities());
		//构建认证结果
		org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken authenticationResult = new org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken(
				loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
				oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
		authenticationResult.setDetails(loginAuthenticationToken.getDetails());
		return authenticationResult;
	}

我们在这段逻辑中看到了this.userService.loadUser,OAuth2UserService的方法loadUser()方法我们可以自定义实现。也就是上边代码示例的第三点。

后续逻辑就是认证成功后的通用逻辑,基本上核心源码就是这些。里边还有很多细节点,比如令牌存储与生产,授权令牌与资源的安全配置,自定义认证成功后处理器中完成用户匹配等等。详细逻辑可以自行查看源码。

3. 总结

Spring Security OAuth2 登录的原理如下:

  1. 用户访问应用程序的登录页面,并选择使用 OAuth2 登录。
  2. 应用程序将用户重定向到授权服务器,以进行身份验证和授权。
  3. 用户在授权服务器上进行身份验证,并授权应用程序访问其受保护的资源。
  4. 授权服务器将授权码或访问令牌返回给应用程序。
  5. 应用程序使用授权码或访问令牌与授权服务器进行通信,以获取用户信息或访问受保护的资源。
  6. 应用程序使用用户信息进行登录,并为用户创建会话或授权访问受保护的资源。

在 Spring Security OAuth2 中,配置文件中定义了客户端信息、授权服务器信息和资源服务器信息。客户端信息包括客户端ID和客户端密钥,用于与授权服务器进行身份验证和授权。授权服务器信息包括授权服务器的URL和令牌端点,用于与授权服务器进行通信。资源服务器信息包括资源服务器的ID和受保护资源的URL,用于限制对受保护资源的访问。

通过配置 Spring Security OAuth2,应用程序可以使用授权服务器进行用户身份验证和授权,并使用访问令牌来访问受保护的资源。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值