最近在了解第三方登入的内容,嘗試對接了一下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的主配置中
七、登入頁面: 看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攔截到
八、登入結果
注意:
1、代碼位址 : https://github.com/flyingDream/springsecurity
2、完成代碼中的 QQLoginFilter中 的 CLIENT_ID,CLIENT_SECRET,REDIRECT_URI就可以實作QQ登入了,參數是什麼意思,代碼中有注釋。