1 原理介绍

根据慕课网的网上教程开发的

image.png

在spring security中的执行逻辑图

通过SocialAuthenticationFilter拦截到第三方登录的请求后,根据对应的ConnectionFactory获取对应第三方的用户信息

image.png

自定义第三方登录逻辑

image.png

2 代码开发

2.1 根据以上逻辑图,先开发 OAuth2ApiBinding 类

QQUserInfo.java

  1. /**
  2. * qq互联:http://wiki.connect.qq.com/get_user_info
  3. */
  4. @Data
  5. public class QQUserInfo {
  6. /**
  7. * 返回码
  8. */
  9. private String ret;
  10. /**
  11. * 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
  12. */
  13. private String msg;
  14. /**
  15. *
  16. */
  17. private String openId;
  18. /**
  19. * 不知道什么东西,文档上没写,但是实际api返回里有。
  20. */
  21. private String is_lost;
  22. /**
  23. * 省(直辖市)
  24. */
  25. private String province;
  26. /**
  27. * 市(直辖市区)
  28. */
  29. private String city;
  30. /**
  31. * 出生年月
  32. */
  33. private String year;
  34. /**
  35. * 用户在QQ空间的昵称。
  36. */
  37. private String nickname;
  38. /**
  39. * 大小为30×30像素的QQ空间头像URL。
  40. */
  41. private String figureurl;
  42. /**
  43. * 大小为50×50像素的QQ空间头像URL。
  44. */
  45. private String figureurl_1;
  46. /**
  47. * 大小为100×100像素的QQ空间头像URL。
  48. */
  49. private String figureurl_2;
  50. /**
  51. * 大小为40×40像素的QQ头像URL。
  52. */
  53. private String figureurl_qq_1;
  54. /**
  55. * 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。
  56. */
  57. private String figureurl_qq_2;
  58. /**
  59. * 性别。 如果获取不到则默认返回”男”
  60. */
  61. private String gender;
  62. /**
  63. * 标识用户是否为黄钻用户(0:不是;1:是)。
  64. */
  65. private String is_yellow_vip;
  66. /**
  67. * 标识用户是否为黄钻用户(0:不是;1:是)
  68. */
  69. private String vip;
  70. /**
  71. * 黄钻等级
  72. */
  73. private String yellow_vip_level;
  74. /**
  75. * 黄钻等级
  76. */
  77. private String level;
  78. /**
  79. * 标识是否为年费黄钻用户(0:不是; 1:是)
  80. */
  81. private String is_yellow_year_vip;
  82. }

QQ.java

  1. public interface QQ {
  2. /**
  3. * 获取用户信息
  4. */
  5. public QQUserInfo getUserInfo();
  6. }

QQImpl.java

  1. @Slf4j
  2. public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
  3. /**
  4. * 获取 openId 的url地址
  5. */
  6. private static final String QQ_URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
  7. /**
  8. * 获取用户信息
  9. * http://wiki.connect.qq.com/get_user_info(access_token由父类提供)
  10. */
  11. private static final String QQ_URL_GET_USER_INFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
  12. /**
  13. * appId 配置文件读取
  14. */
  15. private String appId;
  16. /**
  17. * openId 请求QQ_URL_GET_OPENID返回
  18. */
  19. private String openId;
  20. /**
  21. * 构造方法获取openId
  22. *
  23. * @param accessToken
  24. * @param appId
  25. */
  26. public QQImpl(String accessToken, String appId) {
  27. //access_token作为查询参数来携带。
  28. super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
  29. this.appId = appId;
  30. String url = String.format(QQ_URL_GET_OPENID, accessToken);
  31. String result = getRestTemplate().getForObject(url, String.class);
  32. log.info("【QQImpl】 获取openId地址:QQ_URL_GET_OPENID={} 获取openId结果:result={}", QQ_URL_GET_OPENID, result);
  33. this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
  34. }
  35. @Override
  36. public QQUserInfo getUserInfo() {
  37. String url = String.format(QQ_URL_GET_USER_INFO, appId, openId);
  38. String result = getRestTemplate().getForObject(url, String.class);
  39. //log.info("【QQImpl】 QQ_URL_GET_USER_INFO={} result={}", QQ_URL_GET_USER_INFO, result);
  40. QQUserInfo userInfo = null;
  41. try {
  42. userInfo = JSON.parseObject(result, QQUserInfo.class);
  43. userInfo.setOpenId(openId);
  44. return userInfo;
  45. } catch (Exception e) {
  46. throw new RuntimeException("获取用户信息失败", e);
  47. }
  48. }
  49. }

