Source code analysis
Because spring security social is implemented based on social authentication filter, let's start with social authentication filter
When it comes to Filter, there must be a corresponding configuration class
SocialAuthenticationFilter is no exception. Its corresponding configuration class is SpringSocialConfigurer. To make the project run, let's prepare the configuration first:
SocialConfig
core project com.spring.security.social path
package com.spring.security.social; import com.spring.security.properties.SecurityProperties; import com.spring.security.social.qq.connet.QQConnectionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.social.UserIdSource; import org.springframework.social.config.annotation.ConnectionFactoryConfigurer; import org.springframework.social.config.annotation.EnableSocial; import org.springframework.social.config.annotation.SocialConfigurerAdapter; import org.springframework.social.connect.ConnectionFactoryLocator; import org.springframework.social.connect.UsersConnectionRepository; import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository; import org.springframework.social.security.AuthenticationNameUserIdSource; import org.springframework.social.security.SpringSocialConfigurer; import javax.sql.DataSource; @Configuration @EnableSocial public class SocialConfig extends SocialConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private SecurityProperties securityProperties; @Override public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) { JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText()); // Specify the prefix of the table. The suffix is fixed. It is in the location of the JdbcUsersConnectionRepository repository.setTablePrefix("sys_"); return repository; } @Bean public SpringSocialConfigurer hkSocialSecurityConfig(){ // Default configuration class for component assembly // Include filter SocialAuthenticationFilter added to security filter chain //Custom login path String filterProcessesUrl = "/auth"; HkSpringSocialConfigurer configurer = new HkSpringSocialConfigurer(filterProcessesUrl); return configurer; } // Add a processor about ProviderId=qq. Note that clientId and clientSecret need to apply for qq Internet by themselves @Override public void addConnectionFactories( ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) { connectionFactoryConfigurer.addConnectionFactory( new QQConnectionFactory("qq", "clientId", "clientSecret")); } /** * You must create a UserIdSource, otherwise an error will be reported * * @return */ @Override public UserIdSource getUserIdSource() { return new AuthenticationNameUserIdSource(); } }
HkSpringSocialConfigurer
Ditto directory creation
package com.spring.security.social; import lombok.Data; import org.springframework.social.security.SocialAuthenticationFilter; import org.springframework.social.security.SpringSocialConfigurer; /** * Set login address */ @Data public class HkSpringSocialConfigurer extends SpringSocialConfigurer { private String filterProcessesUrl; public HkSpringSocialConfigurer(String filterProcessesUrl){ this.filterProcessesUrl = filterProcessesUrl; } @Override protected <T> T postProcess(T object) { SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object); filter.setFilterProcessesUrl(filterProcessesUrl); return (T) filter; } }
Create the following data tables in the database:
create table sys_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);
Add authentication module
@Autowired private SpringSocialConfigurer hkSocialSecurityConfig; .and() //Join social sign in .apply(hkSocialSecurityConfig)
The above two configurations can make the program run.
Visit / auth/qq and enter social authentication filter
private static final String DEFAULT_FILTER_PROCESSES_URL = "/auth"; private String filterProcessesUrl = DEFAULT_FILTER_PROCESSES_URL; // Intercept requests that meet the requirements protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { //If the requested url is / auth/qq, then the extracted ProviderId is qq String providerId = getRequestedProviderId(request); if (providerId != null){ Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds(); // Check if there is a SocialAuthenticationService handling ProviderId=qq return authProviders.contains(providerId); } return false; } //Analyze the requested url and extract the ProviderId from the url. //If the requested url is / auth/qq, then the extracted ProviderId is qq private String getRequestedProviderId(HttpServletRequest request) { String uri = request.getRequestURI(); int pathParamIndex = uri.indexOf(';'); if (pathParamIndex > 0) { // strip everything after the first semi-colon uri = uri.substring(0, pathParamIndex); } // uri must start with context path uri = uri.substring(request.getContextPath().length()); // remaining uri must start with filterProcessesUrl if (!uri.startsWith(filterProcessesUrl)) { return null; } uri = uri.substring(filterProcessesUrl.length()); // expect /filterprocessesurl/provider, not /filterprocessesurlproviderr if (uri.startsWith("/")) { return uri.substring(1); } else { return null; } }
public void addAuthenticationService(SocialAuthenticationService<?> authenticationService) { addConnectionFactory(authenticationService.getConnectionFactory()); authenticationServices.put(authenticationService.getConnectionFactory().getProviderId(), authenticationService); }
@Bean public ConnectionFactoryLocator connectionFactoryLocator() { 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); } return cfConfig.getConnectionFactoryLocator(); } }
Finally, through the analysis of the above code, we can override the addconnectionfactors method of the SocialConfigurerAdapter, which is the SocialConfig class above:
// Add a processor about ProviderId=qq. Note that clientId and clientSecret need to apply for qq Internet by themselves @Override public void addConnectionFactories( ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) { connectionFactoryConfigurer.addConnectionFactory( new QQConnectionFactory("qq", "clientId", "clientSecret")); }
If addconnectionfactors appears, spring cloud starter oauth2 cannot be replaced with the latest version
QQConnectionFactory
The path of core project com.spring.security.social.qq.connect creates the QQConnectionFactory class, which inherits oauthconnectionfactory
package com.spring.security.social.qq.connet; import com.spring.security.social.qq.api.QQ; import org.springframework.social.connect.support.OAuth2ConnectionFactory; public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> { public QQConnectionFactory(String providerId, String appId, String appSecret) { super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter()); } }
OAuth2ConnectionFactory generic is an interface for reading user data. When an error is reported, it is not necessary to worry about it before making an interface
Continue to follow up the request of / auth/qq. When we add a ConnectionFactory for qq, the request will be executed backward to:
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(); //Get ProviderId=qq in / auth/qq String authProviderId = getRequestedProviderId(request); if (!authProviders.isEmpty() && authProviderId != null && authProviders.contains(authProviderId)) { // Get social authentication service according to ProviderId=qq SocialAuthenticationService<?> authService = authServiceLocator.getAuthenticationService(authProviderId); // Get Authentication through social Authentication service auth = attemptAuthService(authService, request, response); if (auth == null) { throw new AuthenticationServiceException("authentication failed"); } } return auth; }
private Authentication attemptAuthService(final SocialAuthenticationService<?> authService, final HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException, AuthenticationException { // Get token through social authentication service final SocialAuthenticationToken token = authService.getAuthToken(request, response); if (token == null) return null; Assert.notNull(token.getConnection()); // Get the Authentication in the SecurityContext. If it is not logged in, it is null Authentication auth = getAuthentication(); if (auth == null || !auth.isAuthenticated()) { // Update the obtained third-party user information and return to Authentication return doAuthentication(authService, request, token); } else { // If it is not null, the third-party user information form in the database will be checked. If it does not exist, it will be added to the form addConnection(authService, request, token, auth); return null; } }
Follow the code into the final social authenticationtoken token = authservice.getauthtoken (request, response); that is, OAuth2AuthenticationService
public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException { // Get request parameter code String code = request.getParameter("code"); // If you don't get the code, it means it's not a callback request. According to oauth2.0 protocol, you need to create a request to enter the third-party login page if (!StringUtils.hasText(code)) { //Prepare request parameters OAuth2Parameters params = new OAuth2Parameters(); params.setRedirectUri(buildReturnToUrl(request)); setScope(request, params); params.add("state", generateState(connectionFactory, request)); addCustomParameters(params); // Throw an exception and prepare to jump to the third party login page throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params)); } else if (StringUtils.hasText(code)) { // If the code is not empty, it means the third-party callback request. Then you need to intercept the code value to request the accessToken try { // The callback url should be consistent with the previous callback url String returnToUrl = buildReturnToUrl(request); // The point is, here is to obtain the accessToken AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null); // Setting ConnectionValues through ApiAdapter 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; } }
AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
QQOAuth2Template
The path of core project com.spring.security.social.qq.connect creates the QQOAuth2Template class
package com.spring.security.social.qq.connet; import org.apache.commons.lang.StringUtils; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.social.oauth2.AccessGrant; import org.springframework.social.oauth2.OAuth2Template; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import java.nio.charset.Charset; /** * QQ Custom Template resolution */ public class QQOAuth2Template extends OAuth2Template { public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) { super(clientId, clientSecret, authorizeUrl, accessTokenUrl); // Set to true to place clientId and clientSecret in the request parameters setUseParametersForClientAuthentication(true); } /** * Custom send request and parse * * @param accessTokenUrl * @param parameters * @return */ @Override protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) { String result = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class); System.out.println("Response data:" + result); return createAccessGrant(result); } /** * After the AccessGrant is created according to the returned structure and returned successfully, the Access Token can be obtained in the returned package. Such as: * access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14 * * @param responseResult * @return */ private AccessGrant createAccessGrant(String responseResult) { String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseResult, "&"); String access_token = StringUtils.substringAfterLast(items[0], "="); String expires_in = StringUtils.substringAfterLast(items[1], "="); String refresh_token = StringUtils.substringAfterLast(items[2], "="); return new AccessGrant( access_token, StringUtils.EMPTY, refresh_token, Long.valueOf(expires_in)); } @Override protected RestTemplate createRestTemplate() { RestTemplate restTemplate = super.createRestTemplate(); restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); return restTemplate; } }
QQServiceProvider
core project com.spring.security.social.qq.connect create QQServiceProvider
package com.spring.security.social.qq.connet; import com.spring.security.social.qq.api.QQ; import com.spring.security.social.qq.api.QQImpl; import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider; public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> { private String appId; //Guide the user to the url of the authentication server to obtain the Authorization Code private static final String URL_AUTHORIZE="https://graph.qq.com/oauth2.0/authorize"; //The url of the Authorization Code returned by the authentication server obtains the Access Token through the Authorization Code private static final String URL_ACCESSTOKEN="https://graph.qq.com/oauth2.0/token"; public QQServiceProvider(String appId,String appSecret) { //String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl super(new QQOAuth2Template(appId,appSecret,URL_AUTHORIZE,URL_ACCESSTOKEN)); this.appId = appId; } @Override public QQ getApi(String accessToken) { return new QQImpl(accessToken, appId); } }
Set up Connection through AcessGrant later
// Setting ConnectionValues through ApiAdapter Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
That is to call the following setConnectionValues method
QQAdapter
core project com.spring.security.social.qq.connect create QQAdapter
package com.spring.security.social.qq.connet; import com.spring.security.social.qq.api.QQ; import com.spring.security.social.qq.api.QQUserInfo; import org.springframework.social.connect.ApiAdapter; import org.springframework.social.connect.ConnectionValues; import org.springframework.social.connect.UserProfile; public class QQAdapter implements ApiAdapter<QQ> { /** * Test whether QQ server is available * @param qq * @return */ @Override public boolean test(QQ qq) { return true; } @Override public void setConnectionValues(QQ api, ConnectionValues values) { QQUserInfo userInfo = api.getUserInfo(); //User name values.setDisplayName(userInfo.getNickname()); //User head values.setImageUrl(userInfo.getFigureurl_qq_1()); //Personal homepage values.setProfileUrl(null); //Service provider user ID openid values.setProviderUserId(userInfo.getOpenId()); } @Override public UserProfile fetchUserProfile(QQ qq) { return null; } @Override public void updateStatus(QQ qq, String s) { } }
After getting the token, go back to the doAuthentication operation in the SocialAuthenticationFilter.attemptAuthService method
private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) { try { if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null; token.setDetails(authenticationDetailsSource.buildDetails(request)); // According to whether the current third-party user information exists in the database // If there is no exception, throw it and jump to the registration page Authentication success = getAuthenticationManager().authenticate(token); Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type"); // If the user already exists, update the data updateConnections(authService, token, success); return success; } catch (BadCredentialsException e) { // If the user needs to register, skip the registration page if (signupUrl != null) { // Here, the ConnectionData will be stored in the session, which can be obtained through ProviderSignInUtils sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection())); throw new SocialAuthenticationRedirectException(buildSignupUrl(request)); } throw e; } }
Note here:
Authentication success = getAuthenticationManager().authenticate(token);
This code will jump to ProviderManager.authenticate first, and then enter SocialAuthenticationProvider.authenticate
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(); // Query whether the current third-party data exists in the database, and throw exceptions if not // Query through JdbcUsersConnectionRepository 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)); }
Source code of 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()); // According to the code logic, if connectionSignUp is set, a new userid will be created automatically, and exceptions will not be thrown and the registration page will not be entered if (localUserIds.size() == 0 && connectionSignUp != null) { String newUserId = connectionSignUp.execute(connection); if (newUserId != null) { createConnectionRepository(newUserId).addConnection(connection); return Arrays.asList(newUserId); } } return localUserIds; }
Here, according to the source code analysis, the third-party login is basically over. The remaining user data reading classes are
QQ
The core project om.spring.security.social.qq.api path creates a QQ interface
package com.spring.security.social.qq.api; /** * Get user information */ public interface QQ { QQUserInfo getUserInfo(); }
QQImpl
Same as above
package com.spring.security.social.qq.api; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang.StringUtils; import org.springframework.social.oauth2.AbstractOAuth2ApiBinding; import org.springframework.social.oauth2.TokenStrategy; public class QQImpl extends AbstractOAuth2ApiBinding implements QQ { //Application of ID private String appId; //User OpenID private String openId; //Get the URL of user openId private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s"; //Get user information url private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s"; private ObjectMapper objectMapper = new ObjectMapper(); /** * Get openid * Response: callback ({"client" Id ":" your "appid", "openid": "your" opened "}); * * @param accessToken * @param appId */ public QQImpl(String accessToken, String appId) { super(accessToken, TokenStrategy.AUTHORIZATION_HEADER); this.appId = appId; // The reason why you need to obtain openId is that you need to obtain user information through openId this.openId = requestOpenId(accessToken); } /** * Get openId * * <p>Response: callback ({"client" Id ":" your "appid", "openid": "your" opened "}); * * @param accessToken * @return */ private String requestOpenId(String accessToken) { String url = String.format(URL_GET_OPENID, accessToken); String result = getRestTemplate().getForObject(url, String.class); System.out.println("Obtain openId: " + result); String first = StringUtils.substringBeforeLast(result, "\""); return StringUtils.substringAfterLast(first, "\""); } /** * Get user information * * <p>https://wiki.connect.qq.com/get_user_info * * @return */ @Override public QQUserInfo getUserInfo() { //Send request String url = String.format(URL_GET_USERINFO, appId, openId); String result = getRestTemplate().getForObject(url, String.class); System.out.println("Access to user information:" + result); try { QQUserInfo qqUserInfo = objectMapper.readValue(result, QQUserInfo.class); qqUserInfo.setOpenId(openId); return qqUserInfo; } catch (Exception e) { throw new RuntimeException("Failed to get user information"); } } }
QQUserInfo
Same as above
Parameter reference: https://wiki.connect.qq.com/get_user_info
import lombok.Getter; import lombok.Setter; /** qq User information */ @Getter @Setter public class QQUserInfo { /** Return code */ private String ret; /** If RET < 0, there will be a corresponding error message prompt, and all the returned data will be encoded with UTF-8. */ private String msg; /** */ private String openId; /** I don't know what. It's not written in the document, but it's in the actual api return. */ private String is_lost; /** Province (municipality) */ private String province; /** City (municipality directly under the central government) */ private String city; /** Date of birth */ private String year; /** User's nickname in QQ space. */ private String nickname; /** QQ space image URL with size of 30 × 30 pixels. */ private String figureurl; /** QQ space image URL with size of 50 × 50 pixels. */ private String figureurl_1; /** QQ space image URL with size of 100 × 100 pixels. */ private String figureurl_2; private String figureurl_type; private String figureurl_qq; /** QQ image URL with size of 40 × 40 pixels. */ private String figureurl_qq_1; /** QQ image URL with size of 100 × 100 pixels. It should be noted that not all users have QQ's 100 × 100 head image, but 40 × 40 pixels are certain to have it. */ private String figureurl_qq_2; /** Gender. If not, return to "male" by default */ private String gender; /** Identify whether the user is a yellow diamond user (0: No; 1: Yes). */ private String is_yellow_vip; /** Identify whether the user is a yellow diamond user (0: No; 1: Yes) */ private String vip; /** Yellow drill grade */ private String yellow_vip_level; /** Yellow drill grade */ private String level; /** Identify whether it is an annual fee yellow diamond user (0: No; 1: Yes) */ private String is_yellow_year_vip; private String constellation; }
It should be noted that the configured callback address is consistent with the < a href = "/ auth / QQ" > QQ login < / a > request address
You can modify the local hosts file to test