Welcome to your personal blog. This post is a follow-up to “Developing Enterprise-level Authentication and Authorization for Spring Security Technology Stack (2)”

Develop QQ login function

Preparations: Apply for appId and appSecret. For details, see _oAUTH2-0

The callback domain: www.zhenganwen.top/socialLogin…

To develop a third-party access function is actually to implement the above set of components one by one. In this section, we will develop the QQ login function, starting from the left half of the above figure.

ServiceProvider

Api, declare a method corresponding to OpenAPI that calls the Api and returns the response result as a POJO, corresponding to step 7 in the authorization code pattern sequence diagram

package top.zhenganwen.security.core.social.qq.api;

import top.zhenganwen.security.core.social.qq.QQUserInfo;

/ * * *@author zhenganwen
 * @date 2019/9/4
 * @descQQApi encapsulates the call to QQ open platform interface */
public interface QQApi {

    QQUserInfo getUserInfo(a);
}

Copy the code
package top.zhenganwen.security.core.social.qq.api;

import lombok.Data;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
import top.zhenganwen.security.core.social.qq.QQUserInfo;

/ * * *@author zhenganwen
 * @date 2019/9/3
 * @descQQApiImpl calls open interface with token to obtain user information * 1. First of all, according to https://graph.qq.com/oauth2.0/me/ {token} get the user id = > {on social platforms@code2. Call QQ OpenAPI https://graph.qq.com/user/get_user_info?access_token=YOUR_ACCESS_TOKEN&oauth_consumer_key=YOUR_APP_ID&openid=YOUR_OPENID * Access user information on social platforms => {@link QQApiImpl#getUserInfo()}
 * <p>
 * {@linkAbstractOAuth2ApiBinding} * helps us complete the process of calling OpenAPI with {AbstractOAuth2ApiBinding}@codeToken} parameter, see its member variable {@codeAccessToken} * helps us complete the HTTP call. See its member variable {@codeRestTemplate} * <p> * Note: this component should be multi-example, because each user should have a different OpenAPI, each time a different user QQ joint login should create a new {@link QQApiImpl}
 */
@Data
public class QQApiImpl extends AbstractOAuth2ApiBinding implements QQApi {

    private static final String URL_TO_GET_OPEN_ID = "https://graph.qq.com/oauth2.0/me?access_token=%s";

    // The parent class will help us with the token argument, so the URL ignores the token argument
    private static final String URL_TO_GET_USER_INFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";

    private String openId;

    private String appId;

    private Logger logger = LoggerFactory.getLogger(getClass());

    public QQApiImpl(String accessToken,String appId) {
        // When calling OpenAPI, attach the parameters to be passed to the URL path
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
        this.appId = appId;

        Callback ({"client_id":"YOUR_APPID"," openId ":"YOUR_OPENID"});
        String responseForGetOpenId = getRestTemplate().getForObject(String.format(URL_TO_GET_OPEN_ID, accessToken), String.class);
        logger.info("OpenId {}", responseForGetOpenId);

        this.openId = StringUtils.substringBetween(responseForGetOpenId, "\"openid\":\""."\"}");
    }

    @Override
    public QQUserInfo getUserInfo(a) {
        QQUserInfo qqUserInfo = getRestTemplate().getForObject(String.format(URL_TO_GET_USER_INFO, appId, openId), QQUserInfo.class);
        logger.info(Call QQ OpenAPI to obtain user information: {}", qqUserInfo);
        returnqqUserInfo; }}Copy the code

Then OAuth2Operations is used to encapsulate steps 2 to 6 in the sequence diagram of the authorization code mode, which is used to import the user to the authorization page, obtain the authorization code passed in after the user is authorized, and obtain the token for accessing OpenAPI. Since these steps are fixed, Spring Social does a strong wrapper for us, the OAuth2Template, so we don’t need to implement it ourselves, we can use this component directly later

ServiceProvider integrates OAuth2Operations to obtain tokens and APIS to invoke OpenAPI with tokens to obtain user information

package top.zhenganwen.security.core.social.qq.connect;

import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2Operations;
import top.zhenganwen.security.core.social.qq.api.QQApiImpl;

/ * * *@author zhenganwen
 * @date 2019/9/4
 * @descQQServiceProvider connects with service providers and encapsulates a complete set of authorization login process. From the user clicking the third-party login button to accessing the Connection(user information) * delegate {@linkOAuth2Operations} and {@linkOrg. Springframework. Social. Oauth2. * / AbstractOAuth2ApiBinding} to complete the whole process
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQApiImpl> {

    /** * The application ID registered with the service provider */
    private String appId;

    / * * *@paramOauth2Operations Indicates the encapsulation logic, including switching to the authentication server, user authorization, obtaining the authorization code, and obtaining the token *@paramAppId appId */ of the current application
    public QQServiceProvider(OAuth2Operations oauth2Operations, String appId) {
        super(oauth2Operations);
        this.appId = appId;
    }

    @Override
    public QQApiImpl getApi(String accessToken) {
        return newQQApiImpl(accessToken,appId); }}Copy the code

ConnectionFactory

UserInfo encapsulates the user information returned by OpenAPI

package top.zhenganwen.security.core.social.qq;

import lombok.Data;

import java.io.Serializable;

/ * * *@author zhenganwen
 * @date 2019/9/4
 * @descQQUserInfo User registration information in THE QQ application */
@Data
public class QQUserInfo implements Serializable {
    /** * return code */
    private String ret;
    /** * if ret<0, an error message will be displayed, and all returned data will be encoded in UTF-8. * /
    private String msg;
    / * * * * /
    private String openId;
    /** * I don't know what it is, but it is in the actual API return. * /
    private String is_lost;
    /** ** ** ** ** /
    private String province;
    /** ** ** ** */
    private String city;
    /** * date of birth */
    private String year;
    /** * The user's nickname in qzone. * /
    private String nickname;
    /** * the QQ space avatar URL with a size of 30×30 pixels. * /
    private String figureurl;
    /** * the size of 50×50 pixels QQ space avatar URL. * /
    private String figureurl_1;
    /** * the size of 100×100 pixels QQ space avatar URL. * /
    private String figureurl_2;
    /** * the size of 40×40 pixels QQ avatar URL. * /
    private String figureurl_qq_1;
    /** * QQ avatar URL with size of 100×100 pixels. It should be noted that not all users will have QQ's 100×100 avatar, but 40×40 pixel avatar will definitely exist. * /
    private String figureurl_qq_2;
    /** * Gender. If not, male */ is returned by default
    private String gender;
    /** * Indicates whether the user is a yellow diamond user (0: no; 1: Yes). * /
    private String is_yellow_vip;
    /** * Indicates whether the user is a yellow diamond user (0: no; 1: Yes) */
    private String vip;
    /** * Yellow diamond grade */
    private String yellow_vip_level;
    /** * Yellow diamond grade */
    private String level;
    /** * whether it is an annual fee yellow diamond user (0: no; 1: Yes) */
    private String is_yellow_year_vip;
}
Copy the code

ApiAdapter, which converts different user information data formats returned by different third-party applications into a unified user view

package top.zhenganwen.security.core.social.qq.connect;

import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;
import org.springframework.stereotype.Component;
import top.zhenganwen.security.core.social.qq.QQUserInfo;
import top.zhenganwen.security.core.social.qq.api.QQApiImpl;

/ * * *@author zhenganwen
 * @date 2019/9/4
 * @descQQConnectionAdapter returns different user information from different third-party applications to unified user view {@linkOrg. Springframework. Social. Connect. Connection} adapter * /
@Component
public class QQConnectionAdapter implements ApiAdapter<QQApiImpl> {

    // Test whether OpenAPI interface is available
    @Override
    public boolean test(QQApiImpl api) {
        return true;
    }

    /** * call OpenAPI to get user information and adapt it to {@linkOrg. Springframework. Social. Connect. Connection} * note: not all social applications due to {@linkOrg. Springframework. Social. Connect. Connection} the properties, such as QQ does not have a personal home page * like weibo@param api
     * @param values
     */
    @Override
    public void setConnectionValues(QQApiImpl api, ConnectionValues values) {
        QQUserInfo userInfo = api.getUserInfo();
        // User name
        values.setDisplayName(userInfo.getNickname());
        // User profile picture
        values.setImageUrl(userInfo.getFigureurl_2());
        // User profile page
        values.setProfileUrl(null);
        // User id on the social platform
        values.setProviderUserId(userInfo.getOpenId());
    }

    // This method is similar to setConnectionValues
    @Override
    public UserProfile fetchUserProfile(QQApiImpl api) {
        return null;
    }

    /** * Call OpenAPI to update user dynamic * Since QQ OpenAPI does not have this function, so do not care (if you connect to Weibo, you may need to rewrite this method) *@param api
     * @param message
     */
    @Override
    public void updateStatus(QQApiImpl api, String message) {}}Copy the code

ConnectionFactory

package top.zhenganwen.security.core.social.qq.connect;

import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth2.OAuth2ServiceProvider;
import top.zhenganwen.security.core.social.qq.api.QQApiImpl;

public class QQConnectionFactory extends OAuth2ConnectionFactory<QQApiImpl> {

    public QQConnectionFactory(String providerId,OAuth2ServiceProvider<QQApiImpl> serviceProvider, ApiAdapter<QQApiImpl> apiAdapter) {
        super(providerId, serviceProvider, apiAdapter); }}Copy the code

createConnectionFactory

We need to rewrite the createConnectionFactory SocialAutoConfigurerAdapter method into our custom ConnectionFacory SpringSoical will use it to complete the authorization code mode of step 2 ~ 7

package top.zhenganwen.security.core.social.qq.connect;

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.Bean;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.oauth2.OAuth2Operations;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.stereotype.Component;
import top.zhenganwen.security.core.properties.SecurityProperties;

@Component
@ConditionalOnProperty(prefix = "demo.security.qq",name = "appId")
public class QQLoginAutoConfig extends SocialAutoConfigurerAdapter {

    public static final String URL_TO_GET_AUTHORIZATION_CODE = "https://graph.qq.com/oauth2.0/authorize";

    public static final String URL_TO_GET_TOKEN = "https://graph.qq.com/oauth2.0/token";

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private QQConnectionAdapter qqConnectionAdapter;

    @Override
    protectedConnectionFactory<? > createConnectionFactory() {return new QQConnectionFactory(
                securityProperties.getQq().getProviderId(),
                new QQServiceProvider(oAuth2Operations(), securityProperties.getQq().getAppId()), 
                qqConnectionAdapter);
    }

    @Bean
    public OAuth2Operations oAuth2Operations(a) {
        return newOAuth2Template( securityProperties.getQq().getAppId(), securityProperties.getQq().getAppSecret(), URL_TO_GET_AUTHORIZATION_CODE, URL_TO_GET_TOKEN); }}Copy the code

QQSecurityProperties, QQ login related configuration items

package top.zhenganwen.security.core.social.qq.connect;

import lombok.Data;

@Data
public class QQSecurityPropertie {
    private String appId;
    private String appSecret;
    private String providerId = "qq";
}
Copy the code
package top.zhenganwen.security.core.properties;

@Data
@ConfigurationProperties(prefix = "demo.security")
public class SecurityProperties {
    private BrowserProperties browser = new BrowserProperties();
    private VerifyCodeProperties code = new VerifyCodeProperties();
    private QQSecurityPropertie qq = new QQSecurityPropertie();
}
Copy the code

UsersConnectionRepository

We need a list to maintain the current system user table with the user information registered in third party applications, the corresponding relationship between are giving us the table (in JdbcUsersConnectionRepository. Java file the same directory)

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);
Copy the code

UserId is the unique identifier of the current system user (not necessarily the primary key of the user table, but also the user name, as long as it is the field that uniquely identifies the user in the user table), providerId is used to identify the third-party application, providerUserId is the userId of the user in the third-party application. These three fields identify the providerId user (providerUserId) that corresponds to the user (userId) on the current system. We execute the following SQL on the Datasource’s corresponding database.

Are giving us JdbcUsersConnectionRepository as the form of DAO, we need to inject the current system of data source to it, Inheriting the SocialConfigurerAdapter and adding @enablesocial to enable some automated configuration of SpringSocial

package top.zhenganwen.security.core.social.qq;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.security.SpringSocialConfigurer;

import javax.sql.DataSource;

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Bean
	  @Primary	/ / the superclass will default to InMemoryUsersConnectionRepository as implementation, we need to use @ told container using only our Primary
    @Override
    public JdbcUsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        // The token can be encrypted with the third argument
        return newJdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText()); }}Copy the code

SocialAuthenticationFilter

However, the procedure for using a third-party login is the same as that for user name and password authentication. But the latter is based on the user name entered by the user table to find the user; The former is to obtain the providerUserId of the user in the third-party application through the OAtuh process first, then query the corresponding userId in the UserConnection table according to the providerId and providerUserId, and finally query the user in the user table according to the userId

