欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

认证、授权攻略二、spring social第三方微信登录

程序员文章站 2022-05-05 15:16:56
...

讲解第三方微信登录(OAuth2.0授权码模式)
注意:需要到微信开放平台注册,而不是微信公众平台!!!
认证、授权攻略二、spring social第三方微信登录

spring social就是将OAuth2.0整个流程封装起来并且去进行实现。
原理:它把整个流程封装到了SocialAuthenticationFilter的过滤器中然后把这个过滤器加入到了SpringSecurity的过滤器链上。当访问请求的时候,SocialAuthenticationFilter会将请求拦截下来然后将整个流程走完。进而去实现第三方登录。
认证、授权攻略二、spring social第三方微信登录

spring social将整个流程封装到具体的接口和类
认证、授权攻略二、spring social第三方微信登录
服务提供商相关
在整个流程上面,从第一步到第六步,都是需要跟服务提供商打交道的。所以它的第一个接口叫–ServiceProvider,它实际上就是服务提供商的一个抽象,针对每一个服务提供商(QQ、微信),都需要一个ServiceProvider接口的一个实现。SpringSocial提供了一个AbstractOauth2ServiceProvider抽象类,实现了一些共有的东西,如果要自己写,只需要继承这个类实现其中的公有方法即可。

整个流程又可以具体分为:

  1. 第一步到第五步发放令牌其实是一个标准的流程。
  2. 到了第六步(获取用户信息)的时候其实就是一个个性化的实现,因为每一个服务提供商返回的用户信息的数据结构定义都不一样。

针对1和2,SpringSocial提供了两个接口:

  1. Oauth2Operation(封装第一步到第五步)。Spring提供了一个默认的实现叫Oauth2Template,这个类会帮助我们去完成Oauth协议的执行流程。
  2. Api(个性化第六步),实际上没有一个明确的接口,因为每一个服务提供商对于用 户基本信息的调用都是有区别的。SpringSocial其实也提供了一个抽象类叫AbstractOauth2ApiBinding帮助我们快速开发第六步的实现。

第三方应用内部
到了第七步实际上就跟服务提供商没有任何关系了。都是在第三方应用Client内部去完成的。

第一个接口是Connection,SpringSocial提供的实现类叫OAuth2Connection。其作用是封装前六步执行完毕之后获取到的用户信息。Connection是由ConnectionFactory创建出来的,用到的类叫OAuth2ConnectionFactory。它的作用就是为了创建Connection对象获取用户信息,但是用户信息是在ServiceProvider中去构建的。所以在OAuth2ConnectionFactory中肯定有一个ServiceProvider实例,将ServiceProvider封装起来放到Connection中去。
注意:Connection的对象名和字段名都是固定的

之前说过,每一个服务提供商对于用户信息的定义的数据结构都是不一样的,那么ConnectionFactory是如何做到将这些不用数据结构的信息转化成对象名和字段名都是固定的Connection的对象的呢?
ConnectionFactory中有一个ApiAdapter,将不同格式的用户信息转化为固定格式的Connection对象就是由ApiAdapter接口的实现来完成。转化成功之后就将Connection中封装进去一个用户信息。

拿到用户信息,流程完成之后
我们拿到了服务提供商返回给的用户信息之后,是需要将这个用户信息同步到我们自定义的数据库中去的。那么如何去实现将传过来的用户信息与我们系统中已经保存的用户信息去进行对应的呢?实际上这个对应关系是存在数据库中的。数据库中有一个用户对应表,里面有自己定义的userId以及服务提供商那边对应的用户信息的唯一标识。
那么由谁来操纵这个表呢?就是由UsersConnectionRepository存储器去实现的。在代码中用到的实现类叫做JdbcUsersConnectionRepository,这个类的作用就是去数据库中针对用户对应关系表去做一些增删改查的操作。

可以多学习学习spring social官方文档:
https://docs.spring.io/spring-social/docs/current-SNAPSHOT/reference/htmlsingle/#introduction

