基于Spring的Github第三方登录--通用化的第三方登陆实现
来源:互联网 发布:手机照片制作视频软件 编辑:程序博客网 时间:2024/06/10 09:34
在《OAuth2.0认证和授权机制讲解》中我们知道了现在主流的第三方登陆是怎样一个流程,那么现在,就让我们自己来实现一个通用化的第三方登陆实现吧。
准备工作
在做第三方登陆之前,首先我们当然需要有一个授权服务器承认的第三方应用身份,因此,我们首先前往授权服务器进行申请,由于国内的所有应用都需要进行审核,比较麻烦,这里我们以Github为例,首先申请一个第三方应用的资格。
首先登陆Github账号,进入【Settings】->选择【applications】->选择【Developer applications】,这里我们可以看到当前账户所拥有的第三方应用
点击右上角的【Register new application】,按要求填写信息就可以申请一个第三方应用的身份。由于我们是本地调试,我们按照本地的测试地址填写相关信息即可:
注意右上角的Client Id和Client Secret,这两个信息是用来标识第三方应用身份的相关信息,特别注意Client Secret,Client Secret是用来和授权服务器交换验证凭证(Access Token)的,千万不能暴露出去。
这样,我们就拥有了第三方应用的身份,可以喝Github交互进行OAuth2的授权了。
功能分析
我们的第三方登陆功能实际上就是将OAuth2的授权流程跑通,最后将拿到的用户信息存储在数据库中,并将其与本地数据的用户信息对应起来,以便于下次登陆时直接拿到本地的用户信息。
总结一下OAuth的基本流程,实际上主要涉及到下列URL:
- 引导用户进行授权的授权地址(authorizationUrl)
- 用户授权后传递用户凭证(code)的redirectURL
- 使用用户凭证(code)交换验证凭证(Access Token)的地址
- 获取用户信息的地址(可能不止一个)
通过确认以上地址,我们可以搭建一个通用化的OAuth授权以及验证功能,不同的授权服务器只需要提供不同的地址,其他流程完全可以用相同的代码来解决,这就是我们今天需要实现的通用化第三方登陆功能的基础。
以Github为例,其相应的API地址分别为:
- https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&state={state}
- http://localhost:8080/oauth/github/callback?code={code}&state={state}
- https://github.com/login/oauth/access_token
- https://api.github.com/user?access_token={access_token}
现在,我们需要做下列事情:
- 在首页显示Github的授权链接,使用户能够访问authorizationUrl
- 编写一个Controller,处理
http://localhost:8080/oauth/github/callback?code={code}&state={state}
的请求,主要是拿到code - 用code访问
http://localhost:8080/oauth/github/callback?code={code}&state={state}access token
- 然后利用access token访问
https://api.github.com/user?access_token={access_token}
,拿到用户信息 - 根据用户信息判断该用户是否已经绑定,如果已经绑定,直接登录,如果未绑定则导入到绑定页面,引导用户进入绑定页面
- 在绑定页面添加新的用户
引入依赖
工欲善其事必先利其器,让我们先将我们需要用到的工具添加进来:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.scribe</groupId> <artifactId>scribe</artifactId> <version>1.3.7</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.2</version> </dependency></dependencies>
如果大家经常看天码营,应该对前面四个依赖特别熟悉了,前面四个依赖为我们搭建了一个内存数据库+JPA+thymeleaf+Spring的基本web框架。
Scribe是一个简单的Java OAuth库,通过Scribe,可以很方便的实现OAuth验证的功能。
由于通过资源API拿到的用户资源是json编码,因此我们需要fastjson库来处理json对象。
仅仅看这些依赖确实太过抽象,还是看看具体的实现吧。
搭建OAuth用户系统
首先让我们利用JPA搭建一个简单的用户系统,在此基础之上再来做第三方登陆的功能:
@Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String username; private String password; ......}
public interface UserRepository extends JpaRepository<User, Integer> { User findByUsername(String username);}
一个User模型,再加上一个Repository接口,我们的用户系统雏形就完成了,想了解更多JPA的同学,可以阅读使用JPA访问关系型数据库,这里就不再介绍了。
有了用户系统后,我们希望本地用户与其在Github或者其他网站的用户信息一一对应起来,因此,我们还需要一张OAuthUser表,里面存有该用户在其他网站的基本信息(在哪个网站,唯一标识是多少)以及该用户与本地用户的映射,其领域模型如下:
@Entitypublic class OAuthUser{ @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; @OneToOne private User user; private String oAuthType; private String oAuthId; ......}
通过User以及OAuthUser对象的建立,我们的数据模型就搭建完成了,接下来便是如何通过OAuth验证。
API
在功能分析一节中我们总结了进行Github Oauth验证所需要的几种URL,在Scribe中,将前面三个URL抽象到了接口API
中,当我们需要得到相应的URL时,只需要调用API的某个方法即可。Scribe已经为我们实现了很多API,但是很遗憾并没有Github的API,因此,我们需要手动实现GithubAPI。在抽象类DefaultApi20
中已经有一些通用的方法,让我们来继承它简化我们的工作:
public class GithubApi extends DefaultApi20 { private static final String AUTHORIZE_URL = "https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&state=%s"; private static final String SCOPED_AUTHORIZE_URL = AUTHORIZE_URL + "&scope=%s"; private static final String ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token?state=%s"; private final String githubState; public GithubApi(String state){ this.githubState = state; } @Override public String getAuthorizationUrl(OAuthConfig config) { if (config.hasScope()){ return String.format(SCOPED_AUTHORIZE_URL, config.getApiKey(), OAuthEncoder.encode(config.getCallback()), githubState, OAuthEncoder.encode(config.getScope())); } else{ return String.format(AUTHORIZE_URL, config.getApiKey(), OAuthEncoder.encode(config.getCallback()), githubState); } } @Override public String getAccessTokenEndpoint() { return String.format(ACCESS_TOKEN_URL, githubState); }}
OAuthService
在Scribe的OAuthService中,定义了OAuth的基本方法,包括获取授权URL、根据code得到access token等,包括OAuth验证中的大部分方法,我们可以很容易的使用OAuthService完成《OAuth验证的前四步》。但是最终通过Access token拿到用户资源还需要我们进行一定的拓展。
public abstract class OAuthServiceDeractor implements OAuthService { private final OAuthService oAuthService; private final String oAuthType; private final String authorizationUrl; public OAuthServiceDeractor(OAuthService oAuthService, String type) { super(); this.oAuthService = oAuthService; this.oAuthType = type; this.authorizationUrl = oAuthService.getAuthorizationUrl(null); } ...... public String getoAuthType() { return oAuthType; } public String getAuthorizationUrl(){ return authorizationUrl; } public abstract OAuthUser getOAuthUser(Token accessToken);}
最终,我们通过装饰者模式为OAuthService添加了三个方法:
- getoAuthType() 获取该Service的type
- getAuthorizationUrl() 获取authorizationUrl,方便前端展示
- getOAuthUser(Token accessToken) 根据access token拿到用户资源,并将其转换为对应的OAuthUser
这样,我们就能使用同一个方法来获取用户的相关资源。对于Github来说,需要实现getOAuthUser一个方法:
public class GithubOAuthService extends OAuthServiceDeractor { private static final String PROTECTED_RESOURCE_URL = "https://api.github.com/user"; public GithubOAuthService(OAuthService oAuthService) { super(oAuthService, OAuthTypes.GITHUB); } @Override public OAuthUser getOAuthUser(Token accessToken) { OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL); this.signRequest(accessToken, request); Response response = request.send(); OAuthUser oAuthUser = new OAuthUser(); oAuthUser.setoAuthType(getoAuthType()); Object result = JSON.parse(response.getBody()); oAuthUser.setoAuthId(JSONPath.eval(result, "$.id").toString()); oAuthUser.setUser(new User()); oAuthUser.getUser().setUsername(JSONPath.eval(result, "$.login").toString()); return oAuthUser; }}
如果我们有很多第三方登陆的接口,我们将通过OAuthServices
进行管理,我们通过Spring的依赖式注入获取所有OAuthService,并且通过OAuthType区别不同的OAuthService:
@Servicepublic class OAuthServices { @Autowired List<OAuthServiceDeractor> oAuthServiceDeractors; public OAuthServiceDeractor getOAuthService(String type){ Optional<OAuthServiceDeractor> oAuthService = oAuthServiceDeractors.stream().filter(o -> o.getoAuthType().equals(type)) .findFirst(); if(oAuthService.isPresent()){ return oAuthService.get(); } return null; } public List<OAuthServiceDeractor> getAllOAuthServices(){ return oAuthServiceDeractors; }}
通过OAuthService,我们可以很方便的调用OAuth服务。
Controller
最后,我们要写一个Controller供用户访问:
@Controllerpublic class AccountController { public static final Logger logger = LoggerFactory.getLogger(AccountController.class); @Autowired OAuthServices oAuthServices; @Autowired OauthUserRepository oauthUserRepository; @Autowired UserRepository userRepository; @RequestMapping(value = {"", "/login"}, method=RequestMethod.GET) public String showLogin(Model model){ model.addAttribute("oAuthServices", oAuthServices.getAllOAuthServices()); return "index"; } @RequestMapping(value = "/oauth/{type}/callback", method=RequestMethod.GET) public String claaback(@RequestParam(value = "code", required = true) String code, @PathVariable(value = "type") String type, HttpServletRequest request, Model model){ OAuthServiceDeractor oAuthService = oAuthServices.getOAuthService(type); Token accessToken = oAuthService.getAccessToken(null, new Verifier(code)); OAuthUser oAuthInfo = oAuthService.getOAuthUser(accessToken); OAuthUser oAuthUser = oauthUserRepository.findByOAuthTypeAndOAuthId(oAuthInfo.getoAuthType(), oAuthInfo.getoAuthId()); if(oAuthUser == null){ model.addAttribute("oAuthInfo", oAuthInfo); return "register"; } request.getSession().setAttribute("oauthUser", oAuthUser); return "redirect:/success"; } @RequestMapping(value = "/register", method=RequestMethod.POST) public String register(Model model, User user, @RequestParam(value = "oAuthType", required = false, defaultValue = "") String oAuthType, @RequestParam(value = "oAuthId", required = true, defaultValue = "") String oAuthId, HttpServletRequest request){ OAuthUser oAuthInfo = new OAuthUser(); oAuthInfo.setoAuthId(oAuthId); oAuthInfo.setoAuthType(oAuthType); if(userRepository.findByUsername(user.getUsername()) != null){ model.addAttribute("errorMessage", "用户名已存在"); model.addAttribute("oAuthInfo", oAuthInfo); return "register"; } user = userRepository.save(user); OAuthUser oAuthUser = oauthUserRepository.findByOAuthTypeAndOAuthId(oAuthType, oAuthId); if(oAuthUser == null){ oAuthInfo.setUser(user); oAuthUser = oauthUserRepository.save(oAuthInfo); } request.getSession().setAttribute("oauthUser", oAuthUser); return "redirect:/success"; } @RequestMapping(value = "/success", method=RequestMethod.GET) @ResponseBody public Object success(HttpServletRequest request){ return request.getSession().getAttribute("oauthUser"); } }
最终,我们为用户提供了4个URL:
/login
用户登录页面,为了简化处理,在该页面我们只展示第三方登录的地址
/oauth/{type}/callback
用户授权后的redirect_uri,授权服务器会通知浏览器跳转到该地址,并附带有用户凭证(code)
在该请求中,我们将得到cod->根据code拿到Access Token->根据Access Token拿到授权用户信息->根据授权用户获取本地用户->如果本地用户存在,直接登录,跳转到/success
页面->如果本地用户不存在,跳转到注册页面引导用户注册。
/register
注册页面,该页面将获取本地用户系统所需要的相关信息,以及OAuth的相关信息,存入数据库,最后将用户跳转到/success
页面。
/success
最终的登录成功页面,将展示用户以及OAuth的相关信息。
以上几个请求中,我们需要特别关注 /oauth/{type}/callback
以及/register
。其中/oauth/{type}/callback
负责OAuth相关的所有事宜,/register
负责将OAuth用户与本地用户映射起来,是第三方登录的关键。
怎么样,第三方登录是不是很简单,快自己动手实现一下吧。
- 基于Spring的Github第三方登录--通用化的第三方登陆实现
- 基于Spring的Github第三方登录--通用化的第三方登陆实现
- 基于Spring的微信第三方登录实现
- 基于Spring的新浪微博第三方登录实现
- 基于Spring的QQ第三方登录实现
- 基于Spring的新浪微博第三方登录实现
- 基于Spring的微信第三方登录实现
- 基于Spring的QQ第三方登录实现
- 基于Spring的QQ第三方登录实现
- 第三方登陆的实现
- Github第三方登录
- 基于oauth2.0的第三方登录实现
- 实现android的 google第三方登陆!
- 实现android的 google第三方登陆
- 实现第三方QQ的登陆
- 第三方登陆功能的实现
- 第三方登录(QQ登陆)实现
- django实现github第三方本地登录
- Android 小项目之--Mini音乐播放器【简单版】(附源码)
- 深入Java集合学习系列:HashMap的实现原理
- Spark MLlib 1.6 -- 降维
- Linux IPsec点到点配置
- Android 根据URI获取图片(从相册选择图片后返回)
- 基于Spring的Github第三方登录--通用化的第三方登陆实现
- 5.1 用户手势检测-GestureDetector使用详解
- Linux下C的内存对齐
- 安卓ROM包改为ZIP格式刷机包
- 主Activity双击退出程序
- android 在图片上涂鸦(添加水印同理)
- java socket编程
- ListView实现多种item布局的方法和注意事项
- 第五十二篇:Cascade Classifier Training详解