So we also need to enable SocialAuthenticationFilter:

package top.zhenganwen.security.core.social.qq;

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    public JdbcUsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        // The token can be encrypted with the third argument
        return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
    }

    / / the bean is a login configuration class, and we have written before SmsLoginConfig and VerifyCodeValidatorConfig
	  / / function is the same, except that it is a SocialAuthenticationFilter to increase in the filter chain
    @Bean
    public SpringSocialConfigurer springSocialConfigurer(a) {
        return newSpringSocialConfigurer(); }}Copy the code

SecurityBrowserConfig

  @Override
    protected void configure(HttpSecurity http) throws Exception {

        // Enable the verification filter
        http.apply(verifyCodeValidatorConfig);
        // Enable the SMS login filter
        http.apply(smsLoginConfig);
        / / enable the QQ login (adding SocialAuthenticationFilter to Security in the filter chain)http.apply(springSocialConfigurer); .Copy the code

appId & appSecret & providerId

Since the appId and appSecret requested by each system are different, we extract them into the configuration file

Demo. Security. Qq. The appId = YOUR_APP_ID # replace your appId demo. Security. Qq. The appSecret = appSecret YOUR_APP_SECRET # replace you demo.security.qq.providerId=qqCopy the code

URL setting rules for joint login

We need to provide a QQ joint login link on the login page, and the request is /auth/ QQ

<a href="/auth/qq">Qq login</a>
Copy the code

The first path/auth is should be SocialAuthenticationFilter intercept/auth beginning requests by default

SocialAuthenticationFilter

private static final String DEFAULT_FILTER_PROCESSES_URL = "/auth";
Copy the code

The second path needs to be consistent with providerId and we configure demo. Security. Qq. The provider – id for qq

SocialAuthenticationFilter

@Deprecated
	protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
		String providerId = getRequestedProviderId(request);
		if(providerId ! =null){
			Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
			return authProviders.contains(providerId);
		}
		return false;
	}
Copy the code

The URL of the federated login must be the same as that of the callback field

Now we’ve implemented the various components of SpringSocial, but if we can string them together to make the whole process work, we can try it out and understand the Social authentication process as we work through the bugs

Visit /login. HTML and click qq to login and the response is as follows

Prompt that the callback address is illegal, we can look at the redirect_URL parameter in the address bar

After transcoding is http://localhost:8080/auth/qq, that is to say, if the user agrees to authorize the browser on the joint will be redirected to the login URL.

And I in the QQ in the Internet application callback domain is www.zhenganwen.top/socialLogin/qq (pictured), after the QQ login joint requires the user to authorize redirect to URL must be filled in and apply for appId callback domain, That is, the URL of the syndication login on the page must be the same as the callback field.

First, the domain name and port must be consistent:

Since it is a local server, we need to modify the local hosts file to enable the browser to parse www.zhenganwen.top to 172.0.0.1:

127.0.0.1 www.zhenganwen.top
Copy the code

And change the service port to 80

server.port=80
Copy the code

This domain name, and port can the corresponding, through access to the login page at www.zhenganwen.top/login.html.

Second, joint login URI and we also need to be on the corresponding set of callback domain/auth/socialLogin instead, need to customize SocialAuthenticationFilter filterProcessesUrl attribute values:

New SocialProperties

package top.zhenganwen.security.core.properties;

import lombok.Data;
import top.zhenganwen.security.core.social.qq.connect.QQSecurityPropertie;

@Data
public class SocialProperties {
    public static final String DEFAULT_FILTER_PROCESSING_URL = "/socialLogin";
    private QQSecurityPropertie qq = new QQSecurityPropertie();
    private String filterProcessingUrl = DEFAULT_FILTER_PROCESSING_URL;
}
Copy the code

Modify SecurityProperties

@Data
@ConfigurationProperties(prefix = "demo.security")
public class SecurityProperties {
    private BrowserProperties browser = new BrowserProperties();
    private VerifyCodeProperties code = new VerifyCodeProperties();
	  // private QQSecurityPropertie qq = new QQSecurityPropertie();                  
    private SocialProperties social = new SocialProperties();
}
Copy the code

Application. Properties:

#demo.security.qq.appId=***
#demo.security.qq.appSecret=***
#demo.security.qq.providerId=qq
demo.security.social.qq.appId=***
demo.security.social.qq.appSecret=***
demo.security.social.qq.providerId=qq
Copy the code

QQLoginAutoConfig is synchronized

@Component
//@ConditionalOnProperty(prefix = "demo.security.qq",name = "appId")
@ConditionalOnProperty(prefix = "demo.security.social.qq",name = "appId")
public class QQLoginAutoConfig extends SocialAutoConfigurerAdapter {
Copy the code

PostProcess extension SpringSocialConfigurer, through the hook function to achieve SocialAuthenticationFilter some custom configuration, such as filterProcessingUrl

package top.zhenganwen.security.core.social.qq.connect;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
import top.zhenganwen.security.core.properties.SecurityProperties;

public class QQSpringSocialConfigurer extends SpringSocialConfigurer {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) object;
        filter.setFilterProcessesUrl(securityProperties.getSocial().getFilterProcessingUrl());
        return(T) filter; }}Copy the code

SpringSocialConfigurer with an extension injected in SocialConfig

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    public JdbcUsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        // The token can be encrypted with the third argument
        return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
    }

// @Bean
// public SpringSocialConfigurer springSocialConfigurer() {
// return new SpringSocialConfigurer();
/ /}
                    
    @Bean
    public SpringSocialConfigurer qqSpringSocialConfigurer(a) {
        QQSpringSocialConfigurer qqSpringSocialConfigurer = new QQSpringSocialConfigurer();
        returnqqSpringSocialConfigurer; }}Copy the code

The reason for this is postProcess () is a hook function, in SecurityConfigurerAdapter config method, in adding SocialAuthenticationFilter to filter chain will call postProcess, Allow for SocialAuthenticationFilter subclasses override this method to perform some custom configuration:

public class SpringSocialConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
	@Override
	public void configure(HttpSecurity http) throws Exception {		
		ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
		UsersConnectionRepository usersConnectionRepository = getDependency(applicationContext, UsersConnectionRepository.class);
		SocialAuthenticationServiceLocator authServiceLocator = getDependency(applicationContext, SocialAuthenticationServiceLocator.class);
		SocialUserDetailsService socialUsersDetailsService = getDependency(applicationContext, SocialUserDetailsService.class);
		
		SocialAuthenticationFilter filter = newSocialAuthenticationFilter( http.getSharedObject(AuthenticationManager.class), userIdSource ! =null ? userIdSource : newAuthenticationNameUserIdSource(), usersConnectionRepository, authServiceLocator); . http.authenticationProvider(new SocialAuthenticationProvider(usersConnectionRepository, socialUsersDetailsService))
			.addFilterBefore(postProcess(filter), AbstractPreAuthenticatedProcessingFilter.class);
	}
                    
	protected <T> T postProcess(T object) {
		return (T) this.objectPostProcessor.postProcess(object); }}Copy the code

Modify the login page

<a href="/socialLogin/qq">Qq login</a>
Copy the code

Also release the interception of the federated login URL in the federated login configuration class

public class QQSpringSocialConfigurer extends SpringSocialConfigurer {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) object;
        filter.setFilterProcessesUrl(securityProperties.getSocial().getFilterProcessingUrl());
        return (T) filter;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http); http.authorizeRequests() .mvcMatchers(securityProperties.getSocial().getFilterProcessingUrl() + securityProperties.getSocial().getQq().getProviderId()) .permitAll(); }}Copy the code

Visit www.zhenganwen.top/login.html, click on the qq login found jump as follows

Authorized jump logic passes! For the code of this phase, see: gitee.com/zhenganwen/…

Stage summary

Callback field resolution

You are in the service of local port 80 run, why authentication server to parse the callback in the domain www.zhenganwen.top/socialLogin/qq domain name to jump to your local

Note that in the address bar of the authorized login page above, the URL has the redirect_URL parameter attached, so when you agree to authorize login, the redirect_URL parameter value is performed in your browser. You configured 127.0.0.1 www.zhenganwen.top in hosts, so the browser sends requests /socialLogin/ QQ directly to 127.0.0.1:80 without domain resolution, which is the security-demo service we are running

What does SpringSoicalConfigure do?

Direct source code:

public class SpringSocialConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
	@Override
	public void configure(HttpSecurity http) throws Exception {		
		ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
		UsersConnectionRepository usersConnectionRepository = getDependency(applicationContext, UsersConnectionRepository.class);
		SocialAuthenticationServiceLocator authServiceLocator = getDependency(applicationContext, SocialAuthenticationServiceLocator.class);
		SocialUserDetailsService socialUsersDetailsService = getDependency(applicationContext, SocialUserDetailsService.class);
		
		SocialAuthenticationFilter filter = newSocialAuthenticationFilter( http.getSharedObject(AuthenticationManager.class), userIdSource ! =null ? userIdSource : newAuthenticationNameUserIdSource(), usersConnectionRepository, authServiceLocator); . http.authenticationProvider(newSocialAuthenticationProvider(usersConnectionRepository, socialUsersDetailsService)) .addFilterBefore(postProcess(filter), AbstractPreAuthenticatedProcessingFilter.class); }}Copy the code

If we want to apply all the SpringSoical components we wrote earlier, we need to follow the authentication mechanism of SpringSecurity, that is, to add a new authentication method, we need to add an XxxAuthenticationFilter, And SpringSoical has helped us to achieve the SocialAuthenticationFilter, so we just need to add it in the filter. Just as we had encapsulated SMS logins to SmsLoginConfig, SpringSocial encapsulated social logins to SpringSocialConfigure, This enables social logins as long as business systems (i.e. applications that rely on SpringSocial) simply call httpsecurity.apply (springSocialConfigure).

And in addition to add SoicalAuthenticationFilter outside of the filter in the chain, SpringSocialConfigure will also in a container UsersConnectionRepository and SocialAuthenticationServiceLocator associated with SoicalAuthenticationFilter, SoicalAuthenticationFilter can be according to the request through the former process social information (providerId and providerUserId) query to the userId, Through the latter can take corresponding SocialAuthenticationService according to providerId and get into the ConnectionFactory to obtain authorization code, obtain accessToken, get the user operations such as social information

public interface UsersConnectionRepository {
	List<String> findUserIdsWithConnection(Connection
        connection);
}
Copy the code
public interface SocialAuthenticationServiceLocator extends ConnectionFactoryLocator { SocialAuthenticationService<? > getAuthenticationService(String providerId); }Copy the code
public interface SocialAuthenticationService<S> {
	ConnectionFactory<S> getConnectionFactory(a);
	SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException;
}
Copy the code

Why are there SocialAuthenticationService, come at what time?

SocialAuthenticationService is a wrapper of ConnectionFactory SocialAuthenticationFilter request and hiding OpenAPI call details

Because we added @enablesocial to SocialConfig, So when the system starts according to SocialAutoConfigurerAdapter implementation class createConnectionFactory create corresponding to different social systems in the ConnectionFactory and packaged into SocialAuthentication Service, then all SocialAuthenticationService providerId as key cached SocialAuthenticationLocator

@Component
//@ConditionalOnProperty(prefix = "demo.security.qq",name = "appId")
@ConditionalOnProperty(prefix = "demo.security.social.qq",name = "appId")
public class QQLoginAutoConfig extends SocialAutoConfigurerAdapter {

    public static final String URL_TO_GET_AUTHORIZATION_CODE = "https://graph.qq.com/oauth2.0/authorize";

    public static final String URL_TO_GET_TOKEN = "https://graph.qq.com/oauth2.0/token";

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private QQConnectionAdapter qqConnectionAdapter;

    @Override
    protectedConnectionFactory<? > createConnectionFactory() {return new QQConnectionFactory(
                securityProperties.getSocial().getQq().getProviderId(),
                new QQServiceProvider(oAuth2Operations(), securityProperties.getSocial().getQq().getAppId()),
                qqConnectionAdapter);
    }

    @Bean
    public OAuth2Operations oAuth2Operations(a) {
        return newOAuth2Template( securityProperties.getSocial().getQq().getAppId(), securityProperties.getSocial().getQq().getAppSecret(), URL_TO_GET_AUTHORIZATION_CODE, URL_TO_GET_TOKEN); }}Copy the code
class SecurityEnabledConnectionFactoryConfigurer implements ConnectionFactoryConfigurer {

	private SocialAuthenticationServiceRegistry registry;
	
	public SecurityEnabledConnectionFactoryConfigurer(a) {
		registry = new SocialAuthenticationServiceRegistry();
	}
	
	public void addConnectionFactory(ConnectionFactory
        connectionFactory) {
		registry.addAuthenticationService(wrapAsSocialAuthenticationService(connectionFactory));
	}
	