代码实战
1、pom.xml

	<!-- spring boot spring-security -->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-security</artifactId>
	</dependency>
	<!-- spring-social -->
	<dependency>
	    <groupId>org.springframework.social</groupId>
	    <artifactId>spring-social-security</artifactId>
	    <version>1.1.6.RELEASE</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.social</groupId>
		<artifactId>spring-social-config</artifactId>
		<version>1.1.6.RELEASE</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.social</groupId>
		<artifactId>spring-social-core</artifactId>
		<version>1.1.6.RELEASE</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.social</groupId>
		<artifactId>spring-social-web</artifactId>
		<version>1.1.6.RELEASE</version>
	</dependency>

2、api

package com.javasgj.springboot.weixin.api;

import com.javasgj.springboot.weixin.entity.WeixinUserInfo;

public interface Weixin {

	/**
	 * 获取微信用户信息
	 * @param openId
	 * @return
	 */
	public WeixinUserInfo getUserInfo(String openId);
}

WeixinImpl :

package com.javasgj.springboot.weixin.api.impl;

import java.nio.charset.Charset;
import java.util.List;

import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;

import com.alibaba.fastjson.JSON;
import com.javasgj.springboot.weixin.api.Weixin;
import com.javasgj.springboot.weixin.entity.WeixinUserInfo;

public class WeixinImpl extends AbstractOAuth2ApiBinding implements Weixin {

    /**
     * 获取用户信息url
     */
    private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";

    /**
     * WeixinImpl构造器
     */
    public WeixinImpl(String accessToken){
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
    }

    /**
     * 默认注册的StringHttpMessageConverter字符集为ISO-8859-1,而微信返回的是UTF-8,因此必须覆盖原来的方法
     */
    @Override
    protected List<HttpMessageConverter<?>> getMessageConverters() {
    	
        List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
        messageConverters.remove(0);    // 删除StringHttpMessageConverter
        messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return messageConverters;
    }

    /**
     * 获取微信用户信息
     */
    @Override
    public WeixinUserInfo getUserInfo(String openId) {
    	
        String url = URL_GET_USER_INFO + openId;
        String response = getRestTemplate().getForObject(url, String.class);
        
        if(response.contains("errcode")){   // 如果响应中存在错误码则返回null
            return null;
        }
        
        WeixinUserInfo userInfo = JSON.toJavaObject(JSON.parseObject(response), WeixinUserInfo.class);
        return userInfo;
    }
}

3、connect
WeixinOAuth2Template:

package com.javasgj.springboot.weixin.connect;

import java.nio.charset.Charset;
import java.util.Map;

import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Parameters;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.alibaba.fastjson.JSON;

/**
 * 完成微信的OAuth2认证流程的模板类
 * 国内厂商实现的OAuth2方式不同, Spring默认提供的OAuth2Template无法完整适配,只能针对每个厂商调整
 */
public class WeixinOAuth2Template extends OAuth2Template {

    private String clientId;

    private String clientSecret;

    private String accessTokenUrl;

    private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";

