Spring Boot 接入 GitHub 第三方登录,只要两行配置!

Stella981
• 阅读 879

松哥原创的 Spring Boot 视频教程已经杀青,感兴趣的小伙伴戳这里-->Spring Boot+Vue+微人事视频教程


本文地址:https://www.zyc.red/Spring/Security/OAuth2/OAuth2-Client/

松哥在四月份出过一个 OAuth2 教程(公号后台回复 OAuth2 可以获取),里边也和大家分享了 GitHub 第三方登录,但是我用的是一个更加通用的方式,就是自己配置各种回调,自己去请求各种数据,效果虽然实现了,但是比较麻烦。最近松哥在网上看到一篇文章,里边 Spring Security 自带的 OAuth2 登录功能,再结合 Spring Boot 的自动化配置,这种实现方式只需要在 application.properties 中配置一下 clientid 和 clientsecret 即可,今天就和大家分享一下这种方案,以下是正文(文章细节可能不是很详细,但是思路没问题)。

概述

OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。网上有很多关于OAuth协议的讲解,这里就不在详细解释OAuth相关的概念了,不了解的小伙伴可以在公号后台回复 OAuth2 获取教程链接。

Spring-Security 对 OAuth2.0的支持

截止到本文撰写的日期为止,Spring已经提供了对OAuth提供的支持(spring-security-oauth),但是该工程已经被废弃了,因为Spring-Security工程提供了最新的OAuth2.0支持。如果你的项目中使用了过期的Spring-Security-OAuth,请参考《OAuth 2.0迁移指南》,本文将对OAuth2.0中的客户端模式进行原理分析,结合Spring官方指南中提供了一个简单的基于spring-boot与oauth2.0集成第三方应用登录的案例(spring-boot-oauth2),一步一步分析其内部实现的原理。

创建GitHub OAuth Apps

在Github OAuth Apps中创建一个新的应用

Spring Boot 接入 GitHub 第三方登录,只要两行配置!

这个应用相当于我们自己的应用(客户端),被注册在Github(授权服务器)中了,如果我们应用中的用户有github账号的话,则可以基于oauth2来登录我们的系统,替代原始的用户名密码方式。在官方指南的例子中,使用spring-security和oauth2进行社交登陆只需要在你的pom文件中加入以下几个依赖即可

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-security</artifactId></dependency><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-web</artifactId></dependency>

然后在配置文件中填上刚刚注册的应用的clientId和clientSecret

spring:  security:    oauth2:      client:        registration:          github:            clientId: github-client-id            clientSecret: github-client-secret

紧接着就像普通的spring-security应用一样,继承WebSecurityConfigurerAdapter,进行一些简单的配置即可

