AdminEAP框架-SpringMVC+spring集成通用第三方登錄(以github爲例)

1、概述


AdminEAP

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交互過程圖1

oauth2.0交互過程圖2

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的填寫,目前在本地調試的時候,寫上本地地址。

github生成id 與secret

2、pom.xml引用Scribe的依賴

          <dependency>
                <groupId>org.scribe</groupId>
                <artifactId>scribe</artifactId>
                <version>1.3.7</version>
           </dependency>

3、核心代碼
oAuth.0認證的核心代碼

下面講述這些代碼的關係:

類關係圖

  • 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爲例實現了第三方登錄認證,這個方式是通用的,越來越多的應用接入社會化登錄,通用的方式可以節省很多工作量,希望這篇文章能幫到你。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章