    public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
    	
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true);  //请求中添加client_id和client_secret参数
        
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.accessTokenUrl = accessTokenUrl;
    }

    /**
     * 微信变更了OAuth请求参数的名称,我们覆写相应的方法按照微信的文档改为微信请求参数的名字。
     * @param authorizationCode
     * @param redirectUri
     * @param parameters
     * @return
     */
    @Override
    public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap<String, String> parameters) {

        StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl);
        accessTokenRequestUrl.append("?appid=" + clientId);
        accessTokenRequestUrl.append("&secret=" + clientSecret);
        accessTokenRequestUrl.append("&code=" + authorizationCode);
        accessTokenRequestUrl.append("&grant_type=authorization_code");
        accessTokenRequestUrl.append("&redirect_uri=" + redirectUri);

        return getAccessToken(accessTokenRequestUrl);
    }

    /**
     * 微信变更了OAuth请求参数的名称,我们覆写相应的方法按照微信的文档改为微信请求参数的名字。
     * @param refreshToken
     * @param additionalParameters
     * @return
     */
    public AccessGrant refreshAccess(String refreshToken, MultiValueMap<String, String> additionalParameters) {

        StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL);
        refreshTokenUrl.append("?appid=" + clientId);
        refreshTokenUrl.append("&grant_type=refresh_token");
        refreshTokenUrl.append("&refresh_token=" + refreshToken);

        return getAccessToken(refreshTokenUrl);
    }

    /**
     * 获取微信access_token信息
     * @param accessTokenRequestUrl
     * @return
     */
    @SuppressWarnings("unchecked")
    private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {

        // logger.info("获取access_token, 请求URL: "+accessTokenRequestUrl.toString());
        String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);
        // logger.info("获取access_token, 响应内容: "+response);

        Map<String, Object> result = JSON.toJavaObject(JSON.parseObject(response), Map.class);
        
        // 返回错误码时直接抛出异常
        if(result.containsKey("errcode")){
            String errcode = (String) result.get("errcode");
            String errmsg = (String) result.get("errmsg");
            throw new RuntimeException("获取access token失败, errcode:" + errcode + ", errmsg:" + errmsg);
        }

        WeixinAccessGrant accessToken = new WeixinAccessGrant(
        		(String) result.get("access_token"),
        		(String) result.get("scope"),
        		(String) result.get("refresh_token"),
        		(Long) result.get("expires_in"));
        // 设置openid
        accessToken.setOpenId((String) result.get("openid"));

        return accessToken;
    }

    /**
     * 构建获取授权码的请求。也就是引导用户跳转到微信的地址。
     */
    public String buildAuthenticateUrl(OAuth2Parameters parameters) {
    	
        String url = super.buildAuthenticateUrl(parameters);
        url = url + "&appid=" + clientId + "&scope=snsapi_login";
        return url;
    }

    public String buildAuthorizeUrl(OAuth2Parameters parameters) {
    	
        return buildAuthenticateUrl(parameters);
    }

    /**
     * 微信返回的contentType是html/text,添加相应的HttpMessageConverter来处理。
     */
    protected RestTemplate createRestTemplate() {
    	
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }
}

WeixinServiceProvider:

package com.javasgj.springboot.weixin.connect;

import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;

import com.javasgj.springboot.weixin.api.Weixin;
import com.javasgj.springboot.weixin.api.impl.WeixinImpl;

/**
 * 微信OAuth2流程处理器的提供器,供spring social的connect体系调用
 */