@SpringBootApplication@RestControllerpublic class SocialApplication extends WebSecurityConfigurerAdapter {    // ...    @Override    protected void configure(HttpSecurity http) throws Exception {     // @formatter:off        http            .authorizeRequests(a -> a                .antMatchers("/", "/error", "/webjars/**").permitAll()                .anyRequest().authenticated()            )            .exceptionHandling(e -> e                .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))            )            .oauth2Login();        // @formatter:on    }}

也就是说我们只需要添加maven依赖以及继承WebSecurityConfigurerAdapter进行一些简单的配置,一个oauth2客户端应用就构建完成了。接下来按照指南上的步骤点击页面的github登录链接我们的页面就会跳转到github授权登录页,等待用户授权完成之后浏览器重定向到我们的callback URL最终请求user信息端点即可访问到刚刚登入的github用户信息,整个应用的构建是如此的简单,背后的原理是什么呢?接下来我们开始分析。还是和以前一样,我们在配置文件中将security的日志级别设置为debug

logging:  level:    org.springframework.security: debug

重新启动应用之后,从控制台输出中我们可以看到与普通spring-security应用不同的地方在于整个过滤链多出了以下几个过滤器

OAuth2AuthorizationRequestRedirectFilterOAuth2LoginAuthenticationFilter

联想oauth2的授权码模式以及这两个过滤器的名字,熟悉spring-security的同学心中肯定已经有了一点想法了。对没错,spring-security对客户端模式的支持完全就是基于这两个过滤器来实现的。现在我们来回想以下授权码模式的执行流程

  1. 用户在客户端页面点击三方应用登录按钮(客户端就是我们刚刚注册的github应用)

  2. 页面跳转到三方应用注册的授权方页面(授权服务器即github)

  3. 用户登入授权后, github调用我们应用的回调地址(我们刚刚注册github应用时填写的回调地址)

  4. 第三步的回调地址中github会将code参数放到url中, 接下来我们的客户端就会在内部拿这个code再次去调用github的access_token地址获取令牌

上面就是标准的authorization_code授权模式,OAuth2AuthorizationRequestRedirectFilter的作用就是上面步骤中的1.2步的合体,当用户点击页面的github授权url之后,OAuth2AuthorizationRequestRedirectFilter匹配这个请求,接着它会将我们配置文件中的clientId、scope以及构造一个state参数(防止csrf攻击)拼接成一个url重定向到github的授权url,OAuth2LoginAuthenticationFilter的作用则是上面3.4步骤的合体,当用户在github的授权页面授权之后github调用回调地址,OAuth2LoginAuthenticationFilter匹配这个回调地址,解析回调地址后的code与state参数进行验证之后内部拿着这个code远程调用github的access_token地址,拿到access_token之后通过OAuth2UserService获取相应的用户信息(内部是拿access_token远程调用github的用户信息端点)最后将用户信息构造成Authentication被SecurityContextPersistenceFilter过滤器保存到HttpSession中。下面我们就来看一下这两个过滤器内部执行的原理

OAuth2AuthorizationRequestRedirectFilter

public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {        ......省略部分代码 @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;   }  } catch (Exception failed) {   this.unsuccessfulRedirectForAuthorization(request, response, failed);   return;  }        ......省略部分代码}

通过authorizationRequestResolver解析器解析请求,解析器的默认实现是DefaultOAuth2AuthorizationRequestResolver,核心解析方法如下

// 第一步解析@Overridepublic OAuth2AuthorizationRequest resolve(HttpServletRequest request) {    // 通过内部的authorizationRequestMatcher来解析当前请求中的registrationId    // 也就是/oauth2/authorization/github中的github    String registrationId = this.resolveRegistrationId(request);    String redirectUriAction = getAction(request, "login");    return resolve(request, registrationId, redirectUriAction);}// 第二步解析private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {    if (registrationId == null) {        return null;    } // 根据传入的registrationId找到注册的应用信息    ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);    if (clientRegistration == null) {        throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);    }    Map<String, Object> attributes = new HashMap<>();    attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());    OAuth2AuthorizationRequest.Builder builder;    // 根据不同的AuthorizationGrantType构造不同的builder    if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {        builder = OAuth2AuthorizationRequest.authorizationCode();        Map<String, Object> additionalParameters = new HashMap<>();        if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) &&            clientRegistration.getScopes().contains(OidcScopes.OPENID)) {            // 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.            addNonceParameters(attributes, additionalParameters);        }        if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {            addPkceParameters(attributes, additionalParameters);        }        builder.additionalParameters(additionalParameters);    } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {        builder = OAuth2AuthorizationRequest.implicit();    } else {        throw new IllegalArgumentException("Invalid Authorization Grant Type ("  +                                           clientRegistration.getAuthorizationGrantType().getValue() +                                           ") for Client Registration with Id: " + clientRegistration.getRegistrationId());    }    String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);    OAuth2AuthorizationRequest authorizationRequest = builder        .clientId(clientRegistration.getClientId())        .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())        .redirectUri(redirectUriStr)        .scopes(clientRegistration.getScopes())        // 生成随机state值        .state(this.stateGenerator.generateKey())        .attributes(attributes)        .build();    return authorizationRequest;}