开发OAuth2Template

QQOAuth2Template.java

  1. @Slf4j
  2. public class QQOAuth2Template extends OAuth2Template {
  3. public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
  4. super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
  5. //保证可以带着clent_id 和client_secret参数
  6. setUseParametersForClientAuthentication(true);
  7. }
  8. @Override
  9. protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
  10. String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
  11. log.info("【QQOAuth2Template】获取accessToke的响应:responseStr={}", responseStr);
  12. String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
  13. //http://wiki.connect.qq.com/使用authorization_code获取access_token
  14. //access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
  15. String accessToken = StringUtils.substringAfterLast(items[0], "=");
  16. Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
  17. String refreshToken = StringUtils.substringAfterLast(items[2], "=");
  18. return new AccessGrant(accessToken, null, refreshToken, expiresIn);
  19. }
  20. /**
  21. * 坑,日志debug模式才打印出来 处理qq返回的text/html 类型数据
  22. *
  23. * @return
  24. */
  25. @Override
  26. protected RestTemplate createRestTemplate() {
  27. RestTemplate restTemplate = super.createRestTemplate();
  28. restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
  29. return restTemplate;
  30. }
  31. }

开发OAuth2ServiceProvider

QQOAuth2ServiceProvider.java

  1. public class QQOAuth2ServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
  2. public String appId;
  3. /**
  4. * 获取 授权码code 地址
  5. */
  6. private static final String QQ_URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
  7. /**
  8. * 获取access_token 也就是令牌
  9. */
  10. private static final String QQ_URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
  11. public QQOAuth2ServiceProvider(String appId, String appSecret) {
  12. super(new QQOAuth2Template(appId, appSecret, QQ_URL_AUTHORIZE, QQ_URL_ACCESS_TOKEN));
  13. this.appId = appId;
  14. }
  15. @Override
  16. public QQ getApi(String accessToken) {
  17. return new QQImpl(accessToken, appId);
  18. }
  19. }

开发ApiAdapter

QQAdapter.java

  1. /**
  2. * qq返回的信息为spring social提供的适配器
  3. */
  4. public class QQAdapter implements ApiAdapter<QQ> {
  5. @Override
  6. public boolean test(QQ api) {
  7. //测试qq服务器是否正常工作
  8. return true;
  9. }
  10. @Override
  11. public void setConnectionValues(QQ api, ConnectionValues values) {
  12. //将获取的qq用户信息,转为social的标准用户信息,保存到数据库中
  13. QQUserInfo userInfo = api.getUserInfo();
  14. values.setProviderUserId(userInfo.getOpenId());//openId 唯一标识
  15. values.setDisplayName(userInfo.getNickname());
  16. values.setImageUrl(userInfo.getFigureurl_qq_1());
  17. values.setProfileUrl(null);
  18. }
  19. @Override
  20. public UserProfile fetchUserProfile(QQ api) {
  21. return null;
  22. }
  23. @Override
  24. public void updateStatus(QQ api, String message) {
  25. //返回用户操作信息,例如微博用户,可以更新微博状态等。在QQ中没有这个概念
  26. }
  27. }

开发OAuth2ConnectionFactory

QQConnectionFactory.java

  1. public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
  2. public QQConnectionFactory(String providerId, String appId, String appSecret) {
  3. super(providerId, new QQOAuth2ServiceProvider(appId, appSecret), new QQAdapter());
  4. }
  5. }