	public ConnectionFactoryRegistry getConnectionFactoryLocator(a) {
		return registry;
	}

	private <A> SocialAuthenticationService<A> wrapAsSocialAuthenticationService(ConnectionFactory<A> cf) {
		if (cf instanceof OAuth1ConnectionFactory) {
			return new OAuth1AuthenticationService<A>((OAuth1ConnectionFactory<A>) cf);
		} else if (cf instanceof OAuth2ConnectionFactory) {
			final OAuth2AuthenticationService<A> authService = new OAuth2AuthenticationService<A>((OAuth2ConnectionFactory<A>) cf);
			authService.setDefaultScope(((OAuth2ConnectionFactory<A>) cf).getScope());
			return authService;
		}
		throw new IllegalArgumentException("The connection factory must be one of OAuth1ConnectionFactory or OAuth2ConnectionFactory"); }}Copy the code
public class SocialAuthenticationServiceRegistry extends ConnectionFactoryRegistry implements SocialAuthenticationServiceLocator {

	privateMap<String, SocialAuthenticationService<? >> authenticationServices =newHashMap<String, SocialAuthenticationService<? > > ();publicSocialAuthenticationService<? > getAuthenticationService(String providerId) { SocialAuthenticationService<? > authenticationService = authenticationServices.get(providerId);if (authenticationService == null) {
			throw new IllegalArgumentException("No authentication service for service provider '" + providerId + "' is registered");
		}
		return authenticationService;
	}

	public void addAuthenticationService(SocialAuthenticationService
        authenticationService) {
		addConnectionFactory(authenticationService.getConnectionFactory());
		authenticationServices.put(authenticationService.getConnectionFactory().getProviderId(), authenticationService);
	}

	public void setAuthenticationServices(Iterable
       
        > authenticationServices)
       > {
		for (SocialAuthenticationService<?> authenticationService : authenticationServices) {
			addAuthenticationService(authenticationService);
		}
	}

	public Set<String> registeredAuthenticationProviderIds(a) {
		returnauthenticationServices.keySet(); }}Copy the code

So when SocialAuthenticationFilter intercepted / {filterProcessingUrl} / {providerId}, According to the URL path of the providerId to find corresponding SocialAuthenticationService get authRequest SocialAuthenticationLocator

public class SocialAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	@Deprecated
	protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
		String providerId = getRequestedProviderId(request);
		if(providerId ! =null){
			Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
			return authProviders.contains(providerId);
		}
		return false;
	}     

	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		if (detectRejection(request)) {
			if (logger.isDebugEnabled()) {
				logger.debug("A rejection was detected. Failing authentication.");
			}
			throw new SocialAuthenticationException("Authentication failed because user rejected authorization.");
		}
		
		Authentication auth = null;
		Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
		String authProviderId = getRequestedProviderId(request);
		if(! authProviders.isEmpty() && authProviderId ! =null&& authProviders.contains(authProviderId)) { SocialAuthenticationService<? > authService = authServiceLocator.getAuthenticationService(authProviderId); auth = attemptAuthService(authService, request, response);if (auth == null) {
				throw new AuthenticationServiceException("authentication failed"); }}returnauth; }}Copy the code

Why is the social login URL consistent with the callback field

SocialAuthenticationFilter#attemptAuthService

private Authentication attemptAuthService(finalSocialAuthenticationService<? > authService,final HttpServletRequest request, HttpServletResponse response) 
			throws SocialAuthenticationRedirectException, AuthenticationException {

		final SocialAuthenticationToken token = authService.getAuthToken(request, response);
		if (token == null) return null;
		
		Assert.notNull(token.getConnection());
		
		Authentication auth = getAuthentication();
		if (auth == null| |! auth.isAuthenticated()) {return doAuthentication(authService, request, token);
		} else {
			addConnection(authService, request, token, auth);
			return null; }}Copy the code

OAuth2AuthenticationService#getAuthToken

public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
		String code = request.getParameter("code");
		if(! StringUtils.hasText(code)) { OAuth2Parameters params =new OAuth2Parameters();
			params.setRedirectUri(buildReturnToUrl(request));
			setScope(request, params);
			params.add("state", generateState(connectionFactory, request));
			addCustomParameters(params);
			throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
		} else if (StringUtils.hasText(code)) {
			try {
				String returnToUrl = buildReturnToUrl(request);
				AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
				// TODO avoid API call if possible (auth using token would be fine)
				Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
				return new SocialAuthenticationToken(connection, null);
			} catch (RestClientException e) {
				logger.debug("failed to exchange for access", e);
				return null; }}else {
			return null; }}Copy the code

Can be found that the user login and click on the qq login blocked by SocialAuthenticationFilter, into the getAuthToken method above, request parameters are without authorization code, so 9 guild exception is thrown, This exception is intercepted by the authentication failure handler and directs the user to the social system authentication server

public class SocialAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private AuthenticationFailureHandler delegate;

    public SocialAuthenticationFailureHandler(AuthenticationFailureHandler delegate) {
        this.delegate = delegate;
    }

    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        if (failed instanceof SocialAuthenticationRedirectException) {
            response.sendRedirect(((SocialAuthenticationRedirectException)failed).getRedirectUrl());
        } else {
            this.delegate.onAuthenticationFailure(request, response, failed); }}}Copy the code

After the user agrees to authorize, the authentication server jumps to the callback field and enters the authorization code. At this time, the authentication server enters line 11 of getAuthToken, obtains the AccessGrant (accessToken) with the authorization code, invokes OpenAPI to obtain the user information and ADAPTS to Connection

Why do you agree to the authorization and respond as follows

What happens after we scan the QR code to grant authorization and the browser redirects to /socialLogin/ QQ

public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
		String code = request.getParameter("code");
		if(! StringUtils.hasText(code)) { OAuth2Parameters params =new OAuth2Parameters();
			params.setRedirectUri(buildReturnToUrl(request));
			setScope(request, params);
			params.add("state", generateState(connectionFactory, request));
			addCustomParameters(params);
			throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
		} else if (StringUtils.hasText(code)) {
			try {
				String returnToUrl = buildReturnToUrl(request);
				AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
				// TODO avoid API call if possible (auth using token would be fine)
				Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
				return new SocialAuthenticationToken(connection, null);
			} catch (RestClientException e) {
				logger.debug("failed to exchange for access", e);
				return null; }}else {
			return null; }}Copy the code

Trace the above interruption point at line 12 with an ah, and find that line 13 throws an exception and jumps to line 18 with the following exception:

org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html]
Copy the code

An error was reported when exchangeForAccess was called to our OAuth2Template to get the authorization code for accessToken. The error occurred because the text/ HTML converter was not processed when converting the response to AccessGrant.

First let’s see what the response looks like:

Find that the response result is a string that splits the three key-value pairs with &, and OAuth2Template provides the following converter by default:

OAuth2Template

protected RestTemplate createRestTemplate(a) {
		ClientHttpRequestFactory requestFactory = ClientHttpRequestFactorySelector.getRequestFactory();
		RestTemplate restTemplate = newRestTemplate(requestFactory); List<HttpMessageConverter<? >> converters =newArrayList<HttpMessageConverter<? > > (2);
		converters.add(new FormHttpMessageConverter());
		converters.add(new FormMapHttpMessageConverter());
		converters.add(new MappingJackson2HttpMessageConverter());
		restTemplate.setMessageConverters(converters);
		restTemplate.setErrorHandler(new LoggingErrorHandler());
		if(! useParametersForClientAuthentication) { List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();if (interceptors == null) {   // defensively initialize list if it is null. (See SOCIAL-430)
				interceptors = new ArrayList<ClientHttpRequestInterceptor>();
				restTemplate.setInterceptors(interceptors);
			}
			interceptors.add(new PreemptiveBasicAuthClientHttpRequestInterceptor(clientId, clientSecret));
		}
		return restTemplate;
}	
Copy the code

View the three converters in lines 5 to 7 above, FormHttpMessageConverter, FormMapHttpMessageConverter, MappingJackson2HttpMessageConverter respectively corresponding to parse the content-type for application/x -www-form-urlencoded, multipart/form-data, application/json response body, so an error message is displayed

no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html]
Copy the code

We need to add a text/ HTML converter to the OAuth2Template:

public class QQOAuth2Template extends OAuth2Template {
    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true);
    }

    / * * * add message converter so that can parse the content-type to text/HTML response body * StringHttpMessageConverter can parse any the content-type response body, see its constructor *@return* /
    @Override
    protected RestTemplate createRestTemplate(a) {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }

    /** * If the response body is JSON, OAuth2Template will help us build, but QQ connection OpenAPI return package is text/ HTML string * response body: "Access_token = FE04 * * * * * * * * * * * CCE2 & expires_in = 7776000 & refresh_token = 88 e4 BE14" * * * * * * * * * use StringHttpMessageConverter Convert the response body of the request to a String and manually build the AccessGrant *@paramAccessTokenUrl Indicates the URL * that obtains accessToken with the authorization code@paramParameters request accessToken Requires the attached parameter *@return* /
    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
        String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters,String.class);
        if (StringUtils.isEmpty(responseStr)) {
            return null;
        }
        // 0 -> access_token=FE04***********CCE
        // 1 -> expires_in=7776000
        // 2 -> refresh_token=88E4********BE14
        String[] strings = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
        // accessToken scope refreshToken expiresIn
        AccessGrant accessGrant = new AccessGrant(
                StringUtils.substringAfterLast(strings[0]."="),
                null,
                StringUtils.substringAfterLast(strings[2]."="),
                Long.valueOf(StringUtils.substringAfterLast(strings[1]."=")));
        returnaccessGrant; }}Copy the code

Replace the previously injected OAuth2Template with this QQOAuth2Template

@Component
//@ConditionalOnProperty(prefix = "demo.security.qq",name = "appId")
@ConditionalOnProperty(prefix = "demo.security.social.qq",name = "appId")
public class QQLoginAutoConfig extends SocialAutoConfigurerAdapter {

    public static final String URL_TO_GET_AUTHORIZATION_CODE = "https://graph.qq.com/oauth2.0/authorize";

    public static final String URL_TO_GET_TOKEN = "https://graph.qq.com/oauth2.0/token";

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private QQConnectionAdapter qqConnectionAdapter;

    @Override
    protectedConnectionFactory<? > createConnectionFactory() {return new QQConnectionFactory(
                securityProperties.getSocial().getQq().getProviderId(),
                new QQServiceProvider(oAuth2Operations(), securityProperties.getSocial().getQq().getAppId()),
                qqConnectionAdapter);
    }

// @Bean
// public OAuth2Operations oAuth2Operations() {
// return new OAuth2Template(
// securityProperties.getSocial().getQq().getAppId(),
// securityProperties.getSocial().getQq().getAppSecret(),
// URL_TO_GET_AUTHORIZATION_CODE,
// URL_TO_GET_TOKEN);
/ /}

    @Bean
    public OAuth2Operations oAuth2Operations(a) {
        return newQQOAuth2Template( securityProperties.getSocial().getQq().getAppId(), securityProperties.getSocial().getQq().getAppSecret(), URL_TO_GET_AUTHORIZATION_CODE, URL_TO_GET_TOKEN); }}Copy the code

Now that we can get the AccessGrant that encapsulates accessToken, proceed to the endpoint debug Connection (line 15 below)

OAuth2AuthenticationService

public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
		String code = request.getParameter("code");
		if(! StringUtils.hasText(code)) { OAuth2Parameters params =new OAuth2Parameters();
			params.setRedirectUri(buildReturnToUrl(request));
			setScope(request, params);
			params.add("state", generateState(connectionFactory, request));
			addCustomParameters(params);
			throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
		} else if (StringUtils.hasText(code)) {
			try {
				String returnToUrl = buildReturnToUrl(request);
				AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
				// TODO avoid API call if possible (auth using token would be fine)
				Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
				return new SocialAuthenticationToken(connection, null);
			} catch (RestClientException e) {
				logger.debug("failed to exchange for access", e);
				return null; }}else {
			return null; }}Copy the code

It is found that the QQApiImpl getUserInfo has the same problem, the response type of calling QQ Internet API is text/ HTML, so we can not directly convert into POJO, but need to obtain the response string, and then convert through JSON conversion tool class ObjectMapper:

QQApiImpl

@Override
    public QQUserInfo getUserInfo(a) {
        // The content-type is text/ HTML, so it cannot be converted to QQUserInfo directly
// QQUserInfo qqUserInfo = getRestTemplate().getForObject(String.format(URL_TO_GET_USER_INFO, appId, openId), QQUserInfo.class);
        String responseStr = getRestTemplate().getForObject(String.format(URL_TO_GET_USER_INFO, appId, openId), String.class);
        logger.info(Call QQ OpenAPI to obtain user information: {}", responseStr);
        try {
            QQUserInfo qqUserInfo = objectMapper.readValue(responseStr, QQUserInfo.class);
            qqUserInfo.setOpenId(openId);
            return qqUserInfo;
        } catch (Exception e) {
            logger.error("Failed to transfer user information to QQUserInfo, response message :{}", responseStr);
            return null; }}Copy the code

Saul again breakpoint debugging code login, found that the Connection can also be successfully received, and encapsulated into SocialAuthenticationToken returns, hence getAuthToken finally successfully returns, reached the doAuthentication

SocialAuthenticationFilter

private Authentication attemptAuthService(finalSocialAuthenticationService<? > authService,final HttpServletRequest request, HttpServletResponse response) 
			throws SocialAuthenticationRedirectException, AuthenticationException {

		final SocialAuthenticationToken token = authService.getAuthToken(request, response);
		if (token == null) return null;
		
		Assert.notNull(token.getConnection());
		
		Authentication auth = getAuthentication();
		if (auth == null| |! auth.isAuthenticated()) {return doAuthentication(authService, request, token);
		} else {
			addConnection(authService, request, token, auth);
			return null; }}private Authentication doAuthentication(SocialAuthenticationService
        authService, HttpServletRequest request, SocialAuthenticationToken token) {
		try {
			if(! authService.getConnectionCardinality().isAuthenticatePossible())return null;
			token.setDetails(authenticationDetailsSource.buildDetails(request));
			Authentication success = getAuthenticationManager().authenticate(token);
			Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principal type");
			updateConnections(authService, token, success);			
			return success;
		} catch (BadCredentialsException e) {
			// connection unknown, register new user?
			if(signupUrl ! =null) {
				// store ConnectionData in session and redirect to register page
				sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
				throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
			}
			throwe; }}Copy the code

Then calls a ProviderManager authenticate to check SocialAuthenticationToken ProviderManager will entrust SocialAuthenticationProvider again

SocialAuthenticationProvider will call we injected JdbcUsersConnectionRepository to UserConnection table based on providerId and providerUserId Connection Find the userId

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type"); Assert.isTrue(! authentication.isAuthenticated(),"already authenticated"); SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication; String providerId = authToken.getProviderId(); Connection<? > connection = authToken.getConnection(); String userId = toUserId(connection);if (userId == null) {
			throw new BadCredentialsException("Unknown access token");
		}

		UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
		if (userDetails == null) {
			throw new UsernameNotFoundException("Unknown connected account id");
		}

		return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
}

protected String toUserId(Connection
        connection) {
		List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
		// only if a single userId is connected to this providerUserId
		return (userIds.size() == 1)? userIds.iterator().next() :null;
}
Copy the code

JdbcUsersConnectionRepository

public List<String> findUserIdsWithConnection(Connection
        connection) {
		ConnectionKey key = connection.getKey();
		List<String> localUserIds = jdbcTemplate.queryForList("select userId from " + tablePrefix + "UserConnection where providerId = ? and providerUserId = ?", String.class, key.getProviderId(), key.getProviderUserId());		
		if (localUserIds.size() == 0&& connectionSignUp ! =null) {
			String newUserId = connectionSignUp.execute(connection);
			if(newUserId ! =null)
			{
				createConnectionRepository(newUserId).addConnection(connection);
				returnArrays.asList(newUserId); }}return localUserIds;
}
Copy the code

Not finding it (because our UserConnection table has no data at all at this point), toUserId returns NULL and throws a BadCredentialsException(“Unknown Access Token “), The exception will be SocialAuthenticationFilter capture, and according to its signupUrl attribute redirect (are that the user is not registered in this system, or registered but no local users and QQ login link, so jump to the registration page)

private Authentication doAuthentication(SocialAuthenticationService
        authService, HttpServletRequest request, SocialAuthenticationToken token) {
		try {
			if(! authService.getConnectionCardinality().isAuthenticatePossible())return null;
			token.setDetails(authenticationDetailsSource.buildDetails(request));
			Authentication success = getAuthenticationManager().authenticate(token);
			Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principal type");
			updateConnections(authService, token, success);			
			return success;
		} catch (BadCredentialsException e) {
			// connection unknown, register new user?
			if(signupUrl ! =null) {
				// store ConnectionData in session and redirect to register page
				sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
				throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
			}
			throwe; }}Copy the code

And SocialAuthenticationFilter signupUrl defaults to/signup

public class SocialAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	private String signupUrl = "/signup";
}                    
Copy the code

Jump to/signup, blocked by SpringSecurity, redirect to the loginPage (), and finally to the BrowserSecurityController

SecurityBrowserConfig

.formLogin()
		.loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
Copy the code

SecurityConstants

/** * If you do not log in to the protected URL, jump to this */
String FORWARD_TO_LOGIN_PAGE_URL = "/auth/require";
Copy the code

