springboot+springsecurity运用QQ登陆
尝试对接了一下QQ登录,此次记录一下如何利用springsecurity实现QQ登录的过程
1,在QQ互联上注册开发者身份
2、开发者身份审核通过后,创建一个自己的应用,此时需要记录如下三个信息
APP ID,APP Key,回调地址: 这个地址是你自己网站的回调地址,即QQ登录成功后,QQ会重定向到这个地址上
3,认证流程
首先需明白springsecurity登陆验证的流程以及对源码有一定的认识;springsecurity的流程以及源码解析百度一下很多
1,springseurity首先进入拦截器UsernamePasswordAuthenticationFilter执行attemptAuthentication(HttpServletRequest request, HttpServletResponse response
)方法
通过源码不难看出,springseurity默认采用的表单用户名密码的发送登陆,
QQ登陆需对词方法进行改造
UsernamePasswordAuthenticationFilter 继承AbstractAuthenticationProcessingFilter,所以我们编辑自己的类
QQAuthenticationProcessingFilter继承AbstractAuthenticationProcessingFilter 重新attemptAuthentication方法
@Slf4j
public class QQAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
/**
* client_id 即在QQ互联上创建应用的APP ID
*/
private static final String CLIENT_ID = "";
/**
* client_secret 即在QQ互联上创建应用的APP Key
*/
private static final String CLIENT_SECRET = "";
/**
* redirect_uri 即在QQ互联上创建应用的回调地址
*/
private static final String REDIRECT_URI = "";
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 QQAuthenticationProcessingFilter(String defaultFilterProcessesUrl, RestTemplate restTemplate) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "GET"));
this.restTemplate = restTemplate;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
String code = httpServletRequest.getParameter(CODE);
if (StringUtils.isEmpty(code)) {
log.info("没有获取到code的值,引导用户到QQ授权页面");
httpServletResponse.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(httpServletRequest));
return super.getAuthenticationManager().authenticate(qqLoginToken);
}
}
源码中UsernamePasswordAuthenticationToken中的属性无法满足我存放我特定的信息,当然可以通过UsernamePasswordAuthenticationToken的
setDetails方法设置其他信息,我这里就不那么麻烦了,直接建立新的类QQLoginToken继承AbstractAuthenticationToken;模仿源码
public class QQLoginToken extends AbstractAuthenticationToken {
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;
}
}
2,自定义验证,从springsecurity的源码看出发放最后调用
源码:
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
改造后:
QQLoginToken qqLoginToken = new QQLoginToken(openId, accessToken, CLIENT_ID);
qqLoginToken.setDetails(authenticationDetailsSource.buildDetails(httpServletRequest));
return super.getAuthenticationManager().authenticate(qqLoginToken);
改造后和源码没有区别;
码接下来执行ProviderManager(继承AuthenticationManager)
上图中红色框类是调用验证方法,常规情况需要定义一个类实现UserDetailsService;实现loadUserByUsername方法(一般数据库查询结果,生成一个org.springframework.security.core.userdetails.User);
并注册自定义的实现类。
QQ登陆我们重新认证方法
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;
private final ObjectMapper objectMapper = new ObjectMapper();
public QQLoginProvider(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@SneakyThrows
@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);
Map<String,Object> backDataMap = objectMapper.readValue(userInfo, new TypeReference<Map<String, Object>>() {});
/*
* ...
* 此处可以添加自己的业务
* 比如查询数据库获取已经存储的用户信息
* */
/*本处仅作简单的流程,可以定义自己的User类,实现UserDetails接口或者继承org.springframework.security.core.userdetails.User*/
User securityUser = new User( backDataMap.get("nickname").toString(),"",AuthorityUtils.createAuthorityList("ROLE_USER"));
QQLoginToken token = new QQLoginToken(securityUser, qqLoginToken.getAccessToken(), qqLoginToken.getClientId(), securityUser.getAuthorities());
return token;
}
@Override
public boolean supports(Class<?> aClass) {
return QQLoginToken.class.isAssignableFrom(aClass);
}
}
支持QQ登陆的流程代码已基本完成,接下来是配置注册
3,QQ登陆配置
4,将 QQLoginConfiger 加入到spring security的主配置中
@Configuration
@EnableWebSecurity
public class QQWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**"); // 不拦截静态资源
web.ignoring().antMatchers("/css/**"); // 不拦截静态资源
}
@Bean
public SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> qqLoginConfigure() {
return new QQLoginConfigure();
}
/**
* 注入身份管理器bean
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.apply(qqLoginConfigure()).and().formLogin()
.loginProcessingUrl("/login/qq") // 自定义登录路径
.and()
.logout()
.logoutUrl("/logout")
.and()
.authorizeRequests()// 对请求授权
// 这些页面不需要身份认证,其他请求需要认证
.antMatchers( "/login/qq").permitAll()
.anyRequest() // 任何请求
.authenticated() // 都需要身份认证
.and()
.csrf().disable();// 禁用跨站攻击
}
}
http.apply(qqLoginConfigure())是关键,不然不会生效
以上代码未在实际项目中运用,可能存在小瑕疵,本着学习的态度编写本文
本文相关链接:https://blog.csdn.net/fu_huo_1993/article/details/88350151
本文来自博客园,作者:小白有点黑,转载请注明原文链接:https://www.cnblogs.com/bbtxxz/p/15904780.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)