public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {
	
    /**
     * 微信获取授权码的url
     */
    private static final String URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
    
    /**
     * 微信获取accessToken令牌的url
     */
    private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";

    /**
     * 注入WeixinOAuth2Template微信模板类
     */
    public WeixinServiceProvider(String clientId, String clientSecret) {
    	
        super(new WeixinOAuth2Template(clientId, clientSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
    }

    /**
     * 获取微信个性化用户信息接口
     * @param accessToken
     * @return
     */
    @Override
    public Weixin getApi(String accessToken) {
    	
        return new WeixinImpl(accessToken);
    }
}

WeixinAdapter:

package com.javasgj.springboot.weixin.connect;

import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;

import com.javasgj.springboot.weixin.api.Weixin;
import com.javasgj.springboot.weixin.entity.WeixinUserInfo;

/**
 * Connection的一个角色是为所有服务提供者应用的链接用户帐户提供通用抽象
 * 微信api适配器,将从微信api拿到的用户数据模型转换为Spring Social的标准用户数据模型
 */
public class WeixinAdapter implements ApiAdapter<Weixin> {
	
    private String openId;

    public WeixinAdapter() {}

    public WeixinAdapter(String openId) {
    	
        this.openId = openId;
    }

    @Override
    public boolean test(Weixin weixin) {
    	
        return true;
    }

    /**
     * 将微信的用户信息映射到ConnectionValues标准的数据化结构上
     */
    @Override
    public void setConnectionValues(Weixin weixin, ConnectionValues values) {
    	
        WeixinUserInfo profile = weixin.getUserInfo(openId);
        values.setProviderUserId(profile.getOpenid());
        values.setDisplayName(profile.getNickname());
        values.setImageUrl(profile.getHeadimgurl());
    }

    @Override
    public UserProfile fetchUserProfile(Weixin api) {
    	
        return null;
    }

    @Override
    public void updateStatus(Weixin api, String message) {
        //do nothing
    }
}

WeixinConnectionFactory:

package com.javasgj.springboot.weixin.connect;

import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.support.OAuth2Connection;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2ServiceProvider;

import com.javasgj.springboot.weixin.api.Weixin;

/**
 * 微信连接工厂
 */
public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> {

	/**
	 * 初始化微信连接工厂
	 */
    public WeixinConnectionFactory(String providerId, String clientId, String clientSecret) {
    	
        super(providerId, new WeixinServiceProvider(clientId, clientSecret), new WeixinAdapter());
    }

    /**
     * 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取
     */
    @Override
    protected String extractProviderUserId(AccessGrant accessGrant) {
    	
        if(accessGrant instanceof WeixinAccessGrant) {
            return ((WeixinAccessGrant) accessGrant).getOpenId();
        }
        return null;
    }

    /**
     * WeixinAdapter需要openid
     * 创建connection
     */
    public Connection<Weixin> createConnection(AccessGrant accessGrant) {
    	
        return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
                accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));
    }

    /**
     * WeixinAdapter需要openid
     * 创建connection
     */
    public Connection<Weixin> createConnection(ConnectionData data) {
    	
        return new OAuth2Connection<Weixin>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
    }

    private ApiAdapter<Weixin> getApiAdapter(String providerUserId) {
    	
        return new WeixinAdapter(providerUserId);
    }

    private OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() {
    	
        return (OAuth2ServiceProvider<Weixin>) getServiceProvider();
    }
}

WeixinAccessGrant:

package com.javasgj.springboot.weixin.connect;

import org.springframework.social.oauth2.AccessGrant;

/**
 * 对微信access_token信息的封装
 * 与标准的OAuth2协议不同,微信在获取access_token时会同时返回openId,并没有单独的通过accessToke换取openId的服务
 * 在此处继承标准AccessGrant(Spring提供的令牌封装类),添加openId字段
 */
public class WeixinAccessGrant extends AccessGrant {

   private String openId;

   public WeixinAccessGrant(String accessToken) {
       super(accessToken);
   }
   
   public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
       super(accessToken, scope, refreshToken, expiresIn);
   }

   public String getOpenId() {
       return openId;
   }

   public void setOpenId(String openId) {
       this.openId = openId;
   }
}

WeixinUserInfo:

package com.javasgj.springboot.weixin.entity;

/**
 * 微信用户实体类
 */
public class WeixinUserInfo {

	/**
	 * 普通用户的标识,对当前开发者账号唯一
	 */
	private String openid;
	
	/**
	 * 普通用户昵称
	 */
	private String nickname;
  
    /**
     * 语言
     */
	private String language;
  
	/**
	 * 普通用户性别:1为男性,2为女性
	 */
	private String sex;
	
	/**
	 * 普通用户个人资料填写的省份
	 */
	private String province;
	
	/**
	 * 普通用户个人资料填写的城市
	 */
	private String city;
	
	/**
	 * 国家,如中国为CN
	 */
	private String country;
	
	/**
	 * 用户头像,最后一个数值代表正方形头像大小(有0,46,64,96,132数值可选,0代表640*640正方形)
	 */
	private String headimgurl;
	
	/**
	 * 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
	 */
	private String[] privilege;
	