BrowserSecurityController

@RestController
public class BrowserSecurityController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    // Security stores pre-jump requests in the session
    private RequestCache requestCache = new HttpSessionRequestCache();

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    SecurityProperties securityProperties;

    @RequestMapping("/auth/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponseResult requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {

        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if(savedRequest ! =null) {
            String redirectUrl = savedRequest.getRedirectUrl();
            logger.info("The request that triggers the jump to /auth/login is: {}", redirectUrl);
            if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) {
                / / if the user is blocked by FilterSecurityInterceptor to access the HTML page jump to the auth/login and then redirected to the login pageredirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage()); }}// If a redirect to /auth/login instead of HTML is intercepted, a JSON message is returned
        return new SimpleResponseResult("User is not logged in. Please direct user to login page."); }}Copy the code

The result is the following response:

What did @enablesocial do

It loads a SocialConfiguration class that reads SocialConfigure instances in the container, As we write to expand SocialAutoConfigureAdapter QQLoginAutoConfig and expanded the SocialConfigureAdapter SocialConfig, Will we achieve ConnectionFactory and UsersConnectionRepository skewer and SpringSecurity certification process

/**
 * Configuration class imported by {@link EnableSocial}.
 * @author Craig Walls
 */
@Configuration
public class SocialConfiguration {

	private static boolean securityEnabled = isSocialSecurityAvailable();
	
	@Autowired
	private Environment environment;
	
	private List<SocialConfigurer> socialConfigurers;

	@Autowired
	public void setSocialConfigurers(List<SocialConfigurer> socialConfigurers) {
		Assert.notNull(socialConfigurers, "At least one configuration class must implement SocialConfigurer (or subclass SocialConfigurerAdapter)");
		Assert.notEmpty(socialConfigurers, "At least one configuration class must implement SocialConfigurer (or subclass SocialConfigurerAdapter)");
		this.socialConfigurers = socialConfigurers;
	}

	@Bean
	public ConnectionFactoryLocator connectionFactoryLocator(a) {
		if (securityEnabled) {
			SecurityEnabledConnectionFactoryConfigurer cfConfig = new SecurityEnabledConnectionFactoryConfigurer();
			for (SocialConfigurer socialConfigurer : socialConfigurers) {
				socialConfigurer.addConnectionFactories(cfConfig, environment);
			}
			return cfConfig.getConnectionFactoryLocator();
		} else {
			DefaultConnectionFactoryConfigurer cfConfig = new DefaultConnectionFactoryConfigurer();
			for (SocialConfigurer socialConfigurer : socialConfigurers) {
				socialConfigurer.addConnectionFactories(cfConfig, environment);
			}
			returncfConfig.getConnectionFactoryLocator(); }}@Bean
	public UsersConnectionRepository usersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
		UsersConnectionRepository usersConnectionRepository = null;
		for (SocialConfigurer socialConfigurer : socialConfigurers) {
			UsersConnectionRepository ucrCandidate = socialConfigurer.getUsersConnectionRepository(connectionFactoryLocator);
			if(ucrCandidate ! =null) {
				usersConnectionRepository = ucrCandidate;
				break;
			}
		}
		Assert.notNull(usersConnectionRepository, "One configuration class must implement getUsersConnectionRepository from SocialConfigurer.");
		returnusersConnectionRepository; }}Copy the code

Registration page & Associated social accounts

First, the URL of the registration page is configurable, set to /sign-up. HTML by default, and /user/register, the service interface that handles registration

@Data
public class SocialProperties {

  private QQSecurityPropertie qq = new QQSecurityPropertie();

  public static final String DEFAULT_FILTER_PROCESSING_URL = "/socialLogin";                    
  private String filterProcessingUrl = DEFAULT_FILTER_PROCESSING_URL;

  public static final String DEFAULT_SIGN_UP_URL = "/sign-up.html";                    
  private String signUpUrl = DEFAULT_SIGN_UP_URL;

  public static final String DEFAULT_SING_UP_PROCESSING_URL = "/user/register";
  private String signUpProcessingUrl = DEFAULT_SING_UP_PROCESSING_URL;                    
}
Copy the code

Then release this path in the browser configuration class:

@Autowired
    private SpringSocialConfigurer qqSpringSocialConfigurer;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // Enable the verification filter
        http.apply(verifyCodeValidatorConfig).and()
        // Enable the SMS login filter
            .apply(smsLoginConfig).and()
        // Enable QQ login
            .apply(qqSpringSocialConfigurer).and()
            // Enable the form password login filter
            .formLogin()
                .loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
                .loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .and()
            // The browser application-specific configuration saves the token generated after login in a cookie
            .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(3600)
                .userDetailsService(customUserDetailsService)
                .and()
            // Browser application-specific configuration
            .authorizeRequests()
                .antMatchers(
                        SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL,
                        securityProperties.getBrowser().getLoginPage(),
                        SecurityConstants.VERIFY_CODE_SEND_URL,
                        securityProperties.getSocial().getFilterProcessingUrl() + securityProperties.getSocial().getQq().getProviderId(),
                        securityProperties.getSocial().getSignUpUrl(),
                        securityProperties.getSocial().getSignUpProcessingUrl()).permitAll()
                .anyRequest().authenticated().and()
            .csrf().disable();
    }
Copy the code

Finally, write the registration page:


      
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
  </head>
  <body>
    <h1>Standard registration page</h1>
    <a href="/social">QQ Account information</a>
    <form action="/user/register" method="post">User name:<input type="text" name="username" value="admin">Password:<input type="password" name="password" value="123">
      <button type="submit" name="type" value="register">Register and associate with QQ login</button>
      <button type="submit" name="type" value="binding">An existing account is associated with QQ login</button>
    </form>

  </body>
</html>
Copy the code

ProviderSignInUtils

Registration Services: Although there is no record associated with local users in the UserConnection table, we jump to the registration page, but the Connection obtained or saved in the Session, if you want to automatically associate the QQ account for users when they click to register a local account or manually associate the QQ account for users who already have a local account, You can use the ProviderSignInUtils utility class. All you need to do is tell it to associate with the local account userId, and it will automatically fetch the Connection saved in the Session. And userId, Connection. GetProviderId, Connection. GetProviderUserId as a record is inserted into the database, so that the user during the QQ login again next time won’t jump to the local account registration page

@RestController
@RequestMapping("/user")
public class UserController {

  private Logger logger = LoggerFactory.getLogger(getClass());

  @Autowired
  private UserService userService;

  @Autowired
  private ProviderSignInUtils providerSignInUtils;

  @PostMapping("/register")
  public String register(String username, String password, String type, HttpServletRequest request) {
    if ("register".equalsIgnoreCase(type)) {
      logger.info("Add user and associate QQ login, username :{}", username);
      userService.insertUser();
    } else if ("binding".equalsIgnoreCase(type)) {
      logger.info("Give user associated QQ login, user name :{}", username);
    }
    providerSignInUtils.doPostSignUp(username, new ServletWebRequest(request));
    return "success"; }}Copy the code

Support for binding/unbinding scenarios

Sometimes our system’s account management module needs to allow users to associate or unassociate social accounts, and SpringSocial supports this scenario (see ConnectController). You just need to customize the relevant view component (extensible AbstractView) to implement the “bind/unbind” functionality.

Session management

Single-machine Session Management

In fact, our customized login process will only be executed once during login. After successful login, an Authentication encapsulating Authentication information will be generated and stored in the local thread safe, and the components in the login process will not be involved in subsequent operations such as user access to the protected URL.

Let’s recall the Spring Security filter chain, is SecurityContextPersistenceFilter is located in the first place, it is used to attempt to read from the Session when the receipt of a request generated after a successful login authentication information in the current thread safe, In response to a request out again into the Session, and is located in the filter chain FilterSecurityInterceptor will be on a visit to the Controller at the end of service in check before thread safe authentication information, Therefore, Session management directly affects whether the user can continue to access the protected URL at the moment.

In SpringBoot, you can configure server.session.timeout (unit: second) to set the validity period of the session, so that users need to log in again if they are still accessing the protected URL after logging in for a period of time.

Located in TomcatEmbeddedServletContainerFactory related code

