1、概述
AdminEAP爲本人基於AdminLTE改造的後臺管理框架,包含了基本的系統管理功能和各種交互demo,項目已經開源到Github,並部署到阿里雲。
Github : https://github.com/bill1012/AdminEAP
AdminEAP DEMO: http://www.admineap.com
本文講解在AdminEAP框架下如何集成github等第三登錄,第三方登錄是應用開發中的常用功能,通過第三方登錄,我們可以更加容易的吸引用戶來到我們的應用中。現在,很多網站都提供了第三方登錄的功能,在他們的官網中,都提供瞭如何接入第三方登錄的文檔。但是,不同的網站文檔差別極大,各種第三方文檔也是千奇百怪,同時,很多網站提供的SDK用法也是各不相同。對於不瞭解第三方登錄的新手來說,實現一個支持多網站第三方登錄的功能可以說是極其痛苦。
在集成過程中使用了oauth2.0的協議,關於oauth2.0協議的講解,大家可以參考這篇文章OAuth2.0認證和授權機制講解
爲了方便大家理解,貼上兩張交互過程圖:
oauth2.0交互過程圖1
oauth2.0交互過程圖2(以qq爲例)
本文集成github第三方登錄採用了Scribe,Spring配置註解,並抽象成統一的接口,非常方便其他社會化登錄入口的接入,比如微博、微信、qq等。
本教程的源碼已經在本人Github上AdminEAP項目開源,實現結果。
2、 實現思路
1、使用Scribe提供的接口,Scribe是一個用 Java 開發的 OAuth 開源庫,支持 OAuth 1.0a / OAuth 2.0 標準,使用Scribe可節省很多工作量,而且方便擴展。
2、用戶進行認證後返回到註冊的回調地址http://**/oauth/{type}/callback,其中type爲github,(這樣方法可以通用)
3、通過返回的oAuthId與用戶建立關聯,如果本地系統關聯表(oAuthUser)中存在這條記錄,則直接跳轉到主頁,否者跳轉到註冊界面,引導用戶註冊,並關聯第三方賬號和註冊用戶。
(以上爲實現實錄的核心部分,還需在Github上申請key等,會在下面具體實現裏面提到)
3、核心代碼具體實現
1、在github上填寫應用的信息,這樣在認證的時候,服務器會和客戶端拿着id 和secret 做比對。要注意Authorization callback URL的填寫,目前在本地調試的時候,寫上本地地址。
2、pom.xml引用Scribe的依賴
<dependency>
<groupId>org.scribe</groupId>
<artifactId>scribe</artifactId>
<version>1.3.7</version>
</dependency>
3、核心代碼
下面講述這些代碼的關係:
- OAuthConfig: spirng 的配置類,通過註解的方式調用GithubApi的createService方法,從而創建GithubOAuthService實例
- GithubApi: Scribe 沒有現成的GithubApi,所以要自己寫,通過這個類創建GithubOAuthService實例
- CustomOAuthService:通用接口,繼承OAuthService接口,未來所有的其他第三方登錄都可以簡稱這個接口
- oAuthServices: 這個在圖中漏掉了,這個是實現所有CustomOAuthService接口的類的接口,這樣可以把所有的第三方登錄的接口放在一起,前臺通過統一的方式調用各種第三方登錄。
- OAuthUser :用戶第三方賬號和本地用戶的關聯表
- User: 本地用戶表
- OAuthTypes: 各種第三方認證的靜態變量名稱類
下面開始放代碼:
4、OAuthConfig.java
通過註解方式配置,並注入相關屬性 @Value屬性來自於settting.properties文件
package com.cnpc.framework.conf;
import com.cnpc.framework.oauth.common.CustomOAuthService;
import com.cnpc.framework.oauth.common.OAuthTypes;
import com.cnpc.framework.oauth.github.GithubApi;
import com.cnpc.framework.utils.PropertiesUtil;
import org.scribe.builder.ServiceBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Created by billJiang on 2017/1/15.
* e-mail:[email protected] qq:475572229
*/
@Configuration
public class OAuthConfig {
@Value("${oauth.callback.url}")
String callback_url;
/**
* github配置
*/
@Value("${oauth.github.key}")
String github_key;
@Value("${oauth.github.secret}")
String github_secret;
//該state爲一串隨機碼,大家可隨便給一個uuid
@Value("${oauth.github.state}")
String github_state;
@Bean
public GithubApi githubApi(){
return new GithubApi(github_state);
}
@Bean
public CustomOAuthService getGithubOAuthService(){
return (CustomOAuthService)new ServiceBuilder()
.provider(githubApi())
.apiKey(github_key)
.apiSecret(github_secret)
.callback(String.format(callback_url, OAuthTypes.GITHUB))
.build();
}
}
以上配置要被spring的配置文件掃描到,還需要在spring.xml配置以下內容
<!--掃描到java config配置-->
<context:annotation-config/>
<context:component-scan base-package="com.cnpc.framework.conf" />
<!-- 引入屬性文件 -->
<context:property-placeholder location="classpath:jdbc.properties,classpath:setting.properties" />
5、GithubApi.java
package com.cnpc.framework.oauth.github;
import org.scribe.builder.api.DefaultApi20;
import org.scribe.model.OAuthConfig;
import org.scribe.oauth.OAuthService;
import org.scribe.utils.OAuthEncoder;
/**
* Created by billJiang on 2017/1/15.
* e-mail:[email protected] qq:475572229
* Github Api for oauth2.0
*/
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);
}
@Override
public OAuthService createService(OAuthConfig config){
return new GithubOAuthService(this,config);
}
}
6、GithubOAuthService.java
package com.cnpc.framework.oauth.github;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONPath;
import com.cnpc.framework.oauth.common.CustomOAuthService;
import com.cnpc.framework.oauth.common.OAuthTypes;
import com.cnpc.framework.oauth.entity.OAuthUser;
import com.cnpc.framework.utils.PropertiesUtil;
import org.scribe.builder.api.DefaultApi20;
import org.scribe.model.*;
import org.scribe.oauth.OAuth20ServiceImpl;
/**
* Created by billJiang on 2017/1/15.
* e-mail:[email protected] qq:475572229
*/
public class GithubOAuthService extends OAuth20ServiceImpl implements CustomOAuthService {
private static final String PROTECTED_RESOURCE_URL = "https://api.github.com/user";
private final DefaultApi20 api;
private final OAuthConfig config;
private final String authorizationUrl;
public GithubOAuthService(DefaultApi20 api, OAuthConfig config){
super(api,config);
this.api=api;
this.config=config;
this.authorizationUrl=getAuthorizationUrl(null);
}
@Override
public String getoAuthType() {
return OAuthTypes.GITHUB;
}
@Override
public String getBtnClass(){
return PropertiesUtil.getValue("oauth.github.btnclass");
}
@Override
public String getAuthorizationUrl() {
return authorizationUrl;
}
@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.setUserName(JSONPath.eval(result, "$.login").toString());
return oAuthUser;
}
}
7、CustomOAuthService.java 接口,通用接口,方便其他第三方登錄擴展
package com.cnpc.framework.oauth.common;
import com.cnpc.framework.oauth.entity.OAuthUser;
import org.scribe.model.Token;
import org.scribe.oauth.OAuthService;
/**
* Created by billJiang on 2017/1/15.
* e-mail:[email protected] qq:475572229
*/
public interface CustomOAuthService extends OAuthService {
String getoAuthType();
String getAuthorizationUrl();
OAuthUser getOAuthUser(Token accessToken);
String getBtnClass();
}
8、OAuthServices.java 所有繼承CustomOAuthService的類的集合類,通過@Autowired注入所有繼承CustomOAuthService接口的實例。
package com.cnpc.framework.oauth.service;
import com.cnpc.framework.oauth.common.CustomOAuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* Created by billJiang on 2017/1/15.
* e-mail:[email protected] qq:475572229
*/
@Service
public class OAuthServices {
@Autowired
private List<CustomOAuthService> customOAuthServices;
/*public OAuthServices(){
OAuthConfig config=new OAuthConfig();
customOAuthServices=new ArrayList<CustomOAuthService>();
customOAuthServices.add(config.getGithubOAuthService());
}*/
public CustomOAuthService getOAuthService(String type) {
CustomOAuthService oAuthService = null;
for (CustomOAuthService customOAuthService : customOAuthServices) {
if (customOAuthService.getoAuthType().equals(type)) {
oAuthService = customOAuthService;
break;
}
}
return oAuthService;
}
public List<CustomOAuthService> getAllOAuthServices() {
return customOAuthServices;
}
}
9、OAuthTypes.java
package com.cnpc.framework.oauth.common;
/**
* Created by billJiang on 2017/1/15.
* e-mail:[email protected] qq:475572229
*/
public class OAuthTypes {
public static final String GITHUB="github";
public static final String WEIXIN="weixin";
public static final String QQ="qq";
}
10、OAuthUser.java 第三方應用ID與本地用戶關聯
package com.cnpc.framework.oauth.entity;
import com.cnpc.framework.base.entity.BaseEntity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.persistence.*;
@Entity
@Table(name = "tbl_user_oauth")
@JsonIgnoreProperties(value = { "hibernateLazyInitializer", "handler", "fieldHandler" })
public class OAuthUser extends BaseEntity {
/**
*
*/
private static final long serialVersionUID = 2836972841233228L;
@Column(name="user_id")
private String userId;
@Column(name="user_name")
private String userName;
@Column(name="oauth_type")
private String oAuthType;
@Column(name="oauth_id")
private String oAuthId;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getoAuthType() {
return oAuthType;
}
public void setoAuthType(String oAuthType) {
this.oAuthType = oAuthType;
}
public String getoAuthId() {
return oAuthId;
}
public void setoAuthId(String oAuthId) {
this.oAuthId = oAuthId;
}
}
4、controller層處理與前端頁面
1、LoginController的oauth2.0認證的核心代碼,記得在spring-shiro配置文件上加上oauth/**=anon(允許匿名訪問)
//----------------oauth 認證------------------
@RequestMapping(value = "/oauth/{type}/callback", method = RequestMethod.GET)
public String callback(@RequestParam(value = "code", required = true) String code, @PathVariable(value = "type") String type,
HttpServletRequest request, Model model) {
model.addAttribute("oAuthServices", oAuthServices.getAllOAuthServices());
try {
CustomOAuthService oAuthService = oAuthServices.getOAuthService(type);
Token accessToken = oAuthService.getAccessToken(null, new Verifier(code));
//第三方授權返回的用戶信息
OAuthUser oAuthInfo = oAuthService.getOAuthUser(accessToken);
//查詢本地數據庫中是否通過該方式登陸過
OAuthUser oAuthUser = oAuthUserService.findByOAuthTypeAndOAuthId(oAuthInfo.getoAuthType(), oAuthInfo.getoAuthId());
//未建立關聯,轉入用戶註冊界面
if (oAuthUser == null) {
model.addAttribute("oAuthInfo", oAuthInfo);
return REGISTER_PAGE;
}
//如果已經過關聯,直接登錄
User user = userService.get(User.class, oAuthUser.getUserId());
return loginByAuth(user);
}catch (Exception e){
String msg = "連接"+type+"服務器異常. 錯誤信息爲:"+e.getMessage();
model.addAttribute("message", new ResultCode("1", msg));
LOGGER.error(msg);
return LOGIN_PAGE;
}
}
@RequestMapping(value = "/oauth/register", method = RequestMethod.POST)
public String register_oauth(User user, @RequestParam(value = "oAuthType", required = false, defaultValue = "") String oAuthType,
@RequestParam(value = "oAuthId", required = true, defaultValue = "") String oAuthId,
HttpServletRequest request,Model model) {
model.addAttribute("oAuthServices", oAuthServices.getAllOAuthServices());
OAuthUser oAuthInfo = new OAuthUser();
oAuthInfo.setoAuthId(oAuthId);
oAuthInfo.setoAuthType(oAuthType);
//保存用戶
user.setPassword(EncryptUtil.getPassword(user.getPassword(),user.getLoginName()));
String userId=userService.save(user).toString();
//建立第三方賬號關聯
OAuthUser oAuthUser=oAuthUserService.findByOAuthTypeAndOAuthId(oAuthType,oAuthId);
if(oAuthUser==null&&!oAuthType.equals("-1")){
oAuthInfo.setUserId(userId);
oAuthUserService.save(oAuthInfo);
}
//關聯成功後登陸
return loginByAuth(user);
}
public String loginByAuth(User user){
UsernamePasswordToken token = new UsernamePasswordToken(user.getLoginName(), user.getPassword());
token.setRememberMe(true);
Subject subject = SecurityUtils.getSubject();
subject.login(token);
//通過認證
if (subject.isAuthenticated()) {
return MAIN_PAGE;
} else {
return LOGIN_PAGE;
}
}
/**
* 校驗當前登錄名/郵箱的唯一性
* @param loginName 登錄名
* @param userId 用戶ID(用戶已經存在,改回原來的名字還是唯一的)
* @return
*/
@RequestMapping(value = "/oauth/checkUnique", method = RequestMethod.POST)
@ResponseBody
public Map checkExist(String loginName, String userId) {
Map<String, Boolean> map = new HashMap<String, Boolean>();
User user = userService.getUserByLoginName(loginName);
//用戶不存在,校驗有效
if (user == null) {
map.put("valid", true);
} else {
if(!StrUtil.isEmpty(userId)&&userId.equals(user.getLoginName())){
map.put("valid",true);
}else {
map.put("valid", false);
}
}
return map;
}
2、 login.html的核心代碼
<div class="social-auth-links" style="margin-bottom: 0px;">
<div class="row">
<div class="col-xs-5">
<div class="text-left" style="margin-top: 5px;">快速登錄</div>
</div>
<div class="col-xs-7">
<div class="text-right">
<!--<a class="btn btn-social-icon btn-primary"><i class="fa fa-qq"></i></a>
<a class="btn btn-social-icon btn-success"><i class="fa fa-wechat"></i></a>
<a class="btn btn-social-icon btn-warning"><i class="fa fa-weibo"></i></a>
<a class="btn btn-social-icon btn-info"><i class="fa fa-github"></i></a>-->
<#list oAuthServices as oauth>
<a class="btn btn-social-icon ${oauth.btnClass}" href="${oauth.authorizationUrl}"><i class="fa fa-${oauth.oAuthType}"></i></a>
</#list>
</div>
</div>
</div>
3、register.html的核心代碼(略,後面會寫一篇文章專門講Bootstrap-validator用於註冊頁面的校驗)詳細代碼可查看我的github
以上的代碼實現了在AdminEAP框架下,以github爲例實現了第三方登錄認證,這個方式是通用的,越來越多的應用接入社會化登錄,通用的方式可以節省很多工作量,希望這篇文章能幫到你。