DefaultOAuth2AuthorizationRequestResolver判断请求是否是授权请求,最终返回一个OAuth2AuthorizationRequest对象给OAuth2AuthorizationRequestRedirectFilter,如果OAuth2AuthorizationRequest不为null的话,说明当前请求是一个授权请求,那么接下来就要拿着这个请求重定向到授权服务器的授权端点了,下面我们接着看OAuth2AuthorizationRequestRedirectFilter发送重定向的逻辑

private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,                                          OAuth2AuthorizationRequest authorizationRequest) throws IOException {    if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {        this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);    }    this.authorizationRedirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());}
  1. 如果当前是授权码类型的授权请求那么就需要将这个请求信息保存下来, 因为接下来授权服务器回调我们需要用到这个授权请求的参数进行校验等操作(比对state), 这里是通过authorizationRequestRepository保存授权请求的, 默认的保存方式是通过HttpSessionOAuth2AuthorizationRequestRepository保存在httpsession中的, 具体的保存逻辑很简单, 这里就不细说了。

  2. 保存完成之后就要开始重定向到授权服务端点了, 这里默认的authorizationRedirectStrategy是DefaultRedirectStrategy, 重定向的逻辑很简单, 通过response.sendRedirect方法使前端页面重定向到指定的授权

    public void sendRedirect(HttpServletRequest request, HttpServletResponse response,                         String url) throws IOException {    String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);    redirectUrl = response.encodeRedirectURL(redirectUrl);    if (logger.isDebugEnabled()) {        logger.debug("Redirecting to '" + redirectUrl + "'");    }    response.sendRedirect(redirectUrl);}

OAuth2AuthorizationRequestRedirectFilter处理逻辑讲完了,下面我们对它处理过程做一个总结

  1. 通过内部的OAuth2AuthorizationRequestResolver解析当前的请求,返回一个OAuth2AuthorizationRequest对象,如果当前请求是授权端点请求,那么就会返回一个构造好的对象,包含我们的client_id、state、redirect_uri参数,如果对象为null的话,那么就说明当前请求不是授权端点请求。注意如果OAuth2AuthorizationRequestResolver不为null的话,OAuth2AuthorizationRequestResolver内部会将其保存在httpsession中这样授权服务器在调用我们的回调地址时我们就能从httpsession中取出请求将state进行对比以防csrf攻击。

  2. 如果第一步返回的OAuth2AuthorizationRequest对象不为null的话,接下来就会通过response.sendRedirect的方法将OAuth2AuthorizationRequest中的授权端点请求发送到前端的响应头中然后浏览器就会重定向到授权页面,等待用户授权。

OAuth2LoginAuthenticationFilter

public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {    @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)   throws AuthenticationException {  MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());        // 如果请求参数中没有state和code参数,说明当前请求是一个非法请求  if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {   OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);   throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());  }  // 从httpsession中取出OAuth2AuthorizationRequestRedirectFilter中保存的授权请求,        // 如果找不到的话说明当前请求是非法请求  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());  }          // 如果当前注册的应用中找不到授权请求时的应用了,那么也是一个不正确的请求  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());  }  String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))    .replaceQuery(null)    .build()    .toUriString();  OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);  Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);  OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(    clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));  authenticationRequest.setDetails(authenticationDetails);          // 将未认证的OAuth2LoginAuthenticationToken委托给AuthenticationManager        // 选择合适的AuthenticationProvider来对其进行认证,这里的AuthenticationProvider是        // OAuth2LoginAuthenticationProvider  OAuth2LoginAuthenticationToken authenticationResult =   (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);          // 将最终的认证信息封装成OAuth2AuthenticationToken  OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(   authenticationResult.getPrincipal(),   authenticationResult.getAuthorities(),   authenticationResult.getClientRegistration().getRegistrationId());  oauth2Authentication.setDetails(authenticationDetails);          // 构造OAuth2AuthorizedClient,将所有经过授权的客户端信息保存起来,默认是通过        // AuthenticatedPrincipalOAuth2AuthorizedClientRepository来保存的,        // 然后就能通过其来获取之前所有已授权的client?暂时不能确定其合适的用途  OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(   authenticationResult.getClientRegistration(),   oauth2Authentication.getName(),   authenticationResult.getAccessToken(),   authenticationResult.getRefreshToken());  this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);  return oauth2Authentication; }}