private void configureSession(Context context) {
		long sessionTimeout = getSessionTimeoutInMinutes();
		context.setSessionTimeout((int) sessionTimeout);
		if (isPersistSession()) {
			Manager manager = context.getManager();
			if (manager == null) {
				manager = new StandardManager();
				context.setManager(manager);
			}
			configurePersistSession(manager);
		}
		else {
			context.addLifecycleListener(newDisablePersistSessionListener()); }}private long getSessionTimeoutInMinutes(a) {
		long sessionTimeout = getSessionTimeout();
		if (sessionTimeout > 0) {
			sessionTimeout = Math.max(TimeUnit.SECONDS.toMinutes(sessionTimeout), 1L);
		}
		return sessionTimeout;
	}
Copy the code

SpringBoot will convert the number of seconds you set to minutes, so you will find that server.session.timeout=10 will expire after 1 minute and need to log in again.

application.properties

Server.session. timeout=10 # Set session to expire after 10 secondsCopy the code

But we usually set it to a few hours

Different from accessing a protected URL without logging in, a different message should be displayed when a protected URL cannot be accessed due to Session expiration (for example, the Session you logged in to has expired because there is no operation for a long time. Please log in again. Instead of asking you to log in, please log in first), In this case, you can configure http.sessionManage().invalidsessionURL () to specify the URL to which the user will redirect to when accessing the protected URL after the login time exceeds the specified duration specified by server.session.timeout. You can configure a page or Controller to prompt the user and direct the user to the login page

SecurityBrowserConfig

protected void configure(HttpSecurity http) throws Exception {

        // Enable the verification filter
        http.apply(verifyCodeValidatorConfig).and()
        // Enable the SMS login filter
            .apply(smsLoginConfig).and()
        // Enable QQ login
            .apply(qqSpringSocialConfigurer).and()
            // Enable the form password login filter
            .formLogin()
                .loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
                .loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .and()
            // The browser application-specific configuration saves the token generated after login in a cookie
            .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(3600)
                .userDetailsService(customUserDetailsService)
                .and()
            .sessionManagement()
                .invalidSessionUrl("/session-invalid.html")
                .and()
            // Browser application-specific configuration
            .authorizeRequests()
                .antMatchers(
                        SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL,
                        securityProperties.getBrowser().getLoginPage(),
                        SecurityConstants.VERIFY_CODE_SEND_URL,
                        securityProperties.getSocial().getFilterProcessingUrl() + securityProperties.getSocial().getQq().getProviderId(),
                        securityProperties.getSocial().getSignUpUrl(),
                        securityProperties.getSocial().getSignUpProcessingUrl(),
                        "/session-invalid.html").permitAll()
                .anyRequest().authenticated().and()
            .csrf().disable();
    }
Copy the code

In.sessionManagement() configuration:

MaximumSessions controls the number of sessions a user can log in to at the same time. If set to 1, the last logon will kick the previous logon. The expiredSessionStrategy allows you to set up a callback method for the event (called when the previous person has been bumped and then accesses the protected URL) that retrieves request and Response from the callback parameters

Through maxSessionsPreventsLogin (true) can be set if the user is logged in, then in other Session can’t log in again, the Session due to a timeout setting failure or secondary login is blocked, You can use.invalidsessionStrategy () to configure a processing strategy

Cluster Session Management

In order to achieve high availability and concurrency, enterprise applications usually deploy services in a cluster, and forward requests to specific services according to polling algorithms through gateways or proxies. In this case, if each service manages its own Session separately, repeated requests for user login will occur. We can take Session management out of the system and store it in a separate system. The Spring-Session project can do this for us by telling it what storage system to use to store sessions.

Generally we use Redis to store sessions instead of Mysql for the following reasons:

  • SpringSecurityFor each request will be fromSessionTo read authentication information, so read more frequently, using the cache system faster
  • SessionThere is a valid time, if stored inMysqlThey also need to be cleaned regularly, whileRedisIt has its own cache data timeliness

Install Redis

Official website, download and compile

$Wget HTTP: / / http://download.redis.io/releases/redis-5.0.5.tar.gz
$The tar XZF redis - 5.0.5. Tar. Gz
$ cdRedis - 5.0.5
$ make MALLOC=libc
Copy the code

Yum install -y GCC g++ GCC -c++ make yum install -y GCC g++ GCC -c++ make

Start the service:

./src/redis-server

/redis.conf: bind 192.168.102.2 (my host LAN IP address) to the host computer to access the IP address. This is equivalent to adding an IP whitelist. If you want all hosts to access the service, you can configure bind 0.0.0.0

After modifying the configuration, you need to read the configuration file for the configuration items to take effect during the restart./ SRC /redis-server./redis.conf &

SpringBoot configuration file

Add spring.redis.host=192.168.102.101 to application.properties to specify the redis (default port 6379) to connect to the host when SpringBoot starts, and remove the previous rule that excludes redis automatic integration

//@SpringBootApplication(exclude = {RedisAutoConfiguration.class,RedisRepositoriesAutoConfiguration.class})
@SpringBootApplication
@RestController
@EnableSwagger2
public class SecurityDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityDemoApplication.class, args);
    }

    @RequestMapping("/hello")
    public String hello(a) {
        return "hello spring security"; }}Copy the code

Always specify to host Session to Redis in the configuration file

Spring. The session. Store - type = redis spring. Redis. Host = 192.168.102.101Copy the code

Can support the managed type encapsulated in the org. Springframework. Boot. Autoconfigure. Session. The StoreType.

In clustered mode, the timeout and http.sessionManagement() configurations still take effect.

Note: After hosting sessions to the storage system, ensure that beans written to the Session are Serializable. That is, the Serializable interface is implemented. If attributes in the Bean cannot be serialized, such as BufferedImage image in ImageCode, If you do not need to store it in the Session, you can set this property to NULL when writing to the Session

@Override
public void save(ServletWebRequest request, ImageCode imageCode) {
    ImageCode ic = new ImageCode(imageCode.getCode(), null, imageCode.getExpireTime());
    sessionStrategy.setAttribute(request, SecurityConstants.IMAGE_CODE_SESSION_KEY, ic);
}
Copy the code

Log out

How to Log Out

Security gives us a default to logout the current user’s service /logout, which does three things by default:

  • Make the currentSessionfailure
  • removeremember-meInformation about the functionality
  • removeSecurityContextThe contents of the

You can customize the logout logic using http.logout()

  • logoutUrl()To specify the URL for the deregistration operation request
  • logoutSuccessUrl(), the URL to which to jump after the logout is complete
  • logoutSuccessHandler()The handler that is invoked after logout can dynamically respond to the page or JSON according to the user request type
  • deleteCookies(), according to thekeydeleteCookieIn theitem

Spring Security OAuth develops the APP authentication framework

Everything we’ve talked about so far is based on B/S architecture, which means that the user accesses our service directly through the browser, based on Session/Cookie. However, nowadays, the back-end separation architecture is becoming more and more popular. The user may directly access the APP or WebServer (such as NodeJS), and then the APP and WebServer invoke the back-end services through Ajax. In this scenario, the Session/Cookie mode has many disadvantages

  • Development is cumbersome and requires frequent targetingSession/CookieFor read and write operations, requests sent from the browser will be stored inCookieIn theJSESSIONIDThe back end can find the corresponding according to thisSession, and the response will beJSESSIONIDwriteCookie. If the browser is disabledCookieShould be attached to each URLJSESSIONIDparameter
  • Security and customer experience are poor, and sensitive data is stored on the clientCookieIt’s not very safe,SessionImproper Settings such as time-sensitive management and distributed management will lead to frequent re-login of users, resulting in poor user experience
  • Some of the front-end technologies simply don’t support itCookie, such as App, applets

Spring Security OAuth provides a token-based authentication mechanism. Instead of reading authentication information stored in the Session each time a request is made, a token is issued to the authorized user. Only token parameters are required to access the service. Compared to session-based tokens, tokens are more flexible and secure. Unlike Session tokens, the allocation of sessionids and parameter attachments are fixed. How the token is presented and what information it contains, as well as how the token refresh mechanism can transparently extend the authorization period (without user awareness) to avoid repeated logins, etc., can be customized by us.

When it comes to OAuth, it may be easy to think of the third-party login function developed before. In fact, Spring Social encapsulates the OAuth client process, while Spring Security OAuth encapsulates the OAuth authentication server.

In terms of the system developed by ourselves, the back end is the authentication server and resource server, while the front-end APP and WebServer are equivalent to the OAuth client.

All the authentication server needs to do is provide the four authorization modes and the generation and storage of tokens. The resource server protects the REST service and verifies the tokens in the request through filters before invoking the service. What we need to do is to integrate our customized authentication logic (user name and password login, SMS verification code login, third-party login) into the authentication server, and interconnect to generate and store tokens.

From this chapter, we will use Spring Security OAuth to develop security-app projects, based on pure OAuth authentication mode, without relying on Session/Cookie

The preparatory work

First, we will comment out the security-browser dependency introduced in security-Demo, and introduce security-app. Forget the authentication code developed based on Session/Cookie, and start from scratch to develop authentication and authorization based on OAuth.

Since the VerifyCodeValidateFilter in security-core needs to be injected into the authentication success/failure handler, we will copy the security-Demo into security-app. It responds to the result as JSON (security-browser’s result can be a page, but security-app can only respond to JSON) and moves SimpleResponseResult into security-core.

package top.zhenganwen.securitydemo.app.handler;

@Component("appAuthenticationFailureHandler")
public class AppAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

// @Autowired
// private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// if (securityProperties.getBrowser().getLoginProcessType() == LoginProcessTypeEnum.REDIRECT) {
// super.onAuthenticationFailure(request, response, exception);
// return;
/ /}
        logger.info("Login failed =>{}", exception.getMessage());
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write(objectMapper.writeValueAsString(newSimpleResponseResult(exception.getMessage()))); response.getWriter().flush(); }}Copy the code
package top.zhenganwen.securitydemo.app.handler;

@Component("appAuthenticationSuccessHandler")
public class AppAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

