准备工作
1、在QQ互联申请成为开发者,并创建应用,得到APP ID 和 APP Key。
2、了解QQ登录时的网站应用接入流程。(必须看完看懂)
为了方便各位测试,直接把我自己申请的贡献出来:
参数 | 值 |
---|---|
APP ID | 101386962 |
APP Key | 2a0f820407df400b84a854d054be8b6a |
回调地址 | http://www.ictgu.cn/login/qq |
提醒:因为回调地址不是http://localhost,所以在启动我提供的demo时,需要在host文件中添加一行:
127.0.0.1 www.ictgu.cn
Github 地址
https://github.com/ChinaSilence/any-spring-security
运行应用
1、进入 security-oauth2-qq 目录,执行:
mvn spring-boot:run
2、此处假设你已经修改好host,并启动成功,访问http://www.ictgu.cn
3、登录 -> QQ登录 -> 个人中心,将会看到个人信息。
4、删除host中添加的那一行。
后端详解
1、自定义 QQAuthenticationFilter 继承 AbstractAuthenticationProcessingFilter
package com.spring4all.filter.qq;
import com.alibaba.fastjson.JSON;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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 javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class QQAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private final static String CODE = "code";
/**
* 获取 Token 的 API
*/
private final static String accessTokenUri = "https://graph.qq.com/oauth2.0/token";
/**
* grant_type 由腾讯提供
*/
private final static String grantType = "authorization_code";
/**
* client_id 由腾讯提供
*/
static final String clientId = "101386962";
/**
* client_secret 由腾讯提供
*/
private final static String clientSecret = "2a0f820407df400b84a854d054be8b6a";
/**
* redirect_uri 腾讯回调地址
*/
private final static String redirectUri = "http://www.ictgu.cn/login/qq";
/**
* 获取 OpenID 的 API 地址
*/
private final static String openIdUri = "https://graph.qq.com/oauth2.0/me?access_token=";
/**
* 获取 token 的地址拼接
*/
private final static String TOKEN_ACCESS_API = "%s?grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s";
public QQAuthenticationFilter(String defaultFilterProcessesUrl) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "GET"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String code = request.getParameter(CODE);
System.out.println("Code : " + code);
String tokenAccessApi = String.format(TOKEN_ACCESS_API, accessTokenUri, grantType, clientId, clientSecret, code, redirectUri);
QQToken qqToken = this.getToken(tokenAccessApi);
System.out.println(JSON.toJSONString(qqToken));
if (qqToken != null){
String openId = getOpenId(qqToken.getAccessToken());
System.out.println(openId);
if (openId != null){
// 生成验证 authenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(qqToken.getAccessToken(), openId);
// 返回验证结果
return this.getAuthenticationManager().authenticate(authRequest);
}
}
return null;
}
private QQToken getToken(String tokenAccessApi) throws IOException{
Document document = Jsoup.connect(tokenAccessApi).get();
String tokenResult = document.text();
String[] results = tokenResult.split("&");
if (results.length == 3){
QQToken qqToken = new QQToken();
String accessToken = results[0].replace("access_token=", "");
int expiresIn = Integer.valueOf(results[1].replace("expires_in=", ""));
String refreshToken = results[2].replace("refresh_token=", "");
qqToken.setAccessToken(accessToken);
qqToken.setExpiresIn(expiresIn);
qqToken.setRefresh_token(refreshToken);
return qqToken;
}
return null;
}
private String getOpenId(String accessToken) throws IOException{
String url = openIdUri + accessToken;
Document document = Jsoup.connect(url).get();
String resultText = document.text();
Matcher matcher = Pattern.compile("\"openid\":\"(.*?)\"").matcher(resultText);
if (matcher.find()){
return matcher.group(1);
}
return null;
}
class QQToken {
/**
* token
*/
private String accessToken;
/**
* 有效期
*/
private int expiresIn;
/**
* 刷新时用的 token
*/
private String refresh_token;
String getAccessToken() {
return accessToken;
}
void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public int getExpiresIn() {
return expiresIn;
}
void setExpiresIn(int expiresIn) {
this.expiresIn = expiresIn;
}
public String getRefresh_token() {
return refresh_token;
}
void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
}
}
说明:Filter 过滤时执行的方法是 doFilter(),由于 QQAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter,所以过滤时使用的是父类的doFilter() 方法,代码如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
说明:doFilter()方法中,有一步是 attemptAuthentication(request, response) 即为 QQAuthenticationFilter 中实现的方法。这个方法中调用了 this.getAuthenticationManager().authenticate(authRequest),这里自定义了类 QQAuthenticationManager,代码如下:
package com.spring4all.filter.qq;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.spring4all.domain.QQUser;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static com.spring4all.filter.qq.QQAuthenticationFilter.clientId;
public class QQAuthenticationManager implements AuthenticationManager {
private static final List
<
GrantedAuthority
>
AUTHORITIES = new ArrayList
<
>
();
/**
* 获取 QQ 登录信息的 API 地址
*/
private final static String userInfoUri = "https://graph.qq.com/user/get_user_info";
/**
* 获取 QQ 用户信息的地址拼接
*/
private final static String USER_INFO_API = "%s?access_token=%s
&
oauth_consumer_key=%s
&
openid=%s";
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
if (auth.getName() != null
&
&
auth.getCredentials() != null) {
QQUser user = getUserInfo(auth.getName(), (String) (auth.getCredentials()));
return new UsernamePasswordAuthenticationToken(user,
null, AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
private QQUser getUserInfo(String accessToken, String openId) {
String url = String.format(USER_INFO_API, userInfoUri, accessToken, clientId, openId);
Document document;
try {
document = Jsoup.connect(url).get();
} catch (IOException e) {
throw new BadCredentialsException("Bad Credentials!");
}
String resultText = document.text();
JSONObject json = JSON.parseObject(resultText);
QQUser user = new QQUser();
user.setNickname(json.getString("nickname"));
user.setGender(json.getString("gender"));
user.setProvince(json.getString("province"));
user.setYear(json.getString("year"));
user.setAvatar(json.getString("figureurl_qq_2"));
return user;
}
}
说明:QQAuthenticationManager 的作用是通过传来的 token 和 openID 去请求腾讯的getUserInfo接口,获取腾讯用户的信息,并生成新的 Authtication 对象。
接下来就是要将 QQAuthenticationFilter 与 QQAuthenticationManager 结合,配置到 Spring Security 的过滤器链中。代码如下:
package com.spring4all.config;
import com.spring4all.filter.qq.QQAuthenticationFilter;
import com.spring4all.filter.qq.QQAuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
/**
* 匹配 "/" 路径,不需要权限即可访问
* 匹配 "/user" 及其以下所有路径,都需要 "USER" 权限
* 登录地址为 "/login",登录成功默认跳转到页面 "/user"
* 退出登录的地址为 "/logout",退出成功后跳转到页面 "/login"
* 默认启用 CSRF
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/user/**").hasRole("USER")
.and()
.formLogin().loginPage("/login").defaultSuccessUrl("/user")
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
// 在 UsernamePasswordAuthenticationFilter 前添加 QQAuthenticationFilter
http.addFilterAt(qqAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 自定义 QQ登录 过滤器
*/
private QQAuthenticationFilter qqAuthenticationFilter(){
QQAuthenticationFilter authenticationFilter = new QQAuthenticationFilter("/login/qq");
authenticationFilter.setAuthenticationManager(new QQAuthenticationManager());
return authenticationFilter;
}
}
说明:由于腾讯的回调地址是 /login/qq,所以 QQAuthenticationFilter 拦截的路径是 /login/qq,然后将 QQAuthenticationFilter 置于 UsernamePasswordAuthenticationFilter 相同级别的位置。
前端说明
前端很简单,一个QQ登陆按钮,代码如下:
<a href="https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101386962&redirect_uri=http://www.ictgu.cn/login/qq&state=test" class="btn btn-primary btn-block">QQ登录</a>
其他说明
腾讯官网原话:
openid是此网站上唯一对应用户身份的标识,网站可将此ID进行存储便于用户下次登录时辨识其身份,或将其与用户在网站上的原有账号进行绑定。
通过QQ登录获取的 openid 用于与自己网站的账号一一对应。
相关文章
相关资料
其他
如需转载,请联系社区 http://www.spring4all.com