天天看點

spring security整合QQ登入

      最近在了解第三方登入的内容,嘗試對接了一下QQ登入,此次記錄一下如何實作QQ登入的過程,在這個例子中是和spring secuirty整合的,不整合spring secuirty也是一樣的。

需求: 整合QQ登入

步驟:

     1、在QQ互聯上注冊開發者身份

     2、開發者身份稽核通過後,建立一個自己的應用,此時需要記錄如下三個資訊

          |- APP ID

          |- APP Key

          |- 回調位址: 這個位址是你自己網站的回調位址,即QQ登入成功後,QQ會重定向到這個位址上

                                假設回調位址是 http://www.abc.com /login/qq

          注意:1、 如果是在本機測試需要修改host檔案 增加一行  127.0.0.1 www.abc.com

                     2、記住上方的 /login/qq 在下方和spring security整合時需要用到

     3、需要檢視 QQ互聯-> 網站應用->網站開發流程  不然看不懂下方 QQLoginFilter 中的各個url是什麼意思

     4、QQ登入流程(oauth2的授權碼流程)

          |- 将使用者引導向QQ的認證伺服器

          |- 認證成功後QQ會重定向到 第二步 填寫的回調位址,并會帶上一個 code 參數,即授權碼

          |- 然後再通過 code 去換去通路令牌 即 access_token

          |- 在通過通路令牌去擷取使用者的資訊

     5、和spring security整合

          5.1 建立 QQLoginFilter 繼承spring security 的 AbstractAuthenticationProcessingFilter,攔截位址為 /login/qq ,需要和上方的回調位址的後半部分一緻

           判斷是否存在 code 參數

                 不存在:

                        說明使用者是剛點選頁面上的QQ登入按鈕進來的,此時需要将使用者引導向QQ的認證url,當使用者認證成功後,在回調位址後會帶上code參數,此時又會進入此過濾器中

                  存在:

                          說明使用者同意了授權,即使用者使用QQ登入了,那麼此時就可以進行下一步操作,擷取access_token,即令牌。然後再擷取使用者的openId的值,因為在擷取使用者的QQ基本資訊時需要用到使用者的openId.有了這些參數,然後就可以交給spring security的認證管理器去認證

          5.2 編寫認證提供者,在這個裡面去擷取QQ的使用者資訊,并傳回spring security 的認證對象

代碼實作:

一、編寫 QQUserInfo 實體類,這個類表示的是目前QQ使用者的基本資訊

package com.huan.springsecurity.social.qq;

import com.google.gson.annotations.SerializedName;

import lombok.Data;

/**
 * QQ的使用者資訊
 * 
 * @描述
 * @作者 huan
 * @時間 2018年3月3日 - 下午5:54:30
 */
@Data
public class QQUserInfo {
	private int ret;
	private String msg;
	private int isLost;
	private String nickname;
	private String gender;
	private String province;
	private String city;
	private String year;
	private String figureurl;
	private String figureurl_1;
	private String figureurl_2;
	private String figureurl_qq_1;
	private String figureurl_qq_2;
	@SerializedName(value = "is_yellow_vip")
	private String isYellowVip;
	private String vip;
	@SerializedName(value = "yellow_vip_level")
	private String yellowVipLevel;
	private String level;
	@SerializedName(value = "is_yellow_year_vip")
	private String isYellowYearVip;
}
           

二、編寫 QQLoginToken

package com.huan.springsecurity.social.qq;

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

/**
 * QQ登入的令牌,由spring security的 QQLoginProvider來處理這種令牌的資訊
 * 
 * @描述
 * @作者 huan
 * @時間 2018年3月3日 - 下午5:55:05
 */