// @Autowired
// private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException
            , ServletException {
// if (securityProperties.getBrowser().getLoginProcessType() == LoginProcessTypeEnum.REDIRECT) {
// // redirects to the pre-login request URL cached in session
// super.onAuthenticationSuccess(request, response, authentication);
/ /}
        logger.info("User {} login successful", authentication.getName());
        response.setContentType("application/json; charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); response.getWriter().flush(); }}Copy the code

Restart the service to see if the project runs properly after removing security-Browser and introducing security-app.

Enabling the Authentication Server

Just use an annotation @ EnableAuthorizationServer can make the current service an authentication server, the starter – oauth2 already help us to package the authentication server need to provide 4 kinds of authorization management mode and the token.

package top.zhenganwen.securitydemo.app.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

/**
 * @author zhenganwen
 * @date 2019/9/11
 * @desc AuthorizationServerConfig
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig {
}

Copy the code

Now we can test the authorization code and password patterns in 4 authorization patterns

First of all, there should be a user on the authentication server. For convenience, DAO and UserDetailsService will not be written here. We can add a user through configuration:

Username =test security.user.password=test security.user.role=user # To use OAuth, the user needs to have the user role, which is stored in the database as ROLE_USERCopy the code

Then configure a clientId/clientSecret, which is equivalent to the appId/appSecret registered on the security-Demo interconnected development platform before other applications call security-Demo for third-party login. For example, if an application has been registered and approved on security-Demo, security-Demo will assign an appId:test-client and appSecret:123 to it. Now our security-Demo has also become an authentication server, and any other applications that call the Security-Demo API to obtain tokens can be treated as third-party applications or clients.

security.oauth2.client.client-id=test-client
security.oauth2.client.client-secret=123
Copy the code

Then we can contrast OAuth2 website reference document to verify the @ EnableAuthorizationServer provides four kinds of authorization model and obtain the token

Test the authorization code schema

See Request Criteria

The authorization code mode has two steps:

  1. Obtaining authorization Code

    Observing the boot startup log, it is found that the framework adds several interfaces for us, including /oauth/authorize, which is the interface to obtain the authorization code. We try to obtain an authorization code against the request criteria for obtaining an authorization code in OAuth2

    http://localhost/oauth/authorize?
    response_type=code
    &client_id=test-client
    &redirect_uri=http://example.com
    &scope=all
    Copy the code

    Where response_type is fixed as code to obtain the authorization code, client_id is the appId of the client, and redirect_uri is the callback URL for the client to receive the authorization code and further obtain the token. The authorization code will be attached to the URL to which the authorization successfully jumps), and scope indicates the scope of permission that the authorization needs to obtain (the key value and the meaning of the key value should be determined by the authentication server, so we will write a random one here). After accessing this URL, a login box for basic authentication will pop up. After logging in, we enter user name test and password test and jump to the authorization page, asking us whether to grant all permission (in actual development, we can divide permissions into create, delete, update and read according to operation types. It can also be divided into user, admin, guest, etc.) :

    We click Approve, click Authorize, and then jump to the callback URL with the authorization code attached

    Note down the authorization code yO4Y6q for subsequent token acquisition

  2. Access token

    Restlet Client is a Chrome plugin to do this

    1. Click on theAdd authorizationThe inputclient-idandclient-secretThe tool will automatically encrypt and attach it to the request header for usAuthorizatinIn the
    2. Fill in request parameters

    If Postman is used, the Authorization Settings are as follows:

    Click Send to Send the request, and the response is as follows:

Password mode

In password mode, you can directly obtain a token without an authorization code

Using password mode is equivalent to telling the test-client user to register the user name and password in security-Demo. The client directly obtains the token by using the password mode. The authentication server does not know whether the client requested the token with the user’s authorization or secretly obtained the token with a known username and password, but if the client application is an internal application, you do not need to worry about this

There is another detail here: a token is issued to a corresponding user in authorization code mode, so the token obtained in password mode is still returned, and the expiration time expire_in is gradually shortened

There is no specified way to store the token, so it is stored in memory by default. If you restart the service, you need to apply for the token again

Enabling a Resource Server

Similarly, using an @enableresourceserver annotation makes the service a resource server (verifies the token before invoking the service)

package top.zhenganwen.securitydemo.app.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

/ * * *@author zhenganwen
 * @date 2019/9/11
 * @desc ResourceServerConfig
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig {}Copy the code

Access to the query user interface /user response 401 after the service is restarted indicates that the resource server is in effect (access to the protected service without a token will be blocked). This is also not the default basic authentication for Security, because if basic blocks it, it will bring up the login box, which is not present here

Then we use password mode to generate the token again: 7F6C95FD-558F-4EAE-93FE-1841bd06eA5C with token when accessing the interface (add request header Authorization value token_type access_token)

Using Postman is more convenient:

Spring Security Oauth source code analysis

The framework’s core components are shown below, with boxes in green representing concrete classes, blue representing interfaces/abstractions, and parentheses representing classes that are actually invoked by the runtime. We will use the cipher mode as an example to analyze the source code, you can also break the point of verification step by step.

TokenEndpoint – TokenEndpoint

TokenEndpoint can be thought of as a Controller that accepts our token request, as shown in the postAccessToken method:

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map
       
         parameters)
       ,> throws HttpRequestMethodNotSupportedException {

    if(! (principalinstanceof Authentication)) {
        throw new InsufficientAuthenticationException(
            "There is no client authentication. Try adding an appropriate authentication filter.");
    }

    String clientId = getClientId(principal);
    ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

    TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

    if(clientId ! =null && !clientId.equals("")) {
        // Only validate the client details if a client authenticated during this
        // request.
        if(! clientId.equals(tokenRequest.getClientId())) {// double check to make sure that the client ID in the token request is the same as that in the
            // authenticated client
            throw new InvalidClientException("Given client ID does not match authenticated client"); }}if(authenticatedClient ! =null) {
        oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
    }
    if(! StringUtils.hasText(tokenRequest.getGrantType())) {throw new InvalidRequestException("Missing grant type");
    }
    if (tokenRequest.getGrantType().equals("implicit")) {
        throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
    }

    if (isAuthCodeRequest(parameters)) {
        // The scope was requested or determined during the authorization step
        if(! tokenRequest.getScope().isEmpty()) { logger.debug("Clearing scope of incoming token request"); tokenRequest.setScope(Collections.<String> emptySet()); }}if (isRefreshTokenRequest(parameters)) {
        // A refresh token has its own default scopes, so we should ignore any added by the factory here.
        tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
    }

    OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
    if (token == null) {
        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
    }

    return getResponse(token);

}
Copy the code

The first input parameter contains two parts: principal and parameters, corresponding to the two parts of our password mode request parameters: request header Authorization and request body (grant_type, username, password, scope).

String clientId = getClientId(principal);

Principal of the incoming is actually a the UsernamePasswordToken, corresponding logic in BasicAuthenticationFilter doFilterInternal method:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    final boolean debug = this.logger.isDebugEnabled();

    String header = request.getHeader("Authorization");

    if (header == null| |! header.startsWith("Basic ")) {
        chain.doFilter(request, response);
        return;
    }

    try {
        String[] tokens = extractAndDecodeHeader(header, request);
        assert tokens.length == 2;

        String username = tokens[0];

        if (authenticationIsRequired(username)) {
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, tokens[1]); }}catch (AuthenticationException failed) {

    }

    chain.doFilter(request, response);
}

private String[] extractAndDecodeHeader(String header, HttpServletRequest request)
    throws IOException {

    byte[] base64Token = header.substring(6).getBytes("UTF-8");
    byte[] decoded;
    try {
        decoded = Base64.decode(base64Token);
    }
    catch (IllegalArgumentException e) {
        throw new BadCredentialsException(
            "Failed to decode basic authentication token");
    }

    String token = new String(decoded, getCredentialsCharset(request));

    int delim = token.indexOf(":");

    if (delim == -1) {
        throw new BadCredentialsException("Invalid basic authentication token");
    }
    return new String[] { token.substring(0, delim), token.substring(delim + 1)}; }Copy the code

BasicAuthenticationFilter will intercept/request/token and attempt to parse request Authorization, to get the corresponding Basic string XXX, remove the first 6 characters, Basic, access to XXX, This is actually the result of the colon-concatenated clientId and clientSecret that we passed in using base64 encryption, Therefore, in the extractAndDecodeHeader method, XXX will be base64 decrypted to get the ciphertext composed of clientId and clientSecret separated by colon (borrow the previous clientId=test-client and clientSecret=123) UsernamePasswordToken = test-client:123); client-id = username; clientSecret = password; So the principal in postAccessToken gets the clientId in the request header.

ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

Then call ClientDetailsService to query the registered customer details according to clientId, namely ClientDetails, which is the information filled in and verified by external applications when they register the security-Demo open platform, and contains several items. We only have clientId and clientSecret. (authenticatedClient means that this client is approved by us and allowed to access our open platform)

TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

Then a TokenRequest is constructed according to the request body parameters and the clientDetails. The tokenRequest indicates which clientDetails is requesting the token, which user (parameters.username) is requesting the token, which authorization mode (parameters.grant_type) is requesting the token, and which user is requesting the token Some permissions (parameters.scope)

if (clientId ! = null && ! clientId.equals(""))

The incoming clientId and authenticatedClient’s clientId are then verified. You may ask that the authenticatedClient is based on the incoming clientId. In fact, this is not true. Although the query method is called loadClientByClientId, it can only be understood as querying the audited client according to the unique identifier of the client. Perhaps this unique identifier is the irrelevant primary key ID of the client table in our database, or it may be the value of the clientId field. That is, we need to understand the method name loadClientByClientId at a macro level. So it makes sense to check clientId here.

if (authenticatedClient ! = null)

If authenticatedClient is not null, verify the request’s permission scope:

private void validateScope(Set<String> requestScopes, Set<String> clientScopes) {

    if(clientScopes ! =null && !clientScopes.isEmpty()) {
        for (String scope : requestScopes) {
            if(! clientScopes.contains(scope)) {throw new InvalidScopeException("Invalid scope: "+ scope, clientScopes); }}}if (requestScopes.isEmpty()) {
        throw new InvalidScopeException("Empty scope (either the client or the user is not allowed the requested scopes)"); }}Copy the code

Imagine a scenario where an external application requests access to our open platform to read user information on our platform, and the corresponding clientScopes are [“read”], and the client asks for a token after being audited. Who are you; 2. What can you do? Scope can only be [“read”], and cannot be [“read”,”write”], etc. This is to verify that any scope passed in when the token is requested is included in the scopes registered by the client.

if (! StringUtils.hasText(tokenRequest.getGrantType()))

Then check that the grant_type parameter cannot be empty, as required by the OAuth protocol.

if (tokenRequest.getGrantType().equals("implicit"))

Then check whether the grant_type passed in is implicit, that is, whether the client obtains the token in simple mode. In simple mode, the token is directly obtained after the user agrees to authorization. Therefore, the token acquisition interface should not be called again.

if (isAuthCodeRequest(parameters))

If so, empty the scope in the tokenRequest, because the client’s permissions should not be determined by the scope it passed in itself, but by the scopes we approved when it registered. This property is subsequently overridden by the scope read from the client view.

if (isRefreshTokenRequest(parameters))

private boolean isRefreshTokenRequest(Map<String, String> parameters) {
    return "refresh_token".equals(parameters.get("grant_type")) && parameters.get("refresh_token") != null;
}
Copy the code

Check whether it is a request to refresh the token. In fact, grant_type can request token in addition to the four authorization modes authorization_code, implicit, password, client_credential in OAuth standard, there is also a refresh_token. In order to improve the user experience (traditional login method requires re-login after a period of time), the token refresh mechanism can prolong the token validity without the user being aware of it. If the request is to refresh the token, as noted in the comment, the refresh_token mode also has its own default scopes and should not be used as included in the request.

OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

This is the most important step, which is the encapsulation and validation of the request parameters. This step is called TokenGranter token granter generate token, behind the method getResponse (token) is to generate the token of the direct response. Depending on the authorization type grant_type passed in and the corresponding parameters that need to be passed in, different TokenGranter implementation classes are tuned for token construction. This logic is in the CompositeTokenGranter:

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
    for (TokenGranter granter : tokenGranters) {
        OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
        if(grant! =null) {
            returngrant; }}return null;
}
Copy the code

In AbstractTokenGranter, only TokenGranter corresponding to the request parameter grant_type will be called:

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

    if (!this.grantType.equals(grantType)) {
        return null;
    }

    String clientId = tokenRequest.getClientId();
    ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
    validateGrantType(grantType, client);

    logger.debug("Getting access token for: " + clientId);

    return getAccessToken(client, tokenRequest);

}
Copy the code
public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {
	private static final String GRANT_TYPE = "authorization_code";
}

public class ClientCredentialsTokenGranter extends AbstractTokenGranter {
	private static final String GRANT_TYPE = "client_credentials";
}

public class ImplicitTokenGranter extends AbstractTokenGranter {
	private static final String GRANT_TYPE = "implicit";
}

public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {
	private static final String GRANT_TYPE = "password";
}

public class RefreshTokenGranter extends AbstractTokenGranter {
	private static final String GRANT_TYPE = "refresh_token";
}
Copy the code

Token grantor — TokenGranter

Because is password mode as an example, the process to the ResourceOwnerPasswordTokenGranter. Grant, it didn’t rewrite grant method, so call the parent class’s grant method:

AbstractTokenGranter

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

    if (!this.grantType.equals(grantType)) {
        return null;
    }

    String clientId = tokenRequest.getClientId();
    ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
    validateGrantType(grantType, client);

    return getAccessToken(client, tokenRequest);

}

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
    return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
    return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
Copy the code

Focus on line 20, calling subclass getOAuth2Authentication OAuth2Authentication, and call to token authentication server service AuthorizationServerTokenServices token is generated. For getOAuth2Authentication here, each TokenGranter subclass has different implementations, because the verification logic of different authorization modes is different. Such as the authorization code mode this link need to verify the authorization code of the request (tokenRequest. The parameters. Code) whether I had sent authorization code corresponding to the client (clientDetails); The password mode verifies whether the user name and password passed in by the request exists in the current system and whether the password is correct. After passing the verification, an OAuth2Authentication will be returned, containing oAuth related information and system user related information.

AuthorizationServerTokenServices

OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
Copy the code

ResourceOwnerPasswordTokenGranter

@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

    Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
    String username = parameters.get("username");
    String password = parameters.get("password");
    // Protect from downstream leaks of password
    parameters.remove("password");

    Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
    ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
    try {
        userAuth = authenticationManager.authenticate(userAuth);
    }
    catch (AccountStatusException ase) {
        // Covers expired, locked, disabled cases (mentioned in Section 5.2, Draft 31)
        throw new InvalidGrantException(ase.getMessage());
    }
    catch (BadCredentialsException e) {
        // If the username/password are wrong the spec says we should send 400/invalid grant
        throw new InvalidGrantException(e.getMessage());
    }
    if (userAuth == null| |! userAuth.isAuthenticated()) {throw new InvalidGrantException("Could not authenticate user: " + username);
    }

    OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);		
    return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
Copy the code

Can be found that ResourceOwnerPasswordTokenGranter validation logic and we wrote before the user name password authentication logic of filter is almost the same: Get the username and password from the request, then build an authRequest and pass it to the ProviderManager for verification. ProviderManager entrusted to DaoAuthenticationProvider nature will call us again UserDetailsService custom implementation class CustomUserDetailsService querying user and check.

OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);

Check by return certification after successful Authentication, will call the factory method according to the customer details and tokenRequest build AuthenticationServerTokenServices OAuth2Authentication return as needed.

Token service – AuthorizationServerTokenServices the authentication server

After receiving OAuth2Authentication, the token service generates a token. The token service implementation class DefaultTokenServices generates a token:

@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

    OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
    OAuth2RefreshToken refreshToken = null;
    if(existingAccessToken ! =null) {
        if (existingAccessToken.isExpired()) {
            if(existingAccessToken.getRefreshToken() ! =null) {
                refreshToken = existingAccessToken.getRefreshToken();
                // The token store could remove the refresh token when the
                // access token is removed, but we want to
                // be sure...
                tokenStore.removeRefreshToken(refreshToken);
            }
            tokenStore.removeAccessToken(existingAccessToken);
        }
        else {
            // Re-store the access token in case the authentication has changed
            tokenStore.storeAccessToken(existingAccessToken, authentication);
            returnexistingAccessToken; }}// Only create a new refresh token if there wasn't an existing one
    // associated with an expired access token.
    // Clients might be holding existing refresh tokens, so we re-use it in
    // the case that the old access token
    // expired.
    if (refreshToken == null) {
        refreshToken = createRefreshToken(authentication);
    }
    // But the refresh token itself might need to be re-issued if it has
    // expired.
    else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
        ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
        if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
            refreshToken = createRefreshToken(authentication);
        }
    }

    OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
    tokenStore.storeAccessToken(accessToken, authentication);
    // In case it was modified
    refreshToken = accessToken.getRefreshToken();
    if(refreshToken ! =null) {
        tokenStore.storeRefreshToken(refreshToken, authentication);
    }
    return accessToken;

}
Copy the code

The first step is to try to obtain the token from tokenStore, the token repository, because tokenStore will be called after each token generation and before the response to save the generated token, so that the client will have a basis when accessing the resource with the token.

if (existingAccessToken ! = null)

If a token is obtained from tokenStore, it indicates that the token has been generated before. In this case, there are two situations:

  1. The oldtokenThe expiration date is expiredtokenRemove if thetokentherefresh_tokenAlso remove if it is still theretokenIs required for its correspondingrefresh_tokenIf thetokenFailure is associated with failurerefresh_tokenShould also be unavailable)
  2. The oldtokenIt has not expired. Save it againtokenBefore and after may be generated by different authorization modestoken), and return this directlytoken, the method ends.

If no old token is found from tokenStore, a new token is generated, stored in tokenStore and returned.

summary

Integrate the username and password to obtain the token

Although the framework has helped us encapsulate the four authorization modes required by the authentication server, these are generally external (external applications cannot read our system’s user information) and are used to build an open platform. For internal applications, we still need to provide user name and password login, mobile phone number verification code login and other ways to obtain tokens. First of all, the framework process up to the TokenGranter component is no longer available because it has been solidified by the OAuth process. What we can is token generated AuthorizationServerTokenServices service, but it needs a OAuth2Authentication, We need tokenRequest and authentication to build OAuth2Authentication.

On the basis of the original logon logic, we can modify the logon success processor, in which we can obtain the authentication success. And the ClientDetailsService injected by the clientId call from the request header Authorization finds the clientDetails and constructs the tokenRequest, so that the token generation service can be invoked to generate the token and respond.

Invoke the token service in the login success handler

AppAuthenticationSuccessHandler

package top.zhenganwen.securitydemo.app.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Component("appAuthenticationSuccessHandler")
public class AppAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {

        // Authentication
        Authentication userAuthentication = authentication;

        // ClientDetails
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null| |! authHeader.startsWith("Basic ")) {
            throw new UnapprovedClientAuthenticationException(The request header must contain information about the OAuth client.);
        }
        String[] clientIdAndSecret = extractAndDecodeHeader(authHeader);
        String clientId = clientIdAndSecret[0];
        String clientSecret = clientIdAndSecret[1];
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientIdAndSecret[0]);
        if (clientDetails == null) {
            throw new UnapprovedClientAuthenticationException("Invalid clientId");
        } else if(! StringUtils.equals(clientSecret, clientDetails.getClientSecret())) {throw new UnapprovedClientAuthenticationException("Wrong clientSecret");
        }

        // TokenRequest
        TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom");

        // OAuth2Request
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

        // OAuth2Authentication
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, userAuthentication);

        // AccessToken
        OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

        // response
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write(objectMapper.writeValueAsString(accessToken));
    }

    private String[] extractAndDecodeHeader(String header){

        byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
        byte[] decoded;
        try {
            decoded = Base64.decode(base64Token);
        }
        catch (IllegalArgumentException e) {
            throw new BadCredentialsException(
                    "Failed to decode basic authentication token");
        }

        String token = new String(decoded, StandardCharsets.UTF_8);

        int delim = token.indexOf(":");

        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        }
        return new String[] { token.substring(0, delim), token.substring(delim + 1)}; }}Copy the code

Inheritance ResourceServerConfigurerAdapter implement Security configuration

We copy the security configuration from BrowserSecurityConfig to ResourceServerConfig, enabling form password login only:

package top.zhenganwen.securitydemo.app.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.social.security.SpringSocialConfigurer;
import top.zhenganwen.security.core.SecurityConstants;
import top.zhenganwen.security.core.config.SmsLoginConfig;
import top.zhenganwen.security.core.config.VerifyCodeValidatorConfig;
import top.zhenganwen.security.core.properties.SecurityProperties;

import javax.sql.DataSource;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private AuthenticationSuccessHandler appAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler appAuthenticationFailureHandler;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsService customUserDetailsService;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(a) {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    @Autowired
    SmsLoginConfig smsLoginConfig;

    @Autowired
    private VerifyCodeValidatorConfig verifyCodeValidatorConfig;

    @Autowired
    private SpringSocialConfigurer qqSpringSocialConfigurer;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // Enable the form password login filter
        http.formLogin()
                .loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
                .loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
                .successHandler(appAuthenticationSuccessHandler)
                .failureHandler(appAuthenticationFailureHandler);

        http
// // Enable the verification code filter
// .apply(verifyCodeValidatorConfig).and()
// // Enable SMS login filter
// .apply(smsLoginConfig).and()
// // Enable QQ login
// .apply(qqSpringSocialConfigurer).and()
// // The token generated after login is saved in a cookie
// .rememberMe()
// .tokenRepository(persistentTokenRepository())
// .tokenValiditySeconds(3600)
// .userDetailsService(customUserDetailsService)
// .and()
// .sessionManagement()
// .invalidSessionUrl("/session-invalid.html")
// .invalidSessionStrategy((request, response) -> {})
// .maximumSessions(1)
/ / expiredSessionStrategy (event Ø - > {})
// .maxSessionsPreventsLogin(true)
// .and()
// .and()
                // Browser application-specific configuration
                .authorizeRequests()
                    .antMatchers(
                            SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL,
                            securityProperties.getBrowser().getLoginPage(),
                            SecurityConstants.VERIFY_CODE_SEND_URL,
                            securityProperties.getSocial().getFilterProcessingUrl() + securityProperties.getSocial().getQq().getProviderId(),
                            securityProperties.getSocial().getSignUpUrl(),
                            securityProperties.getSocial().getSignUpProcessingUrl(),
                            "/session-invalid.html").permitAll()
                    .anyRequest().authenticated()
                    .and()
                // Token-based authorization does not have the concept of login/logout, only the concept of token application and expiration.csrf().disable(); }}Copy the code

In this way, the internal application client can obtain the token from the user’s username and password:

  1. The request header should also include client information

  2. Request parameters Pass the user name and password required for login

  3. The token is obtained after the login succeeds

  4. Access services using tokens

    Postman still supports server writing and reading cookies

    To avoid the effects of Session/Cookie logins, we need to clear the Cookie each time before sending the request.

    First, the request without token is found to be blocked:

    Then attach token access request:

At this point, the user name and password login to obtain the token integration success!

The integration process for verification code and SMS login is similar and will not be described here. It is worth noting that token-based approaches to eliminate Session/Cookie operations can put the information stored on the server into a persistent layer such as Redis.

Integrate social login to obtain token

In this section, an internal application uses social login to obtain a token from an internal authentication server.

The simple model

Process analysis

If the internal application adopts the simple mode and the user directly obtains the token issued by the external service provider after authorization, then we cannot use the token to access the internal resource server. Instead, we need to use the token to the internal authentication server to exchange the token for the internal usage of our system.

The idea is that if the user successfully logs in socially, then the internal application gets the user’s providerUserId (called openId in the external service provider), And the UserConnection table should have a record (userId, providerId, providerUserId), internal application simply providerId and providerUserId to internal authentication server, The internal Authentication server checks the UserConnection table for verification and builds Authentication based on the userId to generate accessToken.

To do this, we need to write a set of providerId+openId authentication process on the internal authentication server:

Which UserConnectionRepository, CustomUserDetailsService, AppAuthenticationSuccessHandler are ready, you can use them directly.

SecurityPropertiesAdd processing basisopenIdtaketokenThe URL:

package top.zhenganwen.security.core.properties;

import lombok.Data;
import top.zhenganwen.security.core.social.qq.connect.QQSecurityPropertie;

/ * * *@author zhenganwen
 * @date 2019/9/5
 * @desc SocialProperties
 */
@Data
public class SocialProperties {
    private QQSecurityPropertie qq = new QQSecurityPropertie();

    public static final String DEFAULT_FILTER_PROCESSING_URL = "/socialLogin";
    private String filterProcessingUrl = DEFAULT_FILTER_PROCESSING_URL;

    public static final String DEFAULT_SIGN_UP_URL = "/sign-up.html";
    private String signUpUrl = DEFAULT_SIGN_UP_URL;

    public static final String DEFAULT_SING_UP_PROCESSING_URL = "/user/register";
    private String signUpProcessingUrl = DEFAULT_SING_UP_PROCESSING_URL;

    public static final String DEFAULT_OPEN_ID_FILTER_PROCESSING_URL = "/auth/openId";
    private String openIdFilterProcessingUrl = DEFAULT_OPEN_ID_FILTER_PROCESSING_URL;
}
Copy the code

Custom requestAuthenticationToken

package top.zhenganwen.securitydemo.app.security.openId;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/ * * *@author zhenganwen
 * @date 2019/9/15
 * @desc OpenIdAuthenticationToken
 */
public class OpenIdAuthenticationToken extends AbstractAuthenticationToken {

    // Stores the providerId as the token requesting authentication, and stores the user information as the token successfully authenticated
    private final Object principal;
    // Stores the openId as the token for authentication, and stores the user password as the token for successful authentication
    private Object credentials;

    // Called when requesting authentication
    public OpenIdAuthenticationToken(Object providerId, Object openId) {
        super(null);
        this.principal = providerId;
        this.credentials = openId;
        setAuthenticated(false);
    }

    // Called after the authentication succeeds
    public OpenIdAuthenticationToken(Object userInfo, Object password, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = userInfo;
        this.credentials = password;
        super.setAuthenticated(true);
    }


    @Override
    public Object getCredentials(a) {
        return this.credentials;
    }

    @Override
    public Object getPrincipal(a) {
        return this.principal; }}Copy the code

Authentication interceptorOpenIdAuthenticationFilter

package top.zhenganwen.securitydemo.app.security.openId;

import org.apache.commons.lang.StringUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.web.bind.ServletRequestUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/ * * *@author zhenganwen
 * @date 2019/9/15
 * @desc OpenIdAuthenticationFilter
 */
public class OpenIdAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    protected OpenIdAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

        // authRequest
        String providerId = ServletRequestUtils.getStringParameter(request, "providerId");
        if (StringUtils.isBlank(providerId)) {
            throw new BadCredentialsException("providerId is required");
        }
        String openId = ServletRequestUtils.getStringParameter(request,"openId");
        if (StringUtils.isBlank(openId)) {
            throw new BadCredentialsException("openId is required");
        }
        OpenIdAuthenticationToken authRequest = new OpenIdAuthenticationToken(providerId, openId);

        // authenticate
        returngetAuthenticationManager().authenticate(authRequest); }}Copy the code

