【OAuth2.0 Client 总结】对接github第三方登录以及其他第三方登录总结
之前搞 oauth 登录一直没有搞好,客户端、授权服务端、资源端一起搞对于我刚接触的小菜鸡来说,难度有点大。
然后就先搞了个 Client 端对接 Github 登录。 网上关于 Github 登录的资料有很多,而且框架对 Github 集成的也很好,配置起来并不麻烦。
对接Github登录
首先在 Github 上申请注册一个 oauth application,填写 callback url得到 client ID和 secret
默认的callback url 格式:{baseUrl}/login/oauth2/code/{registrationId}
在这里对接 GIthub 的默认重定向的地址就是http://xxxx/api/login/oauth2/code/github
如果对接其他的第三方就将 registrationId
改成 和我们配置文件中的一致即可
1、作为 Client 端只需要引入依赖
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
2、然后在applicatiom.yml
中进行相关配置
spring:security:oauth2:client:registration:github:clientId: 56ca77ae71xxxxxxxclientSecret: e1aa08298c5d0f5f9c35414355666b81xxxxxxxscope:- user:email- read:user
3、Create OAuth2User and OAuth2UserService Classes
主要用来定义 授权用户 的数据结构 和 加载 授权用户的相关数据
3.1、创建一个实体类去实现 OAuth2User
重写里面的方法
例如:
import java.util.Collection;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;public class GitHubCustomOauth2User implements OAuth2User {private OAuth2User user;public GitHubCustomOauth2User(OAuth2User user) {this.user = user;}@Overridepublic Map<String, Object> getAttributes() {return user.getAttributes();}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return user.getAuthorities();}@Overridepublic String getName() {return user.getAttribute("name");}public String getLogin() {return user.getAttribute("login");}public String getEmail() {return user.getAttribute("email");}
}
⚠️注意:对于对接不同的授权端是getName()
方法返回的值的名称发生变化
3.2 创建一个service 去继承 DefaultOAuth2UserService
里面实现加载授权用户数据的功能,根据不同授权端转变为不同的 OAuth2User
例如:
package com.openbayes.application.oauth;import com.openbayes.domain.oauth.GitHubCustomOauth2User;
import com.openbayes.domain.oauth.SiomCustomOauth2User;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;@Service
@Slf4j
public class CustomOauth2UserService extends DefaultOAuth2UserService {@Overridepublic OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {OAuth2AccessToken accessToken = userRequest.getAccessToken();String tokenValue = accessToken.getTokenValue();log.info("accessToken value: {}", tokenValue);ClientRegistration clientRegistration = userRequest.getClientRegistration();ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails();log.info("clientRegistration 为:{}", clientRegistration);log.info("clientRegistration providerDetails 为:{}", providerDetails);String attributeName =userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();log.info("attributeName 为:{}", attributeName);Map<String, Object> additionalParameters = userRequest.getAdditionalParameters();log.info("additionalParameters 为:{}", additionalParameters);OAuth2User oAuth2User = super.loadUser(userRequest);log.info("User information response: {}", oAuth2User.getAttributes());if (clientRegistration.getRegistrationId().equals("github")) {GitHubCustomOauth2User gitHubCustomOauth2User = new GitHubCustomOauth2User(oAuth2User);log.info("gitHubCustomOauth2User 的 Attributes 为:{}", gitHubCustomOauth2User.getAttributes());return gitHubCustomOauth2User;} else {xxxCustomOauth2User xxCustomOauth2User = new xxCustomOauth2User(oAuth2User);log.info("xxxCustomOauth2User 的 Attributes 为:{}", xxxxCustomOauth2User.getAttributes());return siomCustomOauth2User;}}
}
4、Configure Spring Security for OAuth2 Login
配置 OAuth Login 的配置
例如
@Autowired private OAuth2AuthorizedClientService authorizedClientService;@Autowired private OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository;@Autowired private CustomOauth2UserService customOauth2UserService;@Autowired private Oauth2LoginSuccessHandler oauth2LoginSuccessHandler;@Autowired private Oauth2LoginFailedHandler oauth2LoginFailedHandler;@Autowired private Oauth2LoginOutSuccessHandler oauth2LoginOutSuccessHandler;http.oauth2Login().loginPage("/login").authorizedClientService(authorizedClientService).authorizedClientRepository(oAuth2AuthorizedClientRepository).userInfoEndpoint().userService(customOauth2UserService).and().successHandler(oauth2LoginSuccessHandler).failureHandler(oauth2LoginFailedHandler);
这里面的配置包含了 OAuth 集群下的一些配置(下面讲到),加载用户信息的服务为刚才我们定义的 Service ,以及用户登录成功 和登录失败的 handler
5、extends SimpleUrlAuthenticationSuccessHandler
这个里面主要用来处理授权成功之后的逻辑,是注册用户还是直接登录等业务逻辑
一张图来解释
例子
@Component
@Slf4j
public class Oauth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {@Autowired private CustomOauth2UserService customOauth2UserService;@Autowired private UserRepository userRepository;@Autowired private UserApplicationService userApplicationService;@Autowired private TokenService tokenService;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException, ServletException {if (authentication instanceof OAuth2AuthenticationToken) {OAuth2AuthenticationToken oauth2Token = (OAuth2AuthenticationToken) authentication;String authorizedProvider = oauth2Token.getAuthorizedClientRegistrationId();log.info("authorizedProvider为{}", authorizedProvider);String redirectUrl = null;if (authorizedProvider.equals("github")) {GitHubCustomOauth2User oauth2User = (GitHubCustomOauth2User) authentication.getPrincipal();是注册呢?还是直接返回 token呢? 根据相关业务逻辑来判断} else if (authorizedProvider.equals("siom")) {SiomCustomOauth2User siomCustomOauth2User =(SiomCustomOauth2User) authentication.getPrincipal();和上面同理}}
}
6、extends SimpleUrlAuthenticationFailureHandler
这里主要为了登录失败的情况,主要做一些打印日志,以及给前端返回错误信息。这样前端可以根据返回的结果判断授权登录是成功还是失败,做不一样的操作以及跳转到不同的页面
到这里, 在单节点上使用 Github 登录就已经结束了
OAuth Logout
其实还可以引入登出的逻辑,登出的 handler 里面就是清理 session 的工作
- 首先要进行 logout 的配置
例如
http.logout().logoutUrl("/auth/logout").logoutSuccessHandler(oauth2LoginOutSuccessHandler).invalidateHttpSession(true).clearAuthentication(true).deleteCookies("JSESSIONID");
上面的配置就是,等你调用接口 /auth/logout
登出的时候,会对seeion 以及 cookie进行处理,oauth2LoginOutSuccessHandler 也会做相应的操作(打印日志,返回信息给前端)
例如
@Component
@Slf4j
public class Oauth2LoginOutSuccessHandler extends SimpleUrlLogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException, ServletException {HttpSession session = request.getSession(false);if (session != null) {session.invalidate();}log.info("User logged out at {}", new Date());response.setStatus(HttpStatus.UNAUTHORIZED.value());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.getWriter().write("{\\"message\\": \\"Logout successful\\"}");response.getWriter().flush();}
}
对接其他作为Client登录
如果我们还想引入其他第三方的登录,和上面的流程一下的。 我们这次增加一个和框架没有集成的第三方(像github和谷歌等因为集成的很好,我们的配置就会很好,流程也会比较顺利)
1、首先就是client 的配置
security:oauth2:client:registration:github:clientId: 56ca77ae71cxxxxxxclientSecret: e1aa08298c5d0f5f9c35414355666b8xxxxxxxxxscope:- user:email- read:useraaaa:client-id: 6b91bc5axxxxxxclient-secret: 97eabe644084f6442a58bxxxxxxauthorization-grant-type: authorization_coderedirect-uri: "http://xxxxxxx/api/login/oauth2/code/aaaa"client-name: SIOMprovider:aaaa:authorization-uri: xxxxxxxtoken-uri: xxxxxxuser-info-uri: xxxxxxxuser-name-attribute: id
这里的配置就是看官方文档进行配置。学习网址:OAuth2
2、创建新的实体类去实现 OAuth2User
例如
import java.util.Collection;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;public class xxxxCustomOauth2User implements OAuth2User {private OAuth2User user;public xxxxCustomOauth2User(OAuth2User user) {this.user = user;}@Overridepublic Map<String, Object> getAttributes() {return user.getAttributes();}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return user.getAuthorities();}@Overridepublic String getName() {return user.getAttribute("id");}public String getAccountNo() {return (String)((Map<String, Object>) user.getAttributes().get("attributes")).get("account_no");}public String getUserId() {return (String) ((Map<String, Object>) user.getAttributes().get("attributes")).get("user_uid");}public String getEmail() {return (String) ((Map<String, Object>) user.getAttributes().get("attributes")).get("email");}public String getMobilePhone() {return (String) ((Map<String, Object>) user.getAttributes().get("attributes")).get("mobile");}
}
⚠️注意:这里重写的 getName()
就是根据返回的授权用户的 id
3、在 extends DefaultOAuth2UserService 的Service 中增加判断,授权用户最终转变为那个授权实体类进行返回
上面的例子上已经有所展示
4、在 loginSuccessHandler 里面增加判断是那个 AuthorizedClientRegistrationId ,进行不同的登录或者创建用户操作
在这里单节点下两个不同的第三方的 OAuth Client 的整体流程就已经结束了,但是在集群环境下就会出现新的问题
1、 OAuth Client 集群环境下的会话丢失问题
解决方式
关于 JDBC Session的配置
1、引入 spring-session-jdbc
进行会话存储
application.yml
spring:session:store-type: jdbcjdbc:initialize-schema: always
关于 OAuth Client 的配置
@Configuration
public class OAuth2AuthorizedClientConfig {@Beanpublic OAuth2AuthorizedClientRepository authorizedClientRepository() {return new HttpSessionOAuth2AuthorizedClientRepository();}@Beanpublic OAuth2AuthorizedClientService authorizedClientService(JdbcOperations jdbcOperations, ClientRegistrationRepository clientRegistrationRepository) {return new JdbcOAuth2AuthorizedClientService(jdbcOperations, clientRegistrationRepository);}
}
WebSecurity
http.oauth2Login().loginPage("/login").authorizedClientService(authorizedClientService).authorizedClientRepository(oAuth2AuthorizedClientRepository).userInfoEndpoint().userService(customOauth2UserService).and().successHandler(oauth2LoginSuccessHandler).failureHandler(oauth2LoginFailedHandler);
OAuth 2.0 Client supports application clustering
上面链接的摘录
2、authorization_request_not_fund
情况:在第一次成功登录后,客户端和授权端都登出,客户端再发起第三方登录的请求会出现这个问题。(不一定,需要看授权端的登出操作是否彻底请求用户的session及cookie)
无关集群环境,只是纯属工作中的坑。
解决:授权服务器在登出的时候要彻底清除用户的seesion 和 cookie信息