public class QQLoginToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = 3363567749045247416L;
	private final Object principal;
	private Object credentials;
	private String accessToken;
	private String clientId;

	public QQLoginToken(Object principal, String accessToken, String clientId) {
		super(null);
		this.principal = principal;
		this.accessToken = accessToken;
		this.clientId = clientId;
		this.credentials = null;
		setAuthenticated(false);
	}

	public QQLoginToken(Object principal, String accessToken, String clientId, Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.accessToken = accessToken;
		this.clientId = clientId;
		this.credentials = null;
		super.setAuthenticated(true);
	}

	@Override
	public Object getCredentials() {
		return this.credentials;
	}

	@Override
	public Object getPrincipal() {
		return this.principal;
	}

	public String getAccessToken() {
		return accessToken;
	}

	public String getClientId() {
		return clientId;
	}

	@Override
	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
		if (isAuthenticated) {
			throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
		}

		super.setAuthenticated(false);
	}

	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
		credentials = null;
	}
}
           

三、建立 QQLoginFilter 用來攔截 /login/qq 完成oauth2授權碼流程的基本步驟

package com.huan.springsecurity.social.qq;

import java.io.IOException;
import java.util.Arrays;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.client.RestTemplate;

import lombok.extern.slf4j.Slf4j;

/**
 * 攔截 /login/qq請求,用于引導使用者到QQ的認證伺服器,擷取授權碼(code),擷取通路令牌(access_token)
 * 
 * @描述
 * @作者 huan
 * @時間 2018年3月3日 - 下午5:57:32
 */
@Slf4j
public class QQLoginFilter extends AbstractAuthenticationProcessingFilter {

	/**
	 * client_id 即在QQ互聯上建立應用的APP ID
	 */
	private static final String CLIENT_ID = "自己應用的APP ID的值";

	/**
	 * client_secret 即在QQ互聯上建立應用的APP Key
	 */
	private static final String CLIENT_SECRET = "自己應用的APP Key的值";

	/**
	 * redirect_uri 即在QQ互聯上建立應用的回調位址
	 */
	private static final String REDIRECT_URI = "自己應用的回調位址,假設是http://www.abc.com/login/qq";

	private static final String CODE = "code";

	/**
	 * 需要擷取那些接口的通路權限
	 */
	private static final String SCOPE = "get_user_info";

	/**
	 * 擷取授權碼url
	 */
	private static final String GET_AUTHORIZATION_CODE = "https://graph.qq.com/oauth2.0/authorize?response_type=%s&client_id=%s&redirect_uri=%s&state=%s&scope=%s";

	/**
	 * 擷取access_token
	 */
	public static final String GET_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s";

	/**
	 * 擷取openId
	 */
	public static final String GET_OPEN_ID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
	/**
	 * 用于發送請求
	 */
	private final RestTemplate restTemplate;

	protected QQLoginFilter(String defaultFilterProcessesUrl, RestTemplate restTemplate) {
		super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "GET"));
		this.restTemplate = restTemplate;
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
		String code = obtainCode(request);
		if (StringUtils.isBlank(code)) {
			log.info("沒有擷取到code的值,引導使用者到QQ授權頁面");
			response.sendRedirect(String.format(GET_AUTHORIZATION_CODE, CODE, CLIENT_ID, REDIRECT_URI, UUID.randomUUID().toString(), SCOPE));
			return null;
		}
		log.info("目前是從QQ授權登入傳回,擷取到code的值為:{}", code);
		log.info("根據code:{}的值取換取access_token的值", code);
		String result = restTemplate.getForObject(String.format(GET_ACCESS_TOKEN, CLIENT_ID, CLIENT_SECRET, code, REDIRECT_URI), String.class);
		log.info("根據code:{}擷取access_token的傳回值是:{}", code, result);
		if (result.contains("error")) {
			throw new InternalAuthenticationServiceException("QQ登入失敗");
		}
		String accessToken = Arrays.stream(result.split("&")).filter(s -> s.split("=")[0].equals("access_token")).map(s -> s.split("=")[1]).reduce("", String::concat);
		log.info("擷取到的accessToken:{}", accessToken);
		log.info("根據accessToken去擷取使用者的openId的值。");
		result = restTemplate.getForObject(String.format(GET_OPEN_ID, accessToken), String.class);
		log.info("根據accessToken:{}換取openId的結果為:{}", accessToken, result);
		String openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
		log.info("根據accessToken:{}換取的openId的值為:{}", accessToken, openId);

		QQLoginToken qqLoginToken = new QQLoginToken(openId, accessToken, CLIENT_ID);
		qqLoginToken.setDetails(authenticationDetailsSource.buildDetails(request));

		return super.getAuthenticationManager().authenticate(qqLoginToken);
	}

	protected String obtainCode(HttpServletRequest request) {
		return request.getParameter(CODE);
	}

}
           

