本文章的代码在第二篇(Spring Security使用(二) 异步登录 | 代码日志 (fanxing.live))的代码上继续完成
腾讯互联
申请地址:QQ互联官网首页
接入教程:网站应用接入概述
创建QQLoginUtil类
application.properties
在配置文件内添加下面几个配置
这些配置的内容在腾讯互联申请的应用就能看到
qq.appid=******
qq.appkey=******
qq.redirect_uri=http://127.0.0.1:8080/QQLogin
QQLoginUtil.java
新建一个 util包 com.example.security2.util,在里面创建这个类
package com.example.security2.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class QQLoginUtil {
@Value("${qq.appid}")
private String appid = "101513767";
@Value("${qq.appkey}")
private String appkey = "b1d978cefcf405388893d8e686d307b0";
@Value("${qq.redirect_uri}")
private String redirect_uri = "http://127.0.0.1:8080/QQLogin";
private ObjectMapper mapper;
//生成登录链接
public String getLoginUrl(Object state) throws UnsupportedEncodingException {
StringBuffer url = new StringBuffer("https://graph.qq.com/oauth2.0/authorize?");
url.append("response_type=code&");
url.append("client_id="+appid);
url.append("&redirect_uri="+ redirect_uri);
url.append("&state="+state);
System.out.println(url);
return url.toString();
}
//获取CODE
public Map getOpenId(String access_token) throws JsonProcessingException {
String url = "https://graph.qq.com/oauth2.0/me";
MultiValueMap params = new LinkedMultiValueMap();
params.add("access_token",access_token);
params.add("fmt","json");
String json = httpClient(url,params);
System.out.println(json);
if(mapper == null){
mapper = new ObjectMapper();
}
Map<String, Object> tmpMap=mapper.readValue(json, Map.class);
System.out.println(tmpMap);
return tmpMap;
}
public Map getAccessToken(String code) {
String url = "https://graph.qq.com/oauth2.0/token";
MultiValueMap params = new LinkedMultiValueMap();
params.add("grant_type","authorization_code");
params.add("client_id",appid);
params.add("client_secret",appkey);
params.add("code",code);
params.add("redirect_uri",redirect_uri);
params.add("fmt","json");
String json = httpClient(url,params);
System.out.println(json);
if(mapper == null){
mapper = new ObjectMapper();
}
Map<String, Object> tmpMap = null;
try{
tmpMap =mapper.readValue(json, Map.class);
}catch (Exception ex){
System.out.println("出错了");
}
return tmpMap;
}
//刷新access_token(自动续期)
public Map getNewAccessToken(String refresh_token){
String url = "https://graph.qq.com/oauth2.0/token";
MultiValueMap params = new LinkedMultiValueMap();
params.add("grant_type","refresh_token");
params.add("client_id",appid);
params.add("client_secret",appkey);
params.add("refresh_token",refresh_token);
String json = httpClient(url,params);
if(mapper == null){
mapper = new ObjectMapper();
}
Map<String, Object> tmpMap = null;
try{
tmpMap =mapper.readValue(json, Map.class);
}catch (Exception ex){
System.out.println("出错了");
}
return tmpMap;
}
//获取用户信息
public Map getUserInfo(String access_token,String openid) throws JsonProcessingException {
System.out.println("AccessToken="+access_token + ",OpenId="+openid);
String url = "https://graph.qq.com/user/get_user_info";
MultiValueMap params = new LinkedMultiValueMap();
params.add("access_token",access_token);
params.add("oauth_consumer_key",appid);
params.add("openid",openid);
String json = httpClient(url,params);
if(mapper == null){
mapper = new ObjectMapper();
}
Map<String, Object> tmpMap=mapper.readValue(json, Map.class);
System.out.println(tmpMap);
return tmpMap;
}
//直接一次获取到个人信息
public Map getUserInfoPlus(String code){
try{
String acode = getAccessToken(code).get("access_toke").toString();
String openid = getOpenId(acode).get("openid").toString();
return getUserInfo(acode,openid);
}catch (Exception ex){
System.out.println("出问题了");
return new HashMap();
}
}
//发送请求
public String httpClient(String url,MultiValueMap<String, String> params){
RestTemplate client = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
client.getMessageConverters().set(1,
new StringHttpMessageConverter(StandardCharsets.UTF_8)); // 支持中文编码
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(params, headers);
ResponseEntity<String> response = client.exchange(url, HttpMethod.POST, requestEntity, String.class);
return response.getBody();
}
}
测试是否可以获取到信息
在UserController类下面添加下面方法
@RequestMapping("/QQLogin")
public Map QQLogin(String code) {
return qqLoginUtil.getUserInfoPlus(code);
}
然后在安全框架配置中 对QQLogin进行忽略,要不然还得登录
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/js/**","/QQLogin").permitAll() //就在这一行里面添加
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/logintips")
.loginProcessingUrl("/check") //异步校验
.successHandler(successHandler)
.failureHandler(failHandle)
.permitAll()
.and()
.exceptionHandling().accessDeniedHandler(accessHandle)
.and()
.logout().permitAll();
http.csrf().disable();
}
接下来测试访问下我们的登录地址,然后使用QQ登录后会打印到浏览器上什么信息
看上面图片,说明我们可以获取到个人信息了,这说明,我们的QQLoginUtil类写好了。
安全框架
QQAuthenticationFilter
自定义一个身份验证过滤器,这个过滤器用来过滤 /QQLogin这个路径的内容
package com.example.security2;
import com.example.security2.util.QQLoginUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
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.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class QQAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
protected 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);
// 生成验证 authenticationToken,这个传过去code只是把获取的这个想办法传过去
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(code, null);
// 返回验证结果
System.out.println("进入校验qq登录");
return this.getAuthenticationManager().authenticate(authRequest);
}
}
QQAuthenticationManager
package com.example.security2;
import com.example.security2.pojo.User;
import com.example.security2.util.QQLoginUtil;
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.util.*;
public class QQAuthenticationManager implements AuthenticationManager {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//这个getname方法就是获取的我们上面传过来的code,如果为null的话,直接抛出token异常
if(authentication.getName() != null){
try{
//QQ登录操作类
QQLoginUtil qqLoginUtil = new QQLoginUtil();
Map access_token_get = qqLoginUtil.getAccessToken(authentication.getName());
//通过操作类获取到access_token以及refresh_token
String access_token = access_token_get.get("access_token").toString();
String refresh_token = access_token_get.get("refresh_token").toString();
//获取到openid
String openid = qqLoginUtil.getOpenId(access_token).get("openid").toString();
//判断是否成功获取到,其实access_token为null的话,直接就会报错了,直接抛出token异常,这一步要与不要都行
if(access_token == null || openid == null){
throw new BadCredentialsException("Token is invalid");
}
//直接在这里赋值权限,给它管理员权限
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
//将这俩东西保存到一个map中,到handler那里好获取
Map info = new HashMap();
info.put("access_token",access_token);
info.put("openid",openid);
info.put("refresh_token",refresh_token);
//返回这个认证的token到我们登录成功的Handler里,为什么是登录成功??因为,openid获取到了即使登录成功
//其实这里少一步,就是这个openid应该给我们数据库的用户来对比一下,看看是否绑定,但这里是QQ直登,所以不需要我们的数据库就可以登录
return new UsernamePasswordAuthenticationToken(info,null,authorities);
}catch (Exception ex){
throw new BadCredentialsException("Token is invalid");
}
}
throw new BadCredentialsException("Token is invalid");
}
}
refresh_token: 把这个也放到map里是因为,如果到时候access_token失效后,可以直接从SecurityContextHolder.getContext().getAuthentication().getName(); 这个方法中把这个map给获取出来,拿着这个refresh_token去刷新一遍
SecurityContextHolder.getContext().getAuthentication().getName();
示例内容
{access_token=9E716E069F480D05D1FB73B258F33242, refresh_token=C4536D2BFE439BCE6B898028909E2438, openid=CE722B59C6E0C995F5FD3D7013A0D271}
QQSusscessHandlerImpl
package com.example.security2;
import com.example.security2.util.QQLoginUtil;
import com.example.security2.vo.ResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
public class QQSusscessHandlerImpl implements AuthenticationSuccessHandler {
ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//登录成功了这里让他转发到一个页面,我们用这个test,让他qq登录成功后直接跳转到这个
request.getRequestDispatcher("/test").forward(request,response);
}
}
配置
到这里我们所需要的类已经写完了,只要我们把写的这些装到我们的配置里,那么就可以做测试了
package com.example.security2;
import com.example.security2.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
@Configurable
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomUserDetailsService customUserDetailsService;
@Autowired
SuccessHandlerImpl successHandler;
@Autowired
FailHandleImpl failHandle;
@Autowired
AccessHandleImpl accessHandle;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder(getPasswordEncoder());
}
public PasswordEncoder getPasswordEncoder(){
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
};
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/js/**").permitAll() //.antMatchers("/js/**","/QQLogin").permitAll() 未修改前代码
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/logintips")
.loginProcessingUrl("/check") //异步校验
.successHandler(successHandler)
.failureHandler(failHandle)
.permitAll()
.and()
.exceptionHandling().accessDeniedHandler(accessHandle)
.and()
.logout().permitAll()
;
http.csrf().disable();
http.addFilterAt(qqAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); //使自定义的过滤器生效
}
//获取我们的自定义的过滤器,并且在其里面添加Manager校验以及登录成功后的过滤器
private QQAuthenticationFilter qqAuthenticationFilter(){
QQAuthenticationFilter authenticationFilter = new QQAuthenticationFilter("/QQLogin"); //这一行是让它拦截/QQLogin路径的地址,也就是我们的回调地址
authenticationFilter.setAuthenticationManager(new QQAuthenticationManager());
authenticationFilter.setAuthenticationSuccessHandler(new QQSusscessHandlerImpl());
return authenticationFilter;
}
}
测试
修改UserController类下的test
@RequestMapping("/test")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public Object Test(){
return SecurityContextHolder.getContext().getAuthentication().getName();
}
由于我们是转发到了test页面,所以路径不会改变,但是内容却变成了我们想要获取的
如果项目里面接入了安全框架,这里应该要跳转我们的首页或者其他页面
欢迎访问:http://www.fanxing.live