OAuth2LoginAuthenticationFilter的作用很简单,就是响应授权服务器的回调地址,核心之处在于OAuth2LoginAuthenticationProvider对OAuth2LoginAuthenticationToken的认证。

OAuth2LoginAuthenticationProvider

public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {         ...省略部分代码        @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException {  OAuth2LoginAuthenticationToken authorizationCodeAuthentication =   (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 (authorizationCodeAuthentication.getAuthorizationExchange()   .getAuthorizationRequest().getScopes().contains("openid")) {   // This is an OpenID Connect Authentication Request so return null   // and let OidcAuthorizationCodeAuthenticationProvider handle it instead   return null;  }  OAuth2AccessTokenResponse accessTokenResponse;  try {   OAuth2AuthorizationExchangeValidator.validate(     authorizationCodeAuthentication.getAuthorizationExchange());   // 远程调用授权服务器的access_token端点获取令牌   accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(     new OAuth2AuthorizationCodeGrantRequest(       authorizationCodeAuthentication.getClientRegistration(),       authorizationCodeAuthentication.getAuthorizationExchange()));  } catch (OAuth2AuthorizationException ex) {   OAuth2Error oauth2Error = ex.getError();   throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());  }             OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();  Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();           // 通过userService使用上一步拿到的accessToken远程调用授权服务器的用户信息  OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(    authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));  Collection<? extends GrantedAuthority> mappedAuthorities =   this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());           // 构造认证成功之后的认证信息  OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(   authorizationCodeAuthentication.getClientRegistration(),   authorizationCodeAuthentication.getAuthorizationExchange(),   oauth2User,   mappedAuthorities,   accessToken,   accessTokenResponse.getRefreshToken());  authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());  return authenticationResult; }    ...省略部分代码}

OAuth2LoginAuthenticationProvider的执行逻辑很简单,首先通过code获取access_token,然后通过access_token获取用户信息,这和标准的oauth2授权码模式一致。

自动配置

在spring指南的例子中,我们发现只是配置了一个简单oauth2Login()方法,一个完整的oauth2授权流程就构建好了,其实这完全归功于spring-boot的autoconfigure,我们找到spring-boot-autoconfigure.jar包中的security.oauth2.client.servlet包,可以发现spring-boot给我们提供了几个自动配置类

OAuth2ClientAutoConfigurationOAuth2ClientRegistrationRepositoryConfigurationOAuth2WebSecurityConfiguration

其中OAuth2ClientAutoConfiguration导入了OAuth2ClientRegistrationRepositoryConfiguration和OAuth2WebSecurityConfiguration的配置

OAuth2ClientRegistrationRepositoryConfiguration

@Configuration(proxyBeanMethods = false)@EnableConfigurationProperties(OAuth2ClientProperties.class)@Conditional(ClientsConfiguredCondition.class)class OAuth2ClientRegistrationRepositoryConfiguration {    @Bean    @ConditionalOnMissingBean(ClientRegistrationRepository.class)    InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {        List<ClientRegistration> registrations = new ArrayList<>(            OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());        return new InMemoryClientRegistrationRepository(registrations);    }}

OAuth2ClientRegistrationRepositoryConfiguration将我们在配置文件中注册的client构造成ClientRegistration然后保存到内存之中。这里有一个隐藏的CommonOAuth2Provider类,这是一个枚举类,里面事先定义好了几种常用的三方登录授权服务器的各种参数例如GOOGLE、GITHUB、FACEBOO、OKTA

CommonOAuth2Provider

public enum CommonOAuth2Provider { GOOGLE {  @Override  public Builder getBuilder(String registrationId) {   ClientRegistration.Builder builder = getBuilder(registrationId,     ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);   builder.scope("openid", "profile", "email");   builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");   builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");   builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");   builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");   builder.userNameAttributeName(IdTokenClaimNames.SUB);   builder.clientName("Google");   return builder;  } }, GITHUB {  @Override  public Builder getBuilder(String registrationId) {   ClientRegistration.Builder builder = getBuilder(registrationId,     ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);   builder.scope("read:user");   builder.authorizationUri("https://github.com/login/oauth/authorize");   builder.tokenUri("https://github.com/login/oauth/access_token");   builder.userInfoUri("https://api.github.com/user");   builder.userNameAttributeName("id");   builder.clientName("GitHub");   return builder;  } }, FACEBOOK {  @Override  public Builder getBuilder(String registrationId) {   ClientRegistration.Builder builder = getBuilder(registrationId,     ClientAuthenticationMethod.POST, DEFAULT_REDIRECT_URL);   builder.scope("public_profile", "email");   builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth");   builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token");   builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email");   builder.userNameAttributeName("id");   builder.clientName("Facebook");   return builder;  } }, OKTA {  @Override  public Builder getBuilder(String registrationId) {   ClientRegistration.Builder builder = getBuilder(registrationId,     ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);   builder.scope("openid", "profile", "email");   builder.userNameAttributeName(IdTokenClaimNames.SUB);   builder.clientName("Okta");   return builder;  } }; private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}"; protected final ClientRegistration.Builder getBuilder(String registrationId,               ClientAuthenticationMethod method, String redirectUri) {  ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(registrationId);  builder.clientAuthenticationMethod(method);  builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);  builder.redirectUriTemplate(redirectUri);  return builder; }     public abstract ClientRegistration.Builder getBuilder(String registrationId);}

这就是为什么我们没有配置github授权端点确能够跳转授权页面的原因。

OAuth2WebSecurityConfiguration

OAuth2WebSecurityConfiguration配置一些web相关的类,像如何去保存和获取已经授权过的客户端,以及默认的oauth2客户端相关的配置

@Configuration(proxyBeanMethods = false)@ConditionalOnBean(ClientRegistrationRepository.class)class OAuth2WebSecurityConfiguration { @Bean @ConditionalOnMissingBean OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {  return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); } @Bean @ConditionalOnMissingBean OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {  return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); }     // 默认的oauth2客户端相关的配置 @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();  } }}

今日干货

Spring Boot 接入 GitHub 第三方登录,只要两行配置!

刚刚发表

查看: 13500 回复:135

公众号后台回复 SpringBoot,免费获取 274 页SpringBoot修炼手册。

本文分享自微信公众号 - 江南一点雨(a_javaboy)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Spring Security 中如何细化权限粒度?
松哥原创的SpringBoot视频教程已经杀青,感兴趣的小伙伴戳这里SpringBootVue微人事视频教程(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzI1NDY0MTkzNQ%3D%3D%26mi
Stella981 Stella981
3年前
Spring 源码第四弹!深入理解 BeanDefinition
松哥原创的SpringBoot视频教程已经杀青,感兴趣的小伙伴戳这里SpringBootVue微人事视频教程(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzI1NDY0MTkzNQ%3D%3D%26mi
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Google地球出现“无法连接到登录服务器(错误代码:c00a0194)”解决方法
Google地球出现“无法连接到登录服务器(错误代码:c00a0194)”解决方法参考文章:(1)Google地球出现“无法连接到登录服务器(错误代码:c00a0194)”解决方法(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.codeprj.com%2Fblo
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这