四、編寫QQ認證提供者,用來擷取QQ使用者的資訊

package com.huan.springsecurity.social.qq;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.web.client.RestTemplate;

import com.google.gson.Gson;
import com.huan.springsecurity.security.User;

import lombok.extern.slf4j.Slf4j;

/**
 * 根據使用者的openId去擷取在系統中的實際使用者資訊
 * 
 * @描述
 * @作者 huan
 * @時間 2018年3月3日 - 下午6:04:21
 */
@Slf4j
public class QQLoginProvider implements AuthenticationProvider {

	/**
	 * 擷取QQ的使用者資訊
	 */
	private static final String GET_USER_INFO = "https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s";

	private RestTemplate restTemplate;

	public QQLoginProvider(RestTemplate restTemplate) {
		this.restTemplate = restTemplate;
	}

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		QQLoginToken qqLoginToken = (QQLoginToken) authentication;
		String openId = (String) qqLoginToken.getPrincipal();
		String accessToken = qqLoginToken.getAccessToken();
		String oauthConsumerKey = qqLoginToken.getClientId();
		String userInfo = restTemplate.getForObject(String.format(GET_USER_INFO, accessToken, oauthConsumerKey, openId), String.class);
		log.info("擷取到的qq登入資訊為:{}", userInfo);
		Gson gson = new Gson();
		QQUserInfo qqUserInfo = gson.fromJson(userInfo, QQUserInfo.class);
		if (qqUserInfo.getRet() < 0) {
			throw new InternalAuthenticationServiceException(qqUserInfo.getMsg());
		}
		User securityUser = new User(qqUserInfo.getNickname(), AuthorityUtils.createAuthorityList("ROLE_USER"));
		QQLoginToken token = new QQLoginToken(securityUser, qqLoginToken.getAccessToken(), qqLoginToken.getClientId(), securityUser.getAuthorities());
		return token;
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return QQLoginToken.class.isAssignableFrom(authentication);
	}
}
           

五、編寫QQ登入的配置

package com.huan.springsecurity.social.qq;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.web.client.RestTemplate;

/**
 * QQ登入的配置
 * 
 * @描述
 * @作者 huan
 * @時間 2018年3月3日 - 下午6:05:01
 */
public class QQLoginConfigure extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

	@Autowired
	private RestTemplate restTemplate;

	@Autowired
	private AuthenticationSuccessHandler authenticationSuccessHandler;

	@Override
	public void configure(HttpSecurity http) throws Exception {

		QQLoginFilter qqLoginFilter = new QQLoginFilter("/login/qq", restTemplate);
		qqLoginFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
		qqLoginFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
		http.authenticationProvider(new QQLoginProvider(restTemplate)).addFilterBefore(qqLoginFilter, AbstractPreAuthenticatedProcessingFilter.class);
	}
}
           

 六、将 QQLoginConfiger 加入到spring security的主配置中

spring security整合QQ登入

七、登入頁面: 看QQ登入的這個連結

<form action="auth" method="post">
		使用者名:<input type="text" name="user_name" value="admin"/><br />
		密碼:<input type="password" name="pass_word" value="admin"/><br/>
		記住我:<input type="checkbox" name="remember-me" value="true"><br/>
		<a href="login/qq" target="_blank" rel="external nofollow" >QQ登入</a>
		<input type="submit" value="登入">
	</form>
           

   注意:此時QQ的登入請求為  /login/qq 此時就會被 spring security 的 QQLoginFilter攔截到

八、登入結果

spring security整合QQ登入

 注意:

   1、代碼位址 : https://github.com/flyingDream/springsecurity

   2、完成代碼中的 QQLoginFilter中 的 CLIENT_ID,CLIENT_SECRET,REDIRECT_URI就可以實作QQ登入了,參數是什麼意思,代碼中有注釋。

繼續閱讀