	/**
	 * 用户统一标识,针对一个微信开放平台账号下的应用,同一用户的unionid是唯一的。
	 */
	private String unionid;

	
	public String getOpenid() {
		return openid;
	}

	public void setOpenid(String openid) {
		this.openid = openid;
	}

	public String getNickname() {
		return nickname;
	}

	public void setNickname(String nickname) {
		this.nickname = nickname;
	}

	public String getLanguage() {
		return language;
	}

	public void setLanguage(String language) {
		this.language = language;
	}

	public String getSex() {
		return sex;
	}

	public void setSex(String sex) {
		this.sex = sex;
	}

	public String getProvince() {
		return province;
	}

	public void setProvince(String province) {
		this.province = province;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getCountry() {
		return country;
	}

	public void setCountry(String country) {
		this.country = country;
	}

	public String getHeadimgurl() {
		return headimgurl;
	}

	public void setHeadimgurl(String headimgurl) {
		this.headimgurl = headimgurl;
	}

	public String[] getPrivilege() {
		return privilege;
	}

	public void setPrivilege(String[] privilege) {
		this.privilege = privilege;
	}

	public String getUnionid() {
		return unionid;
	}

	public void setUnionid(String unionid) {
		this.unionid = unionid;
	}
}

4、config
WeixinSpringSocialConfig:

package com.javasgj.springboot.weixin.config;

import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;

public class WeixinSpringSocialConfig extends SpringSocialConfigurer {

	@Override
	protected <T> T postProcess(T object) {
		
		SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
		filter.setFilterProcessesUrl("/user/auth");		// SocialAuthenticationFilter默认拦截/auth/{providerId}
		return (T) filter;
	}
}

WeixinAutoConfiguration:

package com.javasgj.springboot.weixin.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;

import com.javasgj.springboot.weixin.connect.WeixinConnectionFactory;

/**
 * 微信登录配置
 * @EnableSocial:开启spring social的相关功能
 */
@Configuration
@EnableSocial
public class WeixinAutoConfiguration extends SocialConfigurerAdapter {

	@Autowired
	private DataSource dataSource;
	
    public ConnectionFactory<?> createConnectionFactory() {
    	
        String providerId = "weixin";   //第三方id,用来决定发起第三方登录的url,SocialAuthenticationFilter默认拦截/auth/{providerId}
        String clientId = "";
        String clientSecret = "";
        return new WeixinConnectionFactory(providerId, clientId, clientSecret);
    }

	@Override
	public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {

		connectionFactoryConfigurer.addConnectionFactory(createConnectionFactory());
	}
	
	/**
	 * 操作数据库
	 * 默认表结构:
	 * create table UserConnection (userId varchar(255) not null,
		providerId varchar(255) not null,
		providerUserId varchar(255),
		rank int not null,
		displayName varchar(255),
		profileUrl varchar(512),
		imageUrl varchar(512),
		accessToken varchar(512) not null,
		secret varchar(512),
		refreshToken varchar(512),
		expireTime bigint,
		primary key (userId, providerId, providerUserId));
		create unique index UserConnectionRank on UserConnection(userId, providerId, rank);
	 */
	@Override
	public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
		
		JdbcUsersConnectionRepository jdbcUsersConnectionRepository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
		jdbcUsersConnectionRepository.setTablePrefix("t_");
        return jdbcUsersConnectionRepository;
	}

	/**
	 * 声明后还需要加在SpringSecurity过滤器链上,在SpringSecurity Config方法上添加过滤器链
	 * protected void configure(HttpSecurity http) throws Exception {
	 * 		http.apply(new WeixinSpringSocialConfig());
	 * }
	 */
	@Bean
	public WeixinSpringSocialConfig weixinSpringSocialConfig(){
		
		WeixinSpringSocialConfig config = new WeixinSpringSocialConfig();
		// 认证失败跳转注册页面
		// config.signupUrl(registerUrl);
        // 认证成功跳转后处理器,跳转带token的成功页面
		// config.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
        return config;
	}
}