Actual certification OfficerOpenIdAuthenticationProvider

package top.zhenganwen.securitydemo.app.security.openId;

import org.hibernate.validator.internal.util.CollectionHelper;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.util.CollectionUtils;

import java.util.Set;

/ * * *@author zhenganwen
 * @date 2019/9/15
 * @desc OpenIdAuthenticationProvider
 */
public class OpenIdAuthenticationProvider implements AuthenticationProvider {

    private UsersConnectionRepository usersConnectionRepository;

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if(! (authenticationinstanceof OpenIdAuthenticationToken)) {
            throw new IllegalArgumentException("Unsupported token authentication types :" + authentication.getClass());
        }

        // userId
        OpenIdAuthenticationToken authRequest = (OpenIdAuthenticationToken) authentication;
        Set<String> userIds = usersConnectionRepository.findUserIdsConnectedTo(authRequest.getPrincipal().toString(), CollectionHelper.asSet(authRequest.getCredentials().toString()));
        if (CollectionUtils.isEmpty(userIds)) {
            throw new BadCredentialsException("Invalid providerId and openId");
        }

        // userDetails
        String useId = userIds.stream().findFirst().get();
        UserDetails userDetails = userDetailsService.loadUserByUsername(useId);

        // authenticated authentication
        OpenIdAuthenticationToken authenticationToken = new OpenIdAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());

        return authenticationToken;
    }

    @Override
    public boolean supports(Class
        authentication) {
        return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public void setUsersConnectionRepository(UsersConnectionRepository usersConnectionRepository) {
        this.usersConnectionRepository = usersConnectionRepository;
    }

    public UsersConnectionRepository getUsersConnectionRepository(a) {
        return usersConnectionRepository;
    }

    public UserDetailsService getUserDetailsService(a) {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService; }}Copy the code

