使用SpringSocial 开发第三方QQ登录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | OAuth 协议 OAuth协议要解决的问题 解决传统模式的授权(授权协议),认证资源访问问题 OAuth协议中的各种角色 Privider 服务提供商(提供令牌) (如微信) Authorization Server 认证服务器 ResourceServer 资源服务器 Resource Owner 资源所有者 (用户) Client 第三方应用 OAuth协议运行流程 0 访问Client 1 将用户导向认证服务器 2 用户同意授权 3 返回Client并携带授权码 4 申请令牌 5 发放令牌 ( 1 - 5 是OAuth标准流程) 6 获取用户信息 (每个服务商提供返回的字段不一样) 7 根据用户信息构建Authentictioin并放入SecurityContext OAuth协议授权模式 授权码模式 (authorization code) 认证授权在服务器提供商认证服务器完成,更安全 密码模式(Resource owner password credentials) 客户端模式(client credentials) 简化模式(implicit) SpringSocial 基本原理 (就是封装了OAuth2的基本流程 SocialAuthenticationFilter加入到过滤器链中) ServiceProvider (AbstractOAuth2ServiceProvider服务提供商的抽象实现) OAuth2Operations (OAuth2Template 封装 1 - 5 会帮你去完成OAuth的认证流程) Api(AbstractOAuth2ApiBinding获取用户信息抽象实现 6 ) 第 7 步是在第三方应用上操作 Connection(OAuth2Connection服务提供商的信息) 是由 ConnectionFactory(OAuth2ConnectionFactory) 创建的 ConnectionFactory 中是包含(ServiceProvider 的) ApiAdapter (在Aip 和 Connection之间做一个适配,因为Connection字段是固定的,服务商是个性化的) 用户和第三方用户有个关联表,UserConnection UserConnection 表是由 JdbcUsersConnectionRepository(UsersConnectionRepository) 这个类进行操作CRUD |
第一步: 根据图2 先 构建ServiceProvider
(6获取用户信息): 根据OAuth2Operations (OAuth2Template),Api(AbstractOAuth2ApiBinding) 构建 ServiceProvider
创建API 获取用户信息接口
1 2 3 4 5 6 7 8 9 10 | package com.imooc.security.core.social.qq.api; /** * @Title: QQ * @ProjectName spring-security-main * @date 2020/12/710:21 */ public interface QQ { QQUserInfo getUserInfo(); } |
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package com.imooc.security.core.social.qq.api; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.springframework.social.oauth2.AbstractOAuth2ApiBinding; import org.springframework.social.oauth2.TokenStrategy; /** * @Title: QQImpl * @ProjectName spring-security-main * @date 2020/12/710:22 */ @Slf4j public class QQImpl extends AbstractOAuth2ApiBinding implements QQ { // 获取openId private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s" ; // 获取用户信息 accessToken 父类会处理,这里不用拼接参数了 private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s" ; private String appId; private String openId; private ObjectMapper objectMapper = new ObjectMapper(); public QQImpl(String accessToken, String appId) { super (accessToken,TokenStrategy.ACCESS_TOKEN_PARAMETER); // 默认父类是放在header中,这里要放在参数里 this .appId = appId; // 获取openId String url = String.format(URL_GET_OPENID, accessToken); String result = getRestTemplate().getForObject(url, String. class ); log.info(result); // 截取一下openId this .openId = StringUtils.substringBetween(result, "\"openid\":\"" , "\"}" ); } @Override public QQUserInfo getUserInfo() { String url = String.format(URL_GET_USERINFO, appId, openId); String result = getRestTemplate().getForObject(url, String. class ); log.info(result); QQUserInfo userInfo = null ; try { userInfo = objectMapper.readValue(result, QQUserInfo. class ); userInfo.setOpenId(openId); return userInfo; } catch (Exception e) { throw new RuntimeException( "获取用户信息失败" , e); } } } |
QQUserInfo 实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | package com.imooc.security.core.social.qq.api; import lombok.Data; @Data public class QQUserInfo { /** * 返回码 */ private String ret; /** * 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。 */ private String msg; /** * */ private String openId; /** * 不知道什么东西,文档上没写,但是实际api返回里有。 */ private String is_lost; /** * 省(直辖市) */ private String province; /** * 市(直辖市区) */ private String city; /** * 出生年月 */ private String year; /** * 用户在QQ空间的昵称。 */ private String nickname; /** * 大小为30×30像素的QQ空间头像URL。 */ private String figureurl; /** * 大小为50×50像素的QQ空间头像URL。 */ private String figureurl_1; /** * 大小为100×100像素的QQ空间头像URL。 */ private String figureurl_2; /** * 大小为40×40像素的QQ头像URL。 */ private String figureurl_qq_1; /** * 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。 */ private String figureurl_qq_2; /** * 性别。 如果获取不到则默认返回”男” */ private String gender; private String gender_type; private String constellation; /** * 标识用户是否为黄钻用户(0:不是;1:是)。 */ private String is_yellow_vip; /** * 标识用户是否为黄钻用户(0:不是;1:是) */ private String vip; /** * 黄钻等级 */ private String yellow_vip_level; /** * 黄钻等级 */ private String level; /** * 标识是否为年费黄钻用户(0:不是; 1:是) */ private String is_yellow_year_vip; private String figureurl_qq; private String figureurl_type; } |
创建QQOAuth2Template
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package com.imooc.security.core.social.qq.connet; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.social.oauth2.AccessGrant; import org.springframework.social.oauth2.OAuth2Template; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import java.nio.charset.Charset; @Slf4j public class QQOAuth2Template extends OAuth2Template { public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) { super (clientId, clientSecret, authorizeUrl, accessTokenUrl); setUseParametersForClientAuthentication( true ); // 设置true client_secret client_id才会带上这两个参数 } @Override protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) { String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String. class ); // 因为qq返回的是 用&拼接的字符串 需要自己处理一下 log.info( "获取accessToke的响应:" +responseStr); String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&" ); String accessToken = StringUtils.substringAfterLast(items[ 0 ], "=" ); Long expiresIn = new Long(StringUtils.substringAfterLast(items[ 1 ], "=" )); String refreshToken = StringUtils.substringAfterLast(items[ 2 ], "=" ); return new AccessGrant(accessToken, null , refreshToken, expiresIn); } @Override protected RestTemplate createRestTemplate() { RestTemplate restTemplate = super .createRestTemplate(); // qq返回的是text/html 默认的template没有添加处理这样的类型,自己添加一个 restTemplate.getMessageConverters().add( new StringHttpMessageConverter(Charset.forName( "UTF-8" ))); return restTemplate; } } |
根据 QQOAuth2Template ,QQImpl 创建 QQServiceProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package com.imooc.security.core.social.qq.connet; import com.imooc.security.core.social.qq.api.QQ; import com.imooc.security.core.social.qq.api.QQImpl; import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider; import org.springframework.social.oauth2.OAuth2Template; /** * @Title: QQServiceProvider * @ProjectName spring-security-main * @date 2020/12/711:01 */ public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> { private String appId; // 1.导向认证服务器rul private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize" ; // 4.用户同意授权,返回授权码去申请令牌的url private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token" ; public QQServiceProvider(String appId, String appSecret) { // super(oauth2Operations); // OAuth2Template 用系统默认的 // super(new OAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN)); // 默认Template不能满足使用 super ( new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN)); this .appId = appId; } @Override public QQ getApi(String accessToken) { return new QQImpl(accessToken, appId); } } |
图2右边的代码已经完成
图2 左边开发
创建QQAdapter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package com.imooc.security.core.social.qq.connet; import com.imooc.security.core.social.qq.api.QQ; import com.imooc.security.core.social.qq.api.QQUserInfo; import org.springframework.social.connect.ApiAdapter; import org.springframework.social.connect.ConnectionValues; import org.springframework.social.connect.UserProfile; /** * 服务商和第三方应用之间做适配的,适配的类型就是QQ */ public class QQAdapter implements ApiAdapter<QQ> { /** * 测试方法 * @param api * @return */ @Override public boolean test(QQ api) { return true ; } /** * 主要方法, qq api的信息设置到connectionValues * @param api * @param values */ @Override public void setConnectionValues(QQ api, ConnectionValues values) { QQUserInfo userInfo = api.getUserInfo(); values.setDisplayName(userInfo.getNickname()); // 头像 values.setImageUrl(userInfo.getFigureurl_qq_1()); // 个人主页 values.setProfileUrl( null ); values.setProviderUserId(userInfo.getOpenId()); } @Override public UserProfile fetchUserProfile(QQ api) { return null ; } @Override public void updateStatus(QQ api, String message) { } } |
根据 QQAdapter 和 QQServiceProvider 可以构建QQConnectionFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package com.imooc.security.core.social.qq.connet; import com.imooc.security.core.social.qq.api.QQ; import org.springframework.social.connect.support.OAuth2ConnectionFactory; public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> { /** * 根据 QQAdapter 和 QQServiceProvider 可以构建QQConnectionFactory * @param providerId 服务商Id * @param appId * @param appSecret */ public QQConnectionFactory(String providerId, String appId, String appSecret) { super (providerId, new QQServiceProvider(appId, appSecret), new QQAdapter()); } } |
建表 查找 JdbcUsersConnectionRepository.sql
创建 SocialConfig 配置类,启用Social
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | package com.imooc.security.core.social; import com.imooc.security.core.properties.SecurityProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.social.config.annotation.EnableSocial; import org.springframework.social.config.annotation.SocialConfigurerAdapter; import org.springframework.social.connect.ConnectionFactoryLocator; import org.springframework.social.connect.ConnectionSignUp; import org.springframework.social.connect.UsersConnectionRepository; import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository; import org.springframework.social.connect.web.ProviderSignInUtils; import org.springframework.social.security.SpringSocialConfigurer; import javax.sql.DataSource; /** * @Title: SocialConfig * @ProjectName spring-security-main * @date 2020/12/713:57 */ @Configuration @EnableSocial @Slf4j public class SocialConfig extends SocialConfigurerAdapter { @Autowired private SecurityProperties securityProperties; // 数据源 @Autowired private DataSource dataSource; @Autowired (required = false ) private ConnectionSignUp connectionSignUp; /** * 默认使用InMemoryUsersConnectionRepository类 , Primary 候选bean中优先使用,Bean 添加这两个注解 * @param connectionFactoryLocator 作用就是查找ConnectionFactory * @return */ @Primary @Bean @Override public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) { JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText()); // 配置数据库前缀 // repository.setTablePrefix("imooc_"); if (connectionSignUp != null ) { repository.setConnectionSignUp(connectionSignUp); } return repository; } /** * 配置自定义路径 默认是auth *需要 把SpringSocialConfigure 加入到过滤器链上 * @return */ @Bean public SpringSocialConfigurer imoocSocialSecurityConfig() { String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl(); log.info(filterProcessesUrl); ImoocSpringSocialConfigurer imoocSpringSocialConfigurer = new ImoocSpringSocialConfigurer(filterProcessesUrl); // 登录成功 回调的url imoocSpringSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl()); return imoocSpringSocialConfigurer; // return new SpringSocialConfigurer(); } @Bean public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) { return new ProviderSignInUtils(connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator)) { }; } } |
创建 ImoocSpringSocialConfigurer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package com.imooc.security.core.social; import org.springframework.social.security.SocialAuthenticationFilter; import org.springframework.social.security.SpringSocialConfigurer; public class ImoocSpringSocialConfigurer extends SpringSocialConfigurer { /** * 修改默认的路由auth 可配置,默认是auth,自定义回调地址, 要和备案的一致 */ private String filterProcessesUrl; public ImoocSpringSocialConfigurer(String filterProcessesUrl) { this .filterProcessesUrl = filterProcessesUrl; } @Override protected <T> T postProcess(T object) { SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super .postProcess(object); filter.setFilterProcessesUrl(filterProcessesUrl); return (T) filter; } } |
创建 QQAutoConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package com.imooc.security.core.social.qq.config; import com.imooc.security.core.properties.QQProperties; import com.imooc.security.core.properties.SecurityProperties; import com.imooc.security.core.social.qq.connet.QQConnectionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter; import org.springframework.context.annotation.Configuration; import org.springframework.social.connect.ConnectionFactory; @Configuration @ConditionalOnProperty (prefix = "imooc.security.social.qq" , name = "app-id" ) // 当配置文件中,有配置属性才会生效 public class QQAutoConfig extends SocialAutoConfigurerAdapter { @Autowired private SecurityProperties securityProperties; @Override protected ConnectionFactory<?> createConnectionFactory() { QQProperties qqConfig = securityProperties.getSocial().getQq(); return new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret()); } } |
把SpringSocialConfigurer 加入到过滤器链上
1 2 3 4 | @Autowired private SpringSocialConfigurer imoocSocialSecurityConfig; .apply(imoocSocialSecurityConfig) |
imooc-signIn.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <! DOCTYPE html> < html > < head > < meta charset="UTF-8"> < title >登录</ title > </ head > < body > < h2 >标准登录页面</ h2 > < h3 >表单登录</ h3 > < form action="/authentication/form" method="post"> < table > < tr > < td >用户名:</ td > < td >< input type="text" name="username"></ td > </ tr > < tr > < td >密码:</ td > < td >< input type="password" name="password"></ td > </ tr > < tr > < td >图形验证码:</ td > < td > < input type="text" name="imageCode"> < img src="/code/image?width=200"> </ td > </ tr > < tr > < td colspan='2'>< input name="remember-me" type="checkbox" value="true" />记住我</ td > </ tr > < tr > < td colspan="2">< button type="submit">登录</ button ></ td > </ tr > </ table > </ form > < h3 >短信登录</ h3 > < form action="/authentication/mobile" method="post"> < table > < tr > < td >手机号:</ td > < td >< input type="text" name="mobile" value="13012345678"></ td > </ tr > < tr > < td >短信验证码:</ td > < td > < input type="text" name="smsCode"> < a href="/code/sms?mobile=13012345678">发送验证码</ a > </ td > </ tr > < tr > < td colspan="2">< button type="submit">登录</ button ></ td > </ tr > </ table > </ form > < br > < h3 >社交登录</ h3 > < a href="/qqLogin/callback.do">QQ登录2</ a > < a href="/auth/qq">QQ登录1</ a > < a href="/qqLogin/weixin">微信登录</ a > </ body > </ html > |
注册问题 SocialConfig
首次登录自动注册
DemoConnectionSignUp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package com.imooc.security; import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionSignUp; import org.springframework.stereotype.Component; @Component public class DemoConnectionSignUp implements ConnectionSignUp { @Override public String execute(Connection<?> connection) { //根据社交用户信息默认创建用户并返回用户唯一标识 return connection.getDisplayName(); } } |
点击按钮进行手动注册
1 2 3 4 5 6 7 8 9 10 | @Autowired private ProviderSignInUtils providerSignInUtils; @PostMapping ( "/regist" ) public void regist(User user, HttpServletRequest request) { //不管是注册用户还是绑定用户,都会拿到一个用户唯一标识。 String userId = user.getUsername(); providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request)); } |
获取当前用户信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * 前端可以显示 当前登录的用户信息 * @param request * @return */ @GetMapping ( "/social/user" ) public SocialUserInfo getSocialUserInfo(HttpServletRequest request) { SocialUserInfo userInfo = new SocialUserInfo(); // 在session中取出用户信息 Connection<?> connection = providerSignInUtils.getConnectionFromSession( new ServletWebRequest(request)); userInfo.setProviderId(connection.getKey().getProviderId()); userInfo.setProviderUserId(connection.getKey().getProviderUserId()); userInfo.setNickname(connection.getDisplayName()); userInfo.setHeadimg(connection.getImageUrl()); return userInfo; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义