配置ConnectionFactor到系统中

  1. @Configuration
  2. // 当配置了app-id的时候才启用
  3. @ConditionalOnProperty(prefix = "system.security.social.qq", name = "app-id")
  4. public class QQAutoConfig extends SocialConfigurerAdapter {
  5. @Autowired
  6. private SystemSecurityProperties systemSecurityProperties;
  7. /**
  8. * 添加QQConnectionFacitonry到ConnectionFactionry中
  9. *
  10. * @param cfConfig
  11. * @param env
  12. */
  13. @Override
  14. public void addConnectionFactories(ConnectionFactoryConfigurer cfConfig, Environment env) {
  15. QQProperties qq = systemSecurityProperties.getSocial().getQq();
  16. QQConnectionFactory qqConnectionFactory = new QQConnectionFactory(qq.getProviderId(), qq.getAppId(),
  17. qq.getAppKey());
  18. cfConfig.addConnectionFactory(qqConnectionFactory);
  19. }
  20. // 后补:做到处理注册逻辑的时候发现的一个bug:登录完成后,数据库没有数据,但是再次登录却不用注册了
  21. // 就怀疑是否是在内存中存储了。结果果然发现这里父类的内存ConnectionRepository覆盖了SocialConfig中配置的jdbcConnectionRepository
  22. // 这里需要返回null,否则会返回内存的 ConnectionRepository
  23. @Override
  24. public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
  25. return null;
  26. }
  27. }

配置SocialConfig

SocialConfig.java

  1. @Configuration
  2. @EnableSocial
  3. public class SocialConfig extends SocialConfigurerAdapter {
  4. @Autowired
  5. private DataSource dataSource;
  6. //第三方用户登录,是否默认直接注册
  7. @Autowired(required=false)
  8. private ConnectionSignUp connectionSignUp;
  9. @Autowired
  10. private SystemSecurityProperties systemSecurityProperties;
  11. /**
  12. * Default implementation of {@link #getUsersConnectionRepository(ConnectionFactoryLocator)} that creates an in-memory repository.
  13. */
  14. public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
  15. JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
  16. connectionFactoryLocator, Encryptors.noOpText());
  17. repository.setTablePrefix("sys_");
  18. //首次使用第三方用户登录时,自动创建系统用户
  19. if(connectionSignUp != null) {
  20. repository.setConnectionSignUp(connectionSignUp);
  21. }
  22. return repository;
  23. }
  24. @Override
  25. public UserIdSource getUserIdSource() {
  26. return () -> {
  27. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  28. if (authentication == null) {
  29. throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");
  30. }
  31. return authentication.getName();
  32. };
  33. }
  34. @Bean
  35. public SpringSocialConfigurer systemSpringSocialConfigurer() {
  36. //自定义过滤器拦截路径
  37. // String filterProcessesUrl = systemSecurityProperties.getSocial().getFilterProcessesUrl();
  38. // SystemSpringSocialConfigurer systemSpringSocialConfigurer= new SystemSpringSocialConfigurer(filterProcessesUrl);
  39. // return systemSpringSocialConfigurer;
  40. return new SpringSocialConfigurer();
  41. }
  42. }

测试

此时若是在QQ互联上定义回调地址为 /auth/qq,应该是没有问题的了。但是一般我们会自定义拦截路径,此时应该新建SpringSocialConfigurer的实现类,重写 SocialAuthenticationFilter中的拦截路径
SystemSpringSocialConfigurer.java

  1. public class SystemSpringSocialConfigurer extends SpringSocialConfigurer {
  2. private String filterProcessesUrl;
  3. public SystemSpringSocialConfigurer(String filterProcessesUrl) {
  4. this.filterProcessesUrl = filterProcessesUrl;
  5. }
  6. /**
  7. *
  8. * @param object
  9. * 放到过滤器链上的filter
  10. * @param <T>
  11. * @return
  12. */
  13. @SuppressWarnings("unchecked")
  14. @Override
  15. protected <T> T postProcess(T object) {
  16. SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
  17. filter.setFilterProcessesUrl(filterProcessesUrl);
  18. return (T) filter;
  19. }
  20. }

实现登录后自动注册

  1. /**
  2. * 第三方登录时,自动注册用户信息
  3. */
  4. @Slf4j
  5. @Component
  6. public class SystemConnectionSignUp implements ConnectionSignUp {
  7. @Override
  8. public String execute(Connection<?> connection) {
  9. log.info("【SystemConnectionSignUp】 自动注册用户");
  10. OAuth2Connection oAuth2Connection=(OAuth2Connection)connection;
  11. ConnectionKey key = oAuth2Connection.getKey();
  12. log.info("key={}",key);
  13. //若是返回为null,则不会自动注册用户
  14. return oAuth2Connection.getDisplayName();
  15. }
  16. }

项目地址

https://github.com/luoqiz/faster.git