OpenId authentication flow configuration classOpenIdAuthenticationConfig

package top.zhenganwen.securitydemo.app.security.openId;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.stereotype.Component;
import top.zhenganwen.security.core.properties.SecurityProperties;

/ * * *@author zhenganwen
 * @date 2019/9/15
 * @desc OpenIdAuthenticationConfig
 */
@Component
public class OpenIdAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private AuthenticationFailureHandler appAuthenticationFailureHandler;

    @Autowired
    private AuthenticationSuccessHandler appAuthenticationSuccessHandler;

    @Autowired
    private UsersConnectionRepository usersConnectionRepository;

    @Autowired
    private UserDetailsService customUserDetailsService;

    @Override
    public void configure(HttpSecurity builder) throws Exception {

        OpenIdAuthenticationFilter openIdAuthenticationFilter = new OpenIdAuthenticationFilter(securityProperties.getSocial().getOpenIdFilterProcessingUrl());
        openIdAuthenticationFilter.setAuthenticationFailureHandler(appAuthenticationFailureHandler);
        openIdAuthenticationFilter.setAuthenticationSuccessHandler(appAuthenticationSuccessHandler);
        openIdAuthenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));

        OpenIdAuthenticationProvider openIdAuthenticationProvider = newOpenIdAuthenticationProvider(); openIdAuthenticationProvider.setUsersConnectionRepository(usersConnectionRepository); openIdAuthenticationProvider.setUserDetailsService(customUserDetailsService); builder .authenticationProvider(openIdAuthenticationProvider) .addFilterBefore(openIdAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

applyApplied to theSecurityIn the main configuration class

package top.zhenganwen.securitydemo.app.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import top.zhenganwen.security.core.SecurityConstants;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.securitydemo.app.security.openId.OpenIdAuthenticationConfig;

/ * * *@author zhenganwen
 * @date 2019/9/11
 * @desc ResourceServerConfig
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private AuthenticationSuccessHandler appAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler appAuthenticationFailureHandler;

    @Autowired
    private OpenIdAuthenticationConfig openIdAuthenticationConfig;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        // Enable form password login to obtain token
        http.formLogin()
                .loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
                .loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
                .successHandler(appAuthenticationSuccessHandler)
                .failureHandler(appAuthenticationFailureHandler);

        // Enable social login to obtain the token
        http.apply(openIdAuthenticationConfig);

        http
                .authorizeRequests()
                .antMatchers(
                        SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL,
                        securityProperties.getBrowser().getLoginPage(),
                        SecurityConstants.VERIFY_CODE_SEND_URL,
                        securityProperties.getSocial().getFilterProcessingUrl() + securityProperties.getSocial().getQq().getProviderId(),
                        securityProperties.getSocial().getSignUpUrl(),
                        securityProperties.getSocial().getSignUpProcessingUrl(),
                        "/session-invalid.html").permitAll() .anyRequest().authenticated() .and() .csrf().disable(); }}Copy the code

test

/auth/openId request token

And access /user to test token validity, access successful! Integrated social login successful!

Authorization code mode

If the internal application uses the authorization code mode, when the external service provider calls back with the authorization code, the internal application simply forwards the callback request to our authentication server, since we have written the social login module earlier, which makes it seamless.

Or take our previous QQ login as an example:

Internally, the QQ authentication server should simply forward the callback request to the authentication server as it is when the user agrees to authorize and the QQ authentication server redirects to the internal application callback domain, since we previously developed the /socialLogin interface to handle social logins.

Here, we can test that it is impossible to really develop an App. We can use the security-Browser project previously developed, and then make a breakpoint where we obtain the authorization code. After obtaining the authorization code, we stop the service (to avoid the authorization code being invalid when we request token with the authorization code later). Then request token with authorization code in Postman (simulate App forwarding callback field to /socialLogin/ QQ)

First comment security-app in security-demo to enable security-browser

<dependency>
    <groupId>top.zhenganwen</groupId>
    <artifactId>security-browser</artifactId>
    <version>1.0 the SNAPSHOT</version>
</dependency>
<! -- <dependency>-->
<! -- <groupId>top.zhenganwen</groupId>-->
<! -- <artifactId>security-app</artifactId>-->
<! - < version > 1.0 - the SNAPSHOT < / version > -- >
<! -- </dependency>-->
Copy the code

Move the CustomUserDetailsService to security-Core, as browser and app are both useful for:

package top.zhenganwen.security.core.service;

import org.hibernate.validator.constraints.NotBlank;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.social.security.SocialUser;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;

import java.util.Objects;

/ * * *@author zhenganwen
 * @date 2019/8/23
 * @desc CustomUserDetailsService
 */
@Component
public class CustomUserDetailsService implements UserDetailsService.SocialUserDetailsService {

    @Autowired
    BCryptPasswordEncoder passwordEncoder;

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public UserDetails loadUserByUsername(@NotBlank String username) throws UsernameNotFoundException {
        return buildUser(username);
    }

    private SocialUser buildUser(@NotBlank String username) {
        logger.info("Login username:" + username);
        // In a real project you can call Dao or Repository to check whether the user exists
        if (Objects.equals(username, "admin") = =false) {
            throw new UsernameNotFoundException("User name does not exist");
        }
        // Suppose the password is as follows
        String pwd = passwordEncoder.encode("123");

        return new SocialUser(
                "admin", pwd, AuthorityUtils.commaSeparatedStringToAuthorityList("user,admin")); }Select * from user table where user id is unique; select * from user table where user id is unique
    @Override
    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        returnbuildUser(userId); }}Copy the code

Then set the port 80 to start the service and in the following authorization code before the access token to set breakpoints (OAuth2AuthenticationService) :

Visit www.zhenganwen.top/login.html for QQ login authorization (at the same time open the browser console), authorize to, jump stop after the breakpoint stop service, find callback URL in the browser console and copy it:

Then switch the POM of security-Demo to app

<! -- <dependency>-->
<! -- <groupId>top.zhenganwen</groupId>-->
<! -- <artifactId>security-browser</artifactId>-->
<! - < version > 1.0 - the SNAPSHOT < / version > -- >
<! -- </dependency>-->
<dependency>
    <groupId>top.zhenganwen</groupId>
    <artifactId>security-app</artifactId>
    <version>1.0 the SNAPSHOT</version>
</dependency>
Copy the code

Enable QQ login in the Security master profile:

package top.zhenganwen.securitydemo.app.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import top.zhenganwen.security.core.SecurityConstants;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.social.qq.connect.QQSpringSocialConfigurer;
import top.zhenganwen.securitydemo.app.security.openId.OpenIdAuthenticationConfig;

/ * * *@author zhenganwen
 * @date 2019/9/11
 * @desc ResourceServerConfig
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private AuthenticationSuccessHandler appAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler appAuthenticationFailureHandler;

    @Autowired
    private OpenIdAuthenticationConfig openIdAuthenticationConfig;

    @Autowired
    private QQSpringSocialConfigurer qqSpringSocialConfigurer;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        // Enable form password login to obtain token
        http.formLogin()
                .loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
                .loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
                .successHandler(appAuthenticationSuccessHandler)
                .failureHandler(appAuthenticationFailureHandler);

        // Enable social login to obtain the token
        http.apply(openIdAuthenticationConfig);
        http.apply(qqSpringSocialConfigurer);

        http
                .authorizeRequests()
                .antMatchers(
                        SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL,
                        securityProperties.getBrowser().getLoginPage(),
                        SecurityConstants.VERIFY_CODE_SEND_URL,
                        securityProperties.getSocial().getFilterProcessingUrl() + securityProperties.getSocial().getQq().getProviderId(),
                        securityProperties.getSocial().getSignUpUrl(),
                        securityProperties.getSocial().getSignUpProcessingUrl(),
                        "/session-invalid.html").permitAll() .anyRequest().authenticated() .and() .csrf().disable(); }}Copy the code

Then we can use the Postman simulation App to retrieve the received authorization code and forward it to the authentication server for token:

In order to obtain token in PORT, the authentication server returns code is Reused Error (Authorization code is reused), supposedly we did a breakpoint in the previous time and stopped the service in time, the authorization code did not go through request token, this error still needs to be addressed.

Processor mode

But even token to get success, also won’t response we want accessToken, after in the configuration SocialAuthenticationFilter no certification for its successful processor, So we need to set the AppAuthenticationSuccessHandler to it, so that social will only be generated after a successful login and return us to the token.

Let’s again with simple but practical processor refactorings to security – app for security – the core SocialAuthenticationFilter an enhanced:

package top.zhenganwen.security.core.social;

import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;

/ * * *@author zhenganwen
 * @date 2019/9/15
 * @descAuthentication filter rear processor */
public interface AuthenticationFilterPostProcessor<T extends AbstractAuthenticationProcessingFilter> {
    /** * Make an enhancement to the authentication filter, such as replacing the default authentication success handler, etc@param filter
     */
    void process(T filter);
}
Copy the code
package top.zhenganwen.security.core.social.qq.connect;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.social.AuthenticationFilterPostProcessor;

/ * * *@author zhenganwen
 * @date 2019/9/5
 * @desc QQSpringSocialConfigurer
 */
public class QQSpringSocialConfigurer extends SpringSocialConfigurer {

    @Autowired(required = false)    // Not required
    private AuthenticationFilterPostProcessor<SocialAuthenticationFilter> processor;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) object;
        filter.setFilterProcessesUrl(securityProperties.getSocial().getFilterProcessingUrl());
        filter.setSignupUrl(securityProperties.getSocial().getSignUpUrl());
        processor.process(filter);
        return(T) filter; }}Copy the code
package top.zhenganwen.securitydemo.app.security.social;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.stereotype.Component;
import top.zhenganwen.security.core.social.AuthenticationFilterPostProcessor;

/ * * *@author zhenganwen
 * @date 2019/9/15
 * @desc SocialAuthenticationFilterProcessor
 */
@Component
public class SocialAuthenticationFilterProcessor implements AuthenticationFilterPostProcessor<SocialAuthenticationFilter> {

    @Autowired
    private AuthenticationSuccessHandler appAuthenticationSuccessHandler;

    @Override
    public void process(SocialAuthenticationFilter filter) { filter.setAuthenticationSuccessHandler(appAuthenticationSuccessHandler); }}Copy the code

Integrate associated social accounts

Temporary storage of third-party user information

Before, when users used social login for the first time, there was no corresponding association record in UserConnection (userId-> PROVIDerID-ProviderUserID). At that time, the logic was to put the third-party user information queried into the Session. Then jump to the social account management page to guide the user to make an association with the social account. The background can take out the third-party user information from the Session through the ProviderSignInUtils tool class and make an association with the userId passed in when the user confirms the association (insert it into the UserConnection). However, the ProviderSignInUtils provided by Security is session-based and cannot be implemented in token-based authentication.

At this time, we can cache the third-party user information obtained after the OAuth process to Redis with the user device deviceId as the key, and then take it out of Redis and insert it into UserConnection with userId as a record when the user confirms the association. In fact, it is the process of changing the storage mode (from memory Session to cache Redis).

For ProviderSignInUtils we’ll just wrap a RedisProviderSignInUtils and replace it.

Guide users to associate with social accounts

The following interfaces can be implemented in all bean initialization is complete before we call postProcessBeforeInitialization, bean postProcessAfterInitialization initialization after the call, if you don’t want to be enhanced, can return to the bean, Targeted enhancement can be filtered based on the incoming beanName.

public interface BeanPostProcessor {
	Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
	Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
Copy the code

We can be the interface of a class SpringSocialConfigurerPostProcessor in QQSpringSocialConfigurer bean initialization complete reset the configure. SignupUrl, If UserConnection has no associated record, the service corresponding to signupUrl is jumped.

In this service, a JSON message should be returned indicating that the front end needs to associate the social account (and retrieve the third party user information previously obtained through OAuth from Session by ProviderSignInUtils and temporarily store it in Redis using RedisProviderSignInUtils). Instead of redirecting to the social link page as previously set up. The returned information is in the following format: