Добро пожаловать, чтобы следоватьличный блог, этот текст«Разработка стека технологий Spring Security Аутентификация и авторизация на уровне предприятия (2)»следовать за
Разработать функцию входа в QQ
Подготовка: подайте заявку на получение appId и appSecret, см. подробностиПодготовка_oauth2-0
Домен обратного вызова:Woohoo. Работает стабильно. Верх/социальный вход…
Разработка сторонней функции доступа фактически означает реализацию вышеуказанного набора компонентов один за другим.В этом разделе мы разработаем функцию входа в систему QQ, начиная с левой половины рисунка выше.
ServiceProvider
Api
, объявите метод, соответствующий OpenAPI, для вызова API и преобразования результата ответа в POJO и возврата, что соответствует шагу 7 на диаграмме последовательности режима кода авторизации.
package top.zhenganwen.security.core.social.qq.api;
import top.zhenganwen.security.core.social.qq.QQUserInfo;
/**
* @author zhenganwen
* @date 2019/9/4
* @desc QQApi 封装对QQ开放平台接口的调用
*/
public interface QQApi {
QQUserInfo getUserInfo();
}
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
* @desc QQApiImpl 拿token调用开放接口获取用户信息
* 1.首先要根据 https://graph.qq.com/oauth2.0/me/{token} 获取用户在社交平台上的id => {@code openId}
* 2.调用QQ OpenAPI https://graph.qq.com/user/get_user_info?access_token=YOUR_ACCESS_TOKEN&oauth_consumer_key=YOUR_APP_ID&openid=YOUR_OPENID
* 获取用户在社交平台上的信息 => {@link QQApiImpl#getUserInfo()}
* <p>
* {@link AbstractOAuth2ApiBinding}
* 帮我们完成了调用OpenAPI时附带{@code token}参数, 见其成员变量{@code accessToken}
* 帮我们完成了HTTP调用, 见其成员变量{@code restTemplate}
* <p>
* 注意:该组件应是多例的,因为每个用户对应有不同的OpenAPI,每次不同的用户进行QQ联合登录都应该创建一个新的 {@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";
// 因为父类会帮我们附带token参数,因此这里URL忽略了token参数
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) {
// 调用OpenAPI时将需要传递的参数附在URL路径上
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
this.appId = appId;
// 获取用户openId, 响应结果格式: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() {
QQUserInfo qqUserInfo = getRestTemplate().getForObject(String.format(URL_TO_GET_USER_INFO, appId, openId), QQUserInfo.class);
logger.info("调用QQ OpenAPI获取用户信息: {}", qqUserInfo);
return qqUserInfo;
}
}
ПослеOAuth2Operations
, который используется для инкапсуляции импорта пользователя на страницу авторизации, получения кода авторизации, переданного после авторизации пользователя, и получения токена для доступа к OpenAPI, что соответствует шагам 2–6 на диаграмме последовательности режима кода авторизации. Так как мода этих шагов фиксирована, тоSpring Social
Помогите нам сделать сильную инкапсуляцию, то естьOAuth2Template
, поэтому нам не нужно реализовывать его самостоятельно, мы можем использовать этот компонент непосредственно позже
ServiceProvider
, интегрированныйOAuth2Operations
а такжеApi
, используйте первый для завершения авторизации для получения токена и используйте второй для переноса токена для вызова OpenAPI для получения информации о пользователе.
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
* @desc QQServiceProvider 对接服务提供商,封装一整套授权登录流程, 从用户点击第三方登录按钮到掉第三方应用OpenAPI获取Connection(用户信息)
* 委托 {@link OAuth2Operations} 和 {@link org.springframework.social.oauth2.AbstractOAuth2ApiBinding}来完成整个流程
*/
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQApiImpl> {
/**
* 当前应用在服务提供商注册的应用id
*/
private String appId;
/**
* @param oauth2Operations 封装逻辑: 跳转到认证服务器、用户授权、获取授权码、获取token
* @param appId 当前应用的appId
*/
public QQServiceProvider(OAuth2Operations oauth2Operations, String appId) {
super(oauth2Operations);
this.appId = appId;
}
@Override
public QQApiImpl getApi(String accessToken) {
return new QQApiImpl(accessToken,appId);
}
}
ConnectionFactory
UserInfo
, который инкапсулирует информацию о пользователе, возвращаемую OpenAPI.
package top.zhenganwen.security.core.social.qq;
import lombok.Data;
import java.io.Serializable;
/**
* @author zhenganwen
* @date 2019/9/4
* @desc QQUserInfo 用户在QQ应用注册的信息
*/
@Data
public class QQUserInfo implements Serializable {
/**
* 返回码
*/
private String ret;
/**
* 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
*/
private String msg;
/**
*
*/
private String openId;
/**
* 不知道什么东西,文档上没写,但是实际api返回里有。
*/
private String is_lost;
/**
* 省(直辖市)
*/
private String province;
/**
* 市(直辖市区)
*/
private String city;
/**
* 出生年月
*/
private String year;
/**
* 用户在QQ空间的昵称。
*/
private String nickname;
/**
* 大小为30×30像素的QQ空间头像URL。
*/
private String figureurl;
/**
* 大小为50×50像素的QQ空间头像URL。
*/
private String figureurl_1;
/**
* 大小为100×100像素的QQ空间头像URL。
*/
private String figureurl_2;
/**
* 大小为40×40像素的QQ头像URL。
*/
private String figureurl_qq_1;
/**
* 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。
*/
private String figureurl_qq_2;
/**
* 性别。 如果获取不到则默认返回”男”
*/
private String gender;
/**
* 标识用户是否为黄钻用户(0:不是;1:是)。
*/
private String is_yellow_vip;
/**
* 标识用户是否为黄钻用户(0:不是;1:是)
*/
private String vip;
/**
* 黄钻等级
*/
private String yellow_vip_level;
/**
* 黄钻等级
*/
private String level;
/**
* 标识是否为年费黄钻用户(0:不是; 1:是)
*/
private String is_yellow_year_vip;
}
ApiAdapter
, который преобразует различные форматы данных с информацией о пользователе, возвращаемые различными сторонними приложениями, в единое пользовательское представление.
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
* @desc QQConnectionAdapter 从不同第三方应用返回的不同用户信息到统一用户视图{@link org.springframework.social.connect.Connection}的适配
*/
@Component
public class QQConnectionAdapter implements ApiAdapter<QQApiImpl> {
// 测试OpenAPI接口是否可用
@Override
public boolean test(QQApiImpl api) {
return true;
}
/**
* 调用OpenAPI获取用户信息并适配成{@link org.springframework.social.connect.Connection}
* 注意: 不是所有的社交应用都对应有{@link org.springframework.social.connect.Connection}中的属性,例如QQ就不像微博那样有个人主页
* @param api
* @param values
*/
@Override
public void setConnectionValues(QQApiImpl api, ConnectionValues values) {
QQUserInfo userInfo = api.getUserInfo();
// 用户昵称
values.setDisplayName(userInfo.getNickname());
// 用户头像
values.setImageUrl(userInfo.getFigureurl_2());
// 用户个人主页
values.setProfileUrl(null);
// 用户在社交平台上的id
values.setProviderUserId(userInfo.getOpenId());
}
// 此方法作用和 setConnectionValues 类似,在后续开发社交账号绑定、解绑时再说
@Override
public UserProfile fetchUserProfile(QQApiImpl api) {
return null;
}
/**
* 调用OpenAPI更新用户动态
* 由于QQ OpenAPI没有此功能,因此不用管(如果接入微博则可能需要重写此方法)
* @param api
* @param message
*/
@Override
public void updateStatus(QQApiImpl api, String message) {
}
}
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);
}
}
createConnectionFactory
нам нужно переписатьSocialAutoConfigurerAdapter
серединаcreateConnectionFactory
метод для ввода нашего пользовательскогоConnectionFacory
, SpringSoical будет использовать его для выполнения шагов 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
protected ConnectionFactory<?> createConnectionFactory() {
return new QQConnectionFactory(
securityProperties.getQq().getProviderId(),
new QQServiceProvider(oAuth2Operations(), securityProperties.getQq().getAppId()),
qqConnectionAdapter);
}
@Bean
public OAuth2Operations oAuth2Operations() {
return new OAuth2Template(
securityProperties.getQq().getAppId(),
securityProperties.getQq().getAppSecret(),
URL_TO_GET_AUTHORIZATION_CODE,
URL_TO_GET_TOKEN);
}
}
QQSecurityProperties
, Элементы конфигурации, связанные с входом в систему QQ
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";
}
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();
}
UsersConnectionRepository
Нам нужна таблица для поддержания соответствия между текущей системной пользовательской таблицей и информацией, зарегистрированной пользователем в стороннем приложении, SpringSocial предоставляет нам эту таблицу (вJdbcUsersConnectionRepository.java
файл в том же каталоге)
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);
вuserId
Это уникальный идентификатор текущего пользователя системы (не обязательно первичный ключ пользовательской таблицы, но также и имя пользователя, если это поле в пользовательской таблице, которое может однозначно идентифицировать пользователя),providerId
используется для идентификации сторонних приложений,providerUserId
является идентификатором пользователя в стороннем приложении. Эти три поля могут идентифицировать пользователя (userId), соответствующего пользователю стороннего приложения (providerId) (providerUserId) в текущей системе. Мы выполняем следующий SQL в базе данных, соответствующей источнику данных.
SpringSocial предоставляет намJdbcUsersConnectionRepository
В качестве DAO этой таблицы нам нужно внедрить в нее источник данных текущей системы и наследоватьSocialConfigurerAdapter
и добавить@EnableSocial
Чтобы включить автоматическую настройку 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 // 父类会默认使用InMemoryUsersConnectionRepository作为实现,我们要使用@Primary告诉容器只使用我们这个
@Override
public JdbcUsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
// 使用第三个参数可以对 token 进行加密存储
return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
}
}
SocialAuthenticationFilter
Процесс использования стороннего логина фактически аналогичен процессу аутентификации имени пользователя и пароля. Просто последний основан на имени пользователя, введенном пользователем для поиска пользователя в пользовательской таблице, в то время как первый должен пройти через процесс OAtuh, чтобы получить пользователя в стороннем приложении.providerUserId
, то согласноproviderId
а такжеproviderUserId
прибытьUserConnection
запрос, соответствующий таблицеuserId
, и, наконец, согласноuserId
Запросить пользователей в таблице пользователей
Так что нам также нужно включить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) {
// 使用第三个参数可以对 token 进行加密存储
return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
}
// 该bean是联合登录配置类,和我们之前所写的SmsLoginConfig和VerifyCodeValidatorConfig的
// 的作用是一样的,只不过它是增加一个SocialAuthenticationFilter到过滤器链中
@Bean
public SpringSocialConfigurer springSocialConfigurer() {
return new SpringSocialConfigurer();
}
}
SecurityBrowserConfig
@Override
protected void configure(HttpSecurity http) throws Exception {
// 启用验证码校验过滤器
http.apply(verifyCodeValidatorConfig);
// 启用短信登录过滤器
http.apply(smsLoginConfig);
// 启用QQ登录(将SocialAuthenticationFilter加入到Security过滤器链中)
http.apply(springSocialConfigurer);
...
appId & appSecret & providerId
Поскольку каждая система применяетсяappId
а такжеappSecret
отличаются, поэтому мы извлекли его в файл конфигурации
demo.security.qq.appId=YOUR_APP_ID #替换成你的appId
demo.security.qq.appSecret=YOUR_APP_SECRET #替换成你的appSecret
demo.security.qq.providerId=qq
Правила настройки URL-адреса федеративного входа
Нам нужно предоставить совместную ссылку для входа QQ на странице входа, запрос/auth/qq
<a href="/auth/qq">qq登录</a>
первый путь/auth
должно бытьSocialAuthenticationFilter
Блокировать по умолчанию/auth
Запрос
SocialAuthenticationFilter
private static final String DEFAULT_FILTER_PROCESSES_URL = "/auth";
Второй путь требует иproviderId
быть последовательным, в то время как наши настроенныеdemo.security.qq.provider-id
для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;
}
URL-адрес федеративного входа должен совпадать с доменом обратного вызова.
Теперь мы осознали различные компоненты SpringSocial, но можно ли их связать вместе, чтобы пройти весь процесс, мы можем попробовать его и дополнительно понять процесс социальной сертификации в процессе пошагового устранения неполадок.
доступ/login.html
, нажмитеqq登录
Ответ на пост такой
Подскажите что адрес обратного звонка неправильный, можем посмотреть в адресной строкеredirect_url
параметр
После перекодирования это фактическиhttp://localhost:8080/auth/qq
, то есть, если пользователь соглашается на авторизацию, браузер будет перенаправлен на URL-адрес федеративного входа.
И домен обратного вызова, который я заполнил при подаче заявки в QQ Internet,www.zhenganwen.top/socialLogin/qq
(Как показано на рисунке ниже), для совместного входа в QQ требуется, чтобы URL-адрес, на который перенаправляются после того, как пользователь соглашается авторизоваться, должен соответствовать домену обратного вызова, указанному при подаче заявки на appId, то есть URL-адресу совместного входа на странице. должен соответствовать домену обратного вызова.
Во-первых, доменное имя и порт должны быть согласованы:
Поскольку это локальный сервер, нам нужно изменить локальныйhosts
Файл, чтобы браузер разобралwww.zhenganwen.top
При анализе172.0.0.1
:
127.0.0.1 www.zhenganwen.top
и измените порт службы на80
server.port=80
Таким образом, доменное имя и порт могут быть сопоставлены и могут быть переданы черезwww.zhenganwen.top/login.html
Посетите страницу входа.
Во-вторых, также необходимо сопоставить URI совместного входа с установленным нами доменом обратного вызова,/auth
изменить на/socialLogin
, Вам нужно настроитьSocialAuthenticationFilter
изfilterProcessesUrl
Стоимость имущества:
новый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;
}
Исправлять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();
}
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
QQLoginAutoConfig
Синхронная модификация
@Component
//@ConditionalOnProperty(prefix = "demo.security.qq",name = "appId")
@ConditionalOnProperty(prefix = "demo.security.social.qq",name = "appId")
public class QQLoginAutoConfig extends SocialAutoConfigurerAdapter {
расширятьSpringSocialConfigurer
, через функцию ловушкиpostProcess
для достиженияSocialAuthenticationFilter
некоторые пользовательские конфигурации, такие как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;
}
}
существуетSocialConfig
Введите расширенныйSpringSocialConfigurer
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public JdbcUsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
// 使用第三个参数可以对 token 进行加密存储
return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
}
// @Bean
// public SpringSocialConfigurer springSocialConfigurer() {
// return new SpringSocialConfigurer();
// }
@Bean
public SpringSocialConfigurer qqSpringSocialConfigurer() {
QQSpringSocialConfigurer qqSpringSocialConfigurer = new QQSpringSocialConfigurer();
return qqSpringSocialConfigurer;
}
}
Причина этогоpostProcess()
является функцией ловушки, вSecurityConfigurerAdapter
изconfig
метод, вSocialAuthenticationFilter
Вызывается при добавлении в цепочку фильтровpostProcess
, позволяя подклассам переопределять этот метод дляSocialAuthenticationFilter
Выполните пользовательскую настройку:
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 = new SocialAuthenticationFilter(
http.getSharedObject(AuthenticationManager.class),
userIdSource != null ? userIdSource : new AuthenticationNameUserIdSource(),
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);
}
}
Одновременно изменить страницу входа
<a href="/socialLogin/qq">qq登录</a>
В то же время необходимо отключить перехват URL-адреса совместного входа в класс конфигурации совместного входа.
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();
}
}
доступwww.zhenganwen.top/login.html
, нажмитеqq登录
Найдите прыжок следующим образом
Логика скачка авторизации проходит! Код этого этапа можно найти в:git ee.com/beginning/…
Периодическая сводка
Разрешение домена обратного вызова
Вы используете службу на локальном порту 80, почему сервер аутентификации может разрешить домен обратного вызова?www.zhenganwen.top/socialLogin/qq
чтобы перейти к вашему местному
Обратите внимание на адресную строку на странице авторизации выше, URL-адрес содержитredirect_url
Этот параметр, поэтому, когда вы соглашаетесь авторизовать вход, переходите кredirect_url
Операция со значением параметра выполняется в вашем браузере, и выhosts
настроен в127.0.0.1 www.zhenganwen.top
, поэтому браузер напрямую запрашивает запрос, не выполняя разрешение доменного имени./socialLogin/qq
отправлено в127.0.0.1:80
, это то, что мы запускаемsecurity-demo
Служить
Какова роль SpringSoicalConfigure?
Перейдите непосредственно к исходному коду:
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 = new SocialAuthenticationFilter(
http.getSharedObject(AuthenticationManager.class),
userIdSource != null ? userIdSource : new AuthenticationNameUserIdSource(),
usersConnectionRepository,
authServiceLocator);
...
http.authenticationProvider(
new SocialAuthenticationProvider(usersConnectionRepository, socialUsersDetailsService))
.addFilterBefore(postProcess(filter), AbstractPreAuthenticatedProcessingFilter.class);
}
}
Если мы хотим применить все компоненты SpringSoical, написанные ранее, мы должны следовать механизму аутентификации SpringSecurity, то есть, чтобы добавить новый метод аутентификации, нам нужно добавитьXxxAuthenticationFilter
, и SpringSoical помог нам достичьSocialAuthenticationFilter
, поэтому нам просто нужно добавить его в фильтр. Поскольку мы ранее упаковали SMS-вход вSmsLoginConfig
Например, SpringSocial помогает нам инкапсулировать вход через социальные сети вSpringSocialConfigure
, так что до тех пор, пока бизнес-система (то есть приложение, которое зависит от SpringSocial) должна вызывать толькоhttpSecurity.apply(springSocialConfigure)
чтобы включить социальный вход.
и в дополнение кSoicalAuthenticationFilter
добавлен в цепочку фильтров,SpringSocialConfigure
также поместится в контейнерUsersConnectionRepository
а такжеSocialAuthenticationServiceLocator
относится кSoicalAuthenticationFilter
середина,SoicalAuthenticationFilter
Социальная информация, которую можно получить в соответствии с процессом OAuth через первый (providerId
а такжеproviderUserId
)ЗапрошеноuserId
, через который последний может бытьproviderId
получить соответствующийSocialAuthenticationService
И получить его от этогоConnectionFactory
Чтобы получить код авторизации, получитеaccessToken
, получить социальную информацию пользователя и т. д.
public interface UsersConnectionRepository {
List<String> findUserIdsWithConnection(Connection<?> connection);
}
public interface SocialAuthenticationServiceLocator extends ConnectionFactoryLocator {
SocialAuthenticationService<?> getAuthenticationService(String providerId);
}
public interface SocialAuthenticationService<S> {
ConnectionFactory<S> getConnectionFactory();
SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException;
}
Почему у вас есть SocialAuthenticationService и когда он появился?
SocialAuthenticationService
правдаConnectionFactory
пакет, правильноSocialAuthenticationFilter
Скрыть детали вызовов OAuth и OpenAPI
потому что мыSocialConfig
добавлено в@EnableSocial
, поэтому при запуске системы будетSocialAutoConfigurerAdapter
в классе реализацииcreateConnectionFactory
Создавайте соответствующие разным социальным системамConnectionFactory
и упаковать какSocialAuthenticationService
, то поставить всеSocialAuthenticationService
кproviderId
дляkey
кэшируется в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
protected ConnectionFactory<?> 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);
}
}
class SecurityEnabledConnectionFactoryConfigurer implements ConnectionFactoryConfigurer {
private SocialAuthenticationServiceRegistry registry;
public SecurityEnabledConnectionFactoryConfigurer() {
registry = new SocialAuthenticationServiceRegistry();
}
public void addConnectionFactory(ConnectionFactory<?> connectionFactory) {
registry.addAuthenticationService(wrapAsSocialAuthenticationService(connectionFactory));
}
public ConnectionFactoryRegistry getConnectionFactoryLocator() {
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");
}
}
public class SocialAuthenticationServiceRegistry extends ConnectionFactoryRegistry implements SocialAuthenticationServiceLocator {
private Map<String, SocialAuthenticationService<?>> authenticationServices = new HashMap<String, SocialAuthenticationService<?>>();
public SocialAuthenticationService<?> 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<SocialAuthenticationService<?>> authenticationServices) {
for (SocialAuthenticationService<?> authenticationService : authenticationServices) {
addAuthenticationService(authenticationService);
}
}
public Set<String> registeredAuthenticationProviderIds() {
return authenticationServices.keySet();
}
}
так когдаSocialAuthenticationFilter
перехвачен/{filterProcessingUrl}/{providerId}
После этого он будет основан на URL-адресе в путиproviderId
прибытьSocialAuthenticationLocator
найти соответствующийSocialAuthenticationService
ПолучатьauthRequest
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");
}
}
return auth;
}
}
Почему URL-адрес входа в социальную сеть и домен обратного вызова должны быть согласованы
SocialAuthenticationFilter#attemptAuthService
private Authentication attemptAuthService(final SocialAuthenticationService<?> 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;
}
}
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;
}
}
Можно обнаружить, что пользователь также нажимает на логинqq
залогинилсяSocialAuthenticationFilter
перехватить, войти в вышеуказанноеgetAuthToken
метод, параметр запроса не содержит кода авторизации, поэтому первый9
Строка вызовет исключение, которое будет перехвачено обработчиком ошибок аутентификации и направлено на сервер аутентификации социальной системы.
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);
}
}
}
После того, как пользователь соглашается на авторизацию, сервер аутентификации переходит к домену обратного вызова и вводит код авторизации, а затем вводитgetAuthToken
Первый11
Хорошо, получите код авторизацииaccessToken
(AccessGrant
), вызовите OpenAPI, чтобы получить информацию о пользователе и адаптировать ее кConnection
Почему ответ после согласия на авторизацию выглядит следующим образом
Сканируем QR-код для согласия на авторизацию, браузер перенаправляет на/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;
}
}
В первой части вышеуказанного пояса12
Точка останова строки для трассировки, найти выполнение13
бросать исключение при переходе к18
Хорошо, информация об исключении выглядит следующим образом:
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]
Описание зовет насOAuth2Template
изexchangeForAccess
Получить код авторизацииaccessToken
Сообщается об ошибке, причина ошибки в том, что результат ответа преобразованияAccessGrant
не обработаноtext/html
преобразователь.
Сначала давайте посмотрим, что ответит:
найти, что результатом ответа является строка, начинающаяся с&
разбивает три пары ключевых значений, аOAuth2Template
Преобразователи, предоставляемые по умолчанию, следующие:
OAuth2Template
protected RestTemplate createRestTemplate() {
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactorySelector.getRequestFactory();
RestTemplate restTemplate = new RestTemplate(requestFactory);
List<HttpMessageConverter<?>> converters = new ArrayList<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;
}
см. выше5~7
линия из 3 преобразователей,FormHttpMessageConverter
,FormMapHttpMessageConverter
,MappingJackson2HttpMessageConverter
соответствующий анализContent-Type
дляapplication/x-www-form-urlencoded
,multipart/form-data
,application/json
Тело ответа , поэтому сообщается сообщение об ошибке
no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html]
В это время нам необходимоOAuth2Template
На основе добавления обработкиtext/html
преобразователь:
public class QQOAuth2Template extends OAuth2Template {
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);
}
/**
* 添加消息转换器以使能够解析 Content-Type 为 text/html 的响应体
* StringHttpMessageConverter 可解析任何 Content-Type的响应体,见其构造函数
* @return
*/
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
/**
* 如果响应体是json,OAuth2Template会帮我们构建, 但QQ互联的OpenAPI返回包都是 text/html 字符串
* 响应体 : "access_token=FE04***********CCE2&expires_in=7776000&refresh_token=88E4********BE14"
* 使用 StringHttpMessageConverter 将请求的响应体转成 String ,并手动构建 AccessGrant
* @param accessTokenUrl 拿授权码获取accessToken的URL
* @param parameters 请求 accessToken 需要附带的参数
* @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], "=")));
return accessGrant;
}
}
использовать этотQQOAuth2Template
заменить ранее введенныйOAuth2Template
@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
protected ConnectionFactory<?> 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() {
return new QQOAuth2Template(
securityProperties.getSocial().getQq().getAppId(),
securityProperties.getSocial().getQq().getAppSecret(),
URL_TO_GET_AUTHORIZATION_CODE,
URL_TO_GET_TOKEN);
}
}
Теперь мы можем получить пакетaccessToken
изAccessGrant
, а затем продолжить отладку конечной точкиConnection
приобретение (см. ниже15
Ряд)
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;
}
}
ОбнаружитьQQApiImpl
изgetUserInfo
Есть та же проблема, тип ответа на вызов API взаимосвязи QQtext/html
, поэтому мы не можем напрямую конвертировать в POJO, но сначала получаем строку ответа, а затем передаем класс инструмента преобразования JSONObjectMapper
для преобразования:
QQApiImpl
@Override
public QQUserInfo getUserInfo() {
// QQ互联的响应 Content-Type 都是 text/html,因此不能直接转为 QQUserInfo
// 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("调用QQ OpenAPI获取用户信息: {}", responseStr);
try {
QQUserInfo qqUserInfo = objectMapper.readValue(responseStr, QQUserInfo.class);
qqUserInfo.setOpenId(openId);
return qqUserInfo;
} catch (Exception e) {
logger.error("获取用户信息转成 QQUserInfo 失败,响应信息:{}", responseStr);
return null;
}
}
Просканируйте код еще раз, чтобы войти в систему для отладки точки останова, и обнаружите, чтоConnection
также могут быть успешно получены и упакованы вSocialAuthenticationToken
вернуться, так чтоgetAuthToken
Наконец благополучно вернулся, пошелdoAuthentication
SocialAuthenticationFilter
private Authentication attemptAuthService(final SocialAuthenticationService<?> 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));
}
throw e;
}
}
позвонюProviderManager
изauthenticate
правильноSocialAuthenticationToken
проверить,ProviderManager
снова будет введена в эксплуатациюSocialAuthenticationProvider
SocialAuthenticationProvider
позвонит нашему инъецированномуJdbcUsersConnectionRepository
прибытьUserConnection
По таблицеConnection
изproviderId
а такжеproviderUserId
найти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;
}
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);
return Arrays.asList(newUserId);
}
}
return localUserIds;
}
Поскольку его невозможно найти (потому что в это время нашUserConnection
В таблице вообще нет данных),toUserId
Вернусьnull
, затем бросаетBadCredentialsException("Unknown access token")
, исключение будетSocialAuthenticationFilter
захват и по егоsignupUrl
Атрибут для перенаправления (SpringSocial считает, что пользователь не зарегистрирован в системе или зарегистрирован, но не связывает локального пользователя с логином QQ, поэтому переходите на страницу регистрации)
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));
}
throw e;
}
}
а такжеSocialAuthenticationFilter
изsignupUrl
По умолчанию/signup
public class SocialAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String signupUrl = "/signup";
}
перенаправить на/signup
При перехвате SpringSecurity и перенаправлении наloginPage()
, наконец, прибылBrowserSecurityController
SecurityBrowserConfig
.formLogin()
.loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
SecurityConstants
/**
* 未登录访问受保护URL则跳转路径到 此
*/
String FORWARD_TO_LOGIN_PAGE_URL = "/auth/require";
BrowserSecurityController
@RestController
public class BrowserSecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
// security会将跳转前的请求存储在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("引发跳转到/auth/login的请求是: {}", redirectUrl);
if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) {
// 如果用户是访问html页面被FilterSecurityInterceptor拦截从而跳转到了/auth/login,那么就重定向到登录页面
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
// 如果不是访问html而被拦截跳转到了/auth/login,则返回JSON提示
return new SimpleResponseResult("用户未登录,请引导用户至登录页");
}
}
В итоге получил следующий ответ:
что сделал @EnableSocial
он загружает класс конфигурацииSocialConfiguration
, который читает контейнерSocialConfigure
например, как мы написали расширениеSocialAutoConfigureAdapter
изQQLoginAutoConfig
и расширилSocialConfigureAdapter
изSocialConfig
, мы достигнемConnectionFactory
а такжеUsersConnectionRepository
а такжеSpringSecurity
Строка процесса сертификации вместе
/**
* 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() {
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();
}
}
@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.");
return usersConnectionRepository;
}
}
Страница регистрации и привязка социальных аккаунтов
Во-первых, можно настроить URL-адрес страницы регистрации, и по умолчанию установлено значение/sign-up.html
, и интерфейс службы, который обрабатывает регистрацию/user/register
@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;
}
Затем отпустите этот путь в классе конфигурации браузера:
@Autowired
private SpringSocialConfigurer qqSpringSocialConfigurer;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 启用验证码校验过滤器
http.apply(verifyCodeValidatorConfig).and()
// 启用短信登录过滤器
.apply(smsLoginConfig).and()
// 启用QQ登录
.apply(qqSpringSocialConfigurer).and()
// 启用表单密码登录过滤器
.formLogin()
.loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
// 浏览器应用特有的配置,将登录后生成的token保存在cookie中
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(3600)
.userDetailsService(customUserDetailsService)
.and()
// 浏览器应用特有的配置
.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();
}
Наконец напишите страницу регистрации:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>标准注册页</h1>
<a href="/social">QQ账号信息</a>
<form action="/user/register" method="post">
用户名: <input type="text" name="username" value="admin">
密码: <input type="password" name="password" value="123">
<button type="submit" name="type" value="register">注册并关联QQ登录</button>
<button type="submit" name="type" value="binding">已有账号关联QQ登录</button>
</form>
</body>
</html>
ProviderSignInUtils
Регистрационная служба: Хотя потому что вUserConnection
В таблице нет записи, связанной с локальным пользователем и переход на страницу регистрации, но полученныйConnection
или сохранить вSession
, если вы хотите автоматически связать учетную запись QQ, когда пользователь щелкает, чтобы зарегистрировать локальную учетную запись, или если у пользователя уже есть локальная учетная запись и вручную связать учетную запись QQ, вы можете использоватьProviderSignInUtils
Этот класс инструментов, вам нужно только указать локальную учетную запись, которая должна быть связанаuserId
, он автоматически вытащитSession
сохранено вConnection
, и воляuserId
,Connection.getProviderId
,Connection.getProviderUserId
Вставьте его в базу данных как запись, чтобы пользователь не перескакивал на страницу регистрации локальной учетной записи при следующем входе в QQ.
@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("新增用户并关联QQ登录, 用户名:{}", username);
userService.insertUser();
} else if ("binding".equalsIgnoreCase(type)) {
logger.info("给用户关联QQ登录, 用户名:{}", username);
}
providerSignInUtils.doPostSignUp(username, new ServletWebRequest(request));
return "success";
}
}
Поддержка привязки/отвязки сцены
Иногда модулю управления учетными записями нашей системы необходимо разрешить пользователям связывать или отвязывать некоторые учетные записи социальных сетей, и SpringSocial также поддерживает этот сценарий (см.ConnectController
). Вам нужно только настроить связанные с представлением компоненты (ExtensibleAbstractView
"Функция "Привязать/Решение" может быть реализована.
Управление сеансом
Автономное управление сессиями
Фактически, настроенный нами процесс входа в систему будет выполняться только один раз при входе в систему. После успешного входа в систему будет сгенерирована информация аутентификации инкапсуляции.Authentication
Хранится в локальном безопасном потоке, и последующие операции, такие как доступ пользователя к защищенным URL-адресам, не будут включать эти компоненты в процесс входа в систему.
Давайте еще раз вспомним цепочку фильтров Spring Security, первая из нихSecurityContextPersistenceFilter
, который используется, чтобы попытаться прочитать информацию об аутентификации, сгенерированную после успешного входа в сеанс из сеанса при получении запроса, и поместить его в текущий безопасный поток, а затем извлечь его и поместить в сеанс при ответе на запрос, в то время как один расположен в конце цепочки фильтровFilterSecurityInterceptor
будет в гостяхController
Информация об аутентификации в потокобезопасной версии проверяется перед службой, поэтому управление сеансом будет напрямую влиять на то, сможет ли пользователь продолжать доступ к защищенному URL-адресу в этот момент.
В SpringBoot мы можем передать элемент конфигурацииserver.session.timeout
(в секундах), чтобы установить эффективную продолжительность сеанса, чтобы, если пользователь все еще обращался к защищенному URL-адресу после входа в систему в течение определенного периода времени, ему нужно было снова войти в систему.
Соответствующий код находится наTomcatEmbeddedServletContainerFactory
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(new DisablePersistSessionListener());
}
}
private long getSessionTimeoutInMinutes() {
long sessionTimeout = getSessionTimeout();
if (sessionTimeout > 0) {
sessionTimeout = Math.max(TimeUnit.SECONDS.toMinutes(sessionTimeout), 1L);
}
return sessionTimeout;
}
SpringBoot преобразует настроенные вами секунды в минуты, поэтому вы обнаружите, что настройкиserver.session.timeout=10
Но нашел только 1 минуту Сбой сеанса привел к необходимости повторного входа в систему.
application.properties
server.session.timeout=10 #设置Session 10秒后过期
Но мы обычно настроены на несколько часов.
В отличие от доступа к защищенному URL-адресу без входа в систему, должно быть другое приглашение, когда сеанс недействителен и доступ к защищенному URL-адресу невозможен (например: срок действия сеанса, в который вы вошли, истек из-за бездействия в течение длительного времени, пожалуйста, войдите в систему еще раз; вам не следует предлагать Если вы не вошли в систему, сначала войдите), затем мы можем настроитьhttp.sessionManage().invalidSessionUrl()
чтобы указать, что пользователь вошел в систему болееserver.session.timeout
По истечении заданного периода времени пользователь будет перенаправлен на защищенный URL-адрес при посещении защищенного URL-адреса.Вы можете настроить страницу илиController
чтобы предложить пользователю и направить пользователя на страницу входа
SecurityBrowserConfig
protected void configure(HttpSecurity http) throws Exception {
// 启用验证码校验过滤器
http.apply(verifyCodeValidatorConfig).and()
// 启用短信登录过滤器
.apply(smsLoginConfig).and()
// 启用QQ登录
.apply(qqSpringSocialConfigurer).and()
// 启用表单密码登录过滤器
.formLogin()
.loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
// 浏览器应用特有的配置,将登录后生成的token保存在cookie中
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(3600)
.userDetailsService(customUserDetailsService)
.and()
.sessionManagement()
.invalidSessionUrl("/session-invalid.html")
.and()
// 浏览器应用特有的配置
.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();
}
существует.sessionManagement()
В конфигурации:
пройти через.maximumSessions
Он может контролировать количество сеансов, в которые пользователь может войти одновременно.Если установлено значение 1, последний вошедший в систему человек вытеснит предыдущего вошедшего в систему человека. ,пройти черезexpiredSessionStrategy
Вы можете установить метод обратного вызова для этого события (вызывается, когда предыдущий человек выжимается, а затем обращается к защищенному URL-адресу), который можно получить через параметр обратного вызова.request
а такжеresponse
пройти через.maxSessionsPreventsLogin(true)
Можно установить, что если пользователь уже вошел в систему, он не может войти снова в другие сеансы.timeout
Если настройки неверны или вторичный логин заблокирован, вы можете пройти.invalidSessionStrategy()
Настройка стратегии обработки
Управление сеансом кластера
Для достижения высокой доступности и высокого параллелизма приложения корпоративного уровня обычно развертывают службы в кластерах и перенаправляют запросы к определенным службам через шлюз или прокси в соответствии с алгоритмом опроса.В настоящее время, если каждая служба управляет своим сеансом отдельно, затем пользователю неоднократно предлагается войти в систему. Мы можем извлечь управление сеансом и сохранить его в отдельной системе,spring-session
Проект может сделать всю работу за нас, нам просто нужно указать ему, какую систему хранения использовать для хранения сеанса.
Обычно мы используемRedis
сохранить сеанс без использованияMysql
, по следующим причинам:
-
SpringSecurity
Будет с каждого запросаSession
Информация об аутентификации считывается в середине, поэтому чтение происходит чаще, а скорость использования системы кэширования выше. -
Session
действительное время, если хранится вMysql
Его также необходимо регулярно чистить, иRedis
Имеет собственную своевременность данных кеша
Установить Redis
Официальный сайт, скачать и скомпилировать
$ wget http://download.redis.io/releases/redis-5.0.5.tar.gz
$ tar xzf redis-5.0.5.tar.gz
$ cd redis-5.0.5
$ make MALLOC=libc
Если вам будет предложено, что соответствующая команда не может быть найдена, вам необходимо установить соответствующие зависимости.
yum install -y gcc g++ gcc-c++ make
Запустите службу:
./src/redis-server
Так как я в виртуальной машинеCentOS6.5
установлен вRedis
Механизм защиты по умолчанию разрешает только локальный доступ, если вы хотите получить доступ к хосту или внешней сети, вам необходимо настроить./redis.conf
, Добавитьbind 192.168.102.2
(Мой IP-адрес хоста в локальной сети) позволяет хосту получить доступ к IP-адресу, что эквивалентно добавлению белого списка IP-адресов.Если вы хотите, чтобы все хосты могли получить доступ к службе, вы можете настроитьbind 0.0.0.0
После изменения конфигурации необходимо указать чтение файла конфигурации при запуске, чтобы элементы конфигурации вступили в силу:./src/redis-server ./redis.conf &
Конфигурационный файл SpringBoot
существуетapplication.properties
новое вspring.redis.host=192.168.102.101
, который можно указатьSpringBoot
подключен к этому хосту при запускеRedis
(порт по умолчанию 6379) и исключить предыдущийRedis
Автоматическое удаление аннотаций интеграции
//@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() {
return "hello spring security";
}
}
В конфигурационном файле всегда указывайте, чтоSession
Хостинг вRedis
spring.session.store-type=redis
spring.redis.host=192.168.102.101
Поддерживаемые управляемые типы инкапсулированы вorg.springframework.boot.autoconfigure.session.StoreType
середина.
После использования кластерного режима ранее настроенныйtimeout
а такжеhttp.sessionManagement()
все еще в силе.
Примечание. После размещения сеанса в системе хранения убедитесь, что bean-компоненты, записанные в сеанс, сериализуемы, т. е. реализованы
Serializable
интерфейс, если свойства в bean-компоненте не могут быть сериализованы, например.ImageCode
серединаBufferedImage image
Если вам не нужно хранить его на сеансе, вы можете установить это свойство вnull
@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); }
выход
Как выйти из системы
Security
Предоставляет нам сервис, который по умолчанию выходит из системы текущего пользователя./logout
, по умолчанию он будет делать следующие 3 вещи:
- делать текущим
Session
неверный - Чисто
remember-me
Информация о функции - Чисто
SecurityContext
содержание в
мы можем пройтиhttp.logout()
настроить логику выхода из системы
-
logoutUrl()
, указав URL-адрес, запрошенный операцией выхода -
logoutSuccessUrl()
, URL-адрес для перехода после завершения выхода -
logoutSuccessHandler()
, обработчик, вызываемый после завершения выхода из системы, который может динамически реагировать на страницу или JSON в соответствии с типом запроса пользователя. -
deleteCookies()
,согласно сkey
удалятьCookie
серединаitem
Spring Security OAuth разрабатывает инфраструктуру аутентификации приложений
Все, что мы говорили ранее, основано наB/S
Архитектурный, то есть доступ пользователей к нашим сервисам напрямую через браузер, основан наSession/Cookie
из. Но сейчас архитектура передней и задней части сепарации более популярна, пользователь может быть непосредственно доступа к приложению или веб-серверу (например,nodejs
), а APP и WebServer затем передаютajax
Вызов серверных служб в этом сценарииSession/Cookie
Шаблоны имеют много недостатков
- Разработка обременительна и требует частого таргетинга
Session/Cookie
Для операций чтения и записи запрос, отправленный из браузера, будет сохранен вCookie
серединаJSESSIONID
, серверная часть может найти соответствующийSession
, в ответ наJSESSIONID
записыватьCookie
. Если браузер отключенCookie
Вам нужно прикрепить его к каждому URLJSESSIONID
параметр - Плохая безопасность и качество обслуживания клиентов, конфиденциальные данные хранятся на стороне клиента.
Cookie
не очень безопасно,Session
Неправильные настройки, такие как управление старением и распределенное управление, приведут к частым повторным входам пользователей в систему, что приведет к ухудшению взаимодействия с пользователем. - Некоторые интерфейсные технологии вообще не поддерживаются
Cookie
, такие как приложение, апплет
В качестве таких,Spring Security OAuth
обеспечивает на основеtoken
Механизм аутентификации заключается не в том, чтобы каждый раз считывать аутентификационную информацию, хранящуюся в сеансе, а в том, чтобы выдаватьtoken
, просто принесиtoken
параметры. по сравнению с основанным наSession
Путь,token
более гибким и безопасным, неSession
Такой жеSESSIONID
Распределение и вложения параметров затвердевают.token
В какой форме и какая информация включена и доступна черезtoken
Механизм обновления прозрачно продлевает время авторизации (не воспринимаемое пользователем), чтобы избежать повторных входов в систему и т. д., и все это может быть настроено нами.
упомянулOAuth
, может быть легко подумать о сторонней функции входа в систему, разработанной ранее, на самом делеSpring Social
упакованOAuth
Процесс, через который хочет пройти клиент, иSpring Security OAuth
упакованOAuth
Связанные функции сервера аутентификации.
Что касается нашей самостоятельно разработанной системы, то серверная часть является сервером аутентификации и сервером ресурсов, тогда как внешнее приложение и веб-сервер эквивалентныOAuth
клиент.
Сервер аутентификации должен предоставить модель лицензирования и 4token
Генерация и хранение сервера ресурсов является защитойREST
Сервис, с помощью фильтра проверяйте наличие запроса в запросе перед вызовом сервисаtoken
. И все, что нам нужно сделать, это интегрировать нашу пользовательскую логику аутентификации (имя пользователя и пароль для входа, вход с кодом подтверждения SMS, сторонний вход) в сервер аутентификации и подключиться для создания и храненияtoken
.
Из этой главы мы будем использоватьSpring Security OAuth
развиватьsecurity-app
Проект, основанный на чистомOAuth
метод аутентификации, не полагаясь наSession/Cookie
Готов к работе
Сначала мыsecurity-demo
будет представлен вsecurity-browser
Зависимости закомментированы и введеныsecurity-app
Забудьте об этом раньшеSession/Cookie
Сертификационный код разработан, начиная с самого началаOAuth
разработать авторизацию аутентификации.
из-заsecurity-core
Фильтр проверки капчи вVerifyCodeValidateFilter
Обработчики успеха/неудачи аутентификации должны быть внедрены, поэтому мыsecurity-demo
скопировать вsecurity-app
, и отправить результат обработки в формате JSON (security-browser
Результатом обработки может быть страница, ноsecurity-app
может отвечать только в формате JSON) и будетSimpleResponseResult
въезжать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("登录失败=>{}", exception.getMessage());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponseResult(exception.getMessage())));
response.getWriter().flush();
}
}
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) {
// // 重定向到缓存在session中的登录前请求的URL
// super.onAuthenticationSuccess(request, response, authentication);
// }
logger.info("用户{}登录成功", authentication.getName());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
response.getWriter().flush();
}
}
Перезапустите службу, установите флажок, чтобы удалитьsecurity-browser
представлятьsecurity-app
Может ли проект нормально работать после этого.
Включить сервер аутентификации
Просто используйте аннотацию@EnableAuthorizationServer
Вы можете сделать текущую службу сервером аутентификации,starter-oauth2
Это уже помогло нам инкапсулировать 4 режима авторизации и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 {
}
Теперь мы можем протестировать режим авторизации в 4授权码
узор и密码
модель
Во-первых, на стороне сервера аутентификации должен быть пользователь, для удобства писать здесь не будем.DAO
а такжеUserDetailsService
Теперь мы можем добавить пользователя, настроив:
security.user.name=test
security.user.password=test
security.user.role=user # 要使用OAuth,用户需要有user角色,数据库中需存储为ROLE_USER
затем настройтеclientId/clientSecret
, что эквивалентно вызову других приложенийsecurity-demo
Перед выполнением стороннего входа в систему необходимоsecurity-demo
применяется для регистрации на платформе интернет-разработкиappId/appSecret
. Например, есть приложение вsecurity-demo
Проверка регистрации на платформе разработки пройдена,security-demo
назначитappId:test-client
а такжеappSecret:123
. Теперь нашsecurity-demo
Также становится сервером аутентификации, любой вызовsecurity-demo
Приобретение APItoken
Другие приложения можно рассматривать как сторонние приложения или клиенты.
security.oauth2.client.client-id=test-client
security.oauth2.client.client-secret=123
Далее мы можем сравнитьOAuth2
на официальном сайтеСправочная документацияпроверять@EnableAuthorizationServer
Предусмотрено 4 режима авторизации и получитеtoken
Тестовый режим кода авторизации
видетькритерии запроса
Режим кода авторизации состоит из двух шагов:
-
Получить код авторизации
Просмотрите журнал запуска загрузки и обнаружите, что фреймворк добавляет для нас несколько интерфейсов, в том числе
/oauth/authorize
, это интерфейс для получения кода авторизации. мы сравниваемOAuth2
получил код авторизации откритерии запросапопытаться получить код авторизацииhttp://localhost/oauth/authorize? response_type=code &client_id=test-client &redirect_uri=http://example.com &scope=all
в
response_type
исправлено какcode
Указывает на получение кода авторизации,client_id
для клиентаappId
,redirect_uri
Получение кода авторизации для клиента для дальнейшего полученияtoken
URL-адрес обратного вызова (здесь мы будем писать случайный, а URL-адрес, на который происходит успешное перенаправление авторизации, будет сопровождаться кодом авторизации),scope
Указывает объем полномочий, который необходимо получить для данной авторизации (значение значения ключа и значение ключа должно определяться сервером аутентификации, здесь мы пока напишем случайное). После посещения URL-адреса всплывающее окноbasic
Окно входа в систему аутентификации, мы вводим имя пользователяtest
парольtest
После входа в систему перейдите на страницу авторизации и спросите нас, следует ли предоставлятьall
Разрешения (в реальной разработке мы можем разделить разрешения наcreate
,delete
,update
,read
, которые также можно разделить наuser
,admin
,guest
Ждать):Нажимаем, чтобы согласиться
Approve
Затем нажмите АвторизоватьAuthorize
, затем переходит к URL-адресу обратного вызова с прикрепленным кодом авторизацииЗапишите код авторизации
yO4Y6q
для последующихtoken
Получать -
Получать
token
мы можем пройти
Chrome
плагинRestlet Client
чтобы выполнить этот запрос- нажмите
Add authorization
войтиclient-id
а такжеclient-secret
, инструмент автоматически зашифрует и прикрепит его к заголовку запроса для нас.Authorizatin
середина - Заполните параметры запроса
При использовании
Postman
ноAuthorization
Настройки следующие:нажмите
Send
Отправьте запрос, и ответ будет следующим: - нажмите
режим пароля
Режим пароля требует только одного шага, без кода авторизации, вы можете получить его напрямуюtoken
Использование режима пароля эквивалентно тому, что пользователь сообщает клиентуtest-client
пользователь вsecurity-demo
Зарегистрируйте имя пользователя и пароль, и клиент напрямую использует это для полученияtoken
, сервер аутентификации не знает, что клиент запрашивает после авторизации и согласия пользователяtoken
Или тайно возьмите известное имя пользователя и пароль, чтобы получитьtoken
, но если клиентское приложение является внутренним приложением компании, вам не нужно беспокоиться об этом.
Здесь есть еще одна деталь: потому что режим кода авторизации для пользователя был выдан доtoken
, так вот получается через режим пароляtoken
все еще генерируется раньшеtoken
, и срок годностиexpire_in
постепенное сокращение
В настоящее время не указано
token
метод хранения, поэтому он хранится в памяти по умолчанию.Если вы перезапустите службу, вам нужно применить сноваtoken
Включить сервер ресурсов
Аналогичным образом используйте@EnableResourceServer
Аннотация может сделать службу сервером ресурсов (проверьте перед вызовом службыtoken
)
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 {
}
После перезапуска службы получите прямой доступ к пользовательскому интерфейсу запроса./user
отклик401
Указывает, что сервер ресурсов работает (безtoken
доступ к защищенным сервисам заблокирован), это тоже неsecurity
По умолчаниюbasic
Аутентификация работает, потому что если онаbasic
Если его заблокировать, появится окно входа в систему, которого там нет.
Затем мы регенерируем его один раз, используя шаблон пароля.token:7f6c95fd-558f-4eae-93fe-1841bd06ea5c
, и прикрепляется при доступе к интерфейсуtoken
(добавить заголовок запросаAuthorization
значениеtoken_type access_token
)
использоватьPostman
Более удобный:
Анализ исходного кода ядра Spring Security Oauth
Основные компоненты фреймворка следующие: зеленое поле указывает на конкретный класс, а синее поле указывает на интерфейс/абстрактный классы в круглых скобках — это классы, которые фактически вызываются во время выполнения. Ниже мы密码模式
Например, чтобы проанализировать исходный код, вы также можете разбить точку для пошаговой проверки.
Служба выдачи токенов — TokenEndpoint
TokenEndpoint
можно рассматривать какController
, он примет наше приложениеtoken
просьба, см.postAccessToken
метод:
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof 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);
}
Первая запись состоит из двух частей:principal
а такжеparameters
, соответствующий двум частям параметров запроса в режиме пароля: заголовку запросаAuthorization
и тело запроса (grant_type
,username
,password
,scope
).
String clientId = getClientId(principal);
principal
Приход на самом делеUsernamePasswordToken
, соответствующая логика находится вBasicAuthenticationFilter
изdoFilterInternal
В методе:
@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) };
}
BasicAuthenticationFilter
перехватит/oauth/token
и попробуйте разобрать заголовок запросаAuthorization
, получить соответствующийBasic xxx
Строка с удаленными первыми 6 символамиBasic
,Получатьxxx
, что на самом деле мы и передаемclientId
а такжеclientSecret
Используйте двоеточие, чтобы объединиться, а затем используйтеbase64
полученный алгоритмом шифрования, поэтому вextractAndDecodeHeader
метод будетxxx
провестиbase64
расшифровывается, чтобы отделиться двоеточиямиclientId
а такжеclientSecret
составленный зашифрованный текст (заимствованный из предыдущегоclientId=test-client
а такжеclientSecret=123
Например, полученный здесь зашифрованный текст имеет видtest-client:123
), и наконецclient-id
так какusername
,clientSecret
так какpassword
построилUsernamePasswordToken
и вернуться, так что вpostAccessToken
серединаprincipal
может получить заголовок запросаclientId
.
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
Тогда позвониClientDetailsService
согласно сclientId
Запрос сведений о зарегистрированном клиенте, т.е.ClientDetails
, который является внешним приложением, регистрирующимsecurity-demo
Информация, заполняемая и просматриваемая при открытии платформы, содержит несколько пунктов, у нас есть только здесьclientId
а такжеclientSecret
два. (authenticatedClient
указать этоclient
Мы проверили его, чтобы разрешить доступ к нашей открытой платформе.client
)
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
Затем по параметрам тела запросаparameters
и данные клиентаclientDetails
построилTokenRequest
,этоtokenRequest
Указывает, что текущее приобретениеtoken
К какому клиенту относится запрос (clientDetails
), чтобы узнать, какой пользователь (parameters.username
) права доступа и режим авторизации (parameters.grant_type
), какие разрешения получить (parameters.scope
).
if (clientId != null && !clientId.equals(""))
Затем на входящийclientId
а такжеauthenticatedClient
изclientId
Проверьте это. Может быть, вы спросите,authenticatedClient
не по приходуclientId
Вы это выяснили, и перекалибровать было бы лишним. На самом деле нет, хотя метод запроса называетсяloadClientByClientId
, но может быть понято только как основанное наclient
Уникально идентифицирует проверенный запросclient
, возможно этот уникальный идентификатор есть в нашей базеclient
нерелевантный первичный ключ таблицыid
, или возможноclientId
значение поля. То есть мы должны понимать имя метода с точки зрения макроса.loadClientByClientId
. Так вотclientId
Ничего страшного в проверке нет.
if (authenticatedClient != null)
Затем определяет, еслиauthenticatedClient
Если не пусто, проверьте область разрешений запроса.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)");
}
}
Вы можете представить такой сценарий: внешнее приложение запрашивает доступ к нашей открытой платформе, чтобы прочитать информацию о пользователе нашей платформы, затем соответствующийclientScopes
для["read"]
, клиент просит получить после прохождения аудитаtoken
(token
Может указывать: 1. Кто вы есть; 2. Что вы можете сделать; 3. Запрос параметров при доступеscope
Это может быть только["read"]
, вместо["read","write"]
Ждать. Вот запрос на проверкуtoken
входящийscope
Все ли включены в зарегистрированный клиентscopes
середина.
if (!StringUtils.hasText(tokenRequest.getGrantType()))
Затем проверьтеgrant_type
Параметр не может быть пустым, что такжеoauth
оговорено в соглашении.
if (tokenRequest.getGrantType().equals("implicit"))
Затем определить входящиеgrant_type
Этоimplicit
, то есть использует ли клиент简易模式
Получатьtoken
,потому что简易模式
Получен непосредственно после того, как пользователь соглашается разрешитьtoken
, поэтому get больше не следует вызыватьtoken
интерфейс.
if (isAuthCodeRequest(parameters))
Затем оцените, использует ли клиент режим кода авторизации в соответствии с параметрами запроса.tokenRequest
серединаscope
Установите пустое значение, поскольку разрешения клиента не должны передаваться сами по себе.scope
решать, но он утверждается нами при регистрацииscopes
Чтобы решить, что это свойство будет прочитано из подробностей клиента позжеscope
покрытие.
if (isRefreshTokenRequest(parameters))
private boolean isRefreshTokenRequest(Map<String, String> parameters) {
return "refresh_token".equals(parameters.get("grant_type")) && parameters.get("refresh_token") != null;
}
Определите, является ли это обновлениемtoken
запрос. действительно может запроситьtoken
изgrant_type
Кромеoauth
4 режима авторизации в стандартеauthorization_code
,implicit
,password
,client_credential
, есть еще одинrefresh_token
, чтобы улучшить взаимодействие с пользователем (традиционные методы входа требуют повторного входа через определенный период времени),token
Механизм обновления может быть реализован без восприятия пользователемtoken
продление времени. если обновитьtoken
запрос, как написано в комментариях,refresh_token
путь тоже имеет свой дефолтscopes
, поэтому тот, который включен в запрос, не должен использоваться.
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
Это самый важный шаг, впереди — инкапсуляция и проверка параметров запроса. Этот шаг вызоветTokenGranter
Генерация токеновtoken
,НазадgetResponse(token)
будет генерироватьtoken
Ответил прямо. По типу входящей авторизацииgrant_type
и соответствующие параметры, которые необходимо передать, будут настроены по-разномуTokenGranter
класс реализацииtoken
построение этой логики вCompositeTokenGranter
середина:
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
Он вызовет соответствующий режим авторизации через 4 по очереди.TokenGranter
класса реализацииgrant
только метод и параметры запросаgrant_type
соответствующийTokenGranter
будет вызываться, эта логика находится в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);
logger.debug("Getting access token for: " + clientId);
return getAccessToken(client, tokenRequest);
}
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";
}
Токен Грантер - TokenGranter
Потому что это так密码模式
Например, так поток идет кResourceOwnerPasswordTokenGranter.grant
, это не отменяетgrant
метод, поэтому родительский класс вызываетсяgrant
метод:
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));
}
сосредоточиться на20
линии, вызовите подклассgetOAuth2Authentication
ПолучатьOAuth2Authentication
, и передать его вызывающему серверу аутентификацииtoken
СлужитьAuthorizationServerTokenServices
генерироватьtoken
. здесьgetOAuth2Authentication
,каждыйTokenGranter
Подклассы имеют разные реализации, потому что логика проверки разных режимов авторизации разная, например授权码模式
Эта ссылка необходима для проверки кода авторизации, переданного в запросе (tokenRequest.parameters.code
) тот, который я отправил соответствующему клиенту раньше (clientDetails
); а также密码模式
Это нужно для проверки того, существуют ли имя пользователя и пароль, переданные в запросе, в моей текущей системе и правильный ли пароль. После прохождения проверки он вернетOAuth2Authentication
, включаяoauth
Сопутствующая информация и информация о пользователях системы.
AuthorizationServerTokenServices
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
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);
}
Его можно найти,ResourceOwnerPasswordTokenGranter
Логика проверки почти такая же, как и логика фильтра аутентификации по имени пользователя и паролю, который мы писали ранее: получить имя пользователя и пароль из запроса, а затем построитьauthRequest
перейти кProviderManager
проверить,ProviderManager
ПорученоDaoAuthenticationProvider
Природа позовет насUserDetailsService
пользовательский класс реализацииCustomUserDetailsService
Запросите пользователя и проверьте.
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
Если проверка пройдена, он вернется, если аутентификация прошла успешно.Authentication
После этого фабричный метод будет вызван в соответствии с данными клиента иtokenRequest
ПостроитьAuthenticationServerTokenServices
нужныйOAuth2Authentication
вернуть.
Службы токенов сервера аутентификации — AuthorizationServerTokenServices
при полученииOAuth2Authentication
После этого служба токенов может генерироватьtoken
Теперь давайте взглянем на класс реализации службы токенов.DefaultTokenServices
как он генерируется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);
return existingAccessToken;
}
}
// 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;
}
сначала попытается получить токен из репозиторияtokenStore
получено вtoken
, потому что каждое поколениеtoken
Затем он позвонит, прежде чем ответитьtokenStore
сохранить сгенерированныйtoken
, чтобы последующие клиенты получалиtoken
Есть доказательства, на которые можно положиться при доступе к ресурсам.
if (existingAccessToken != null)
если изtokenStore
понятноtoken
, что указывает на то, что ранее сгенерированныйtoken
, возможны две ситуации:
- Старый
token
истек, тоtoken
удаляется, еслиtoken
изrefresh_token
Если он все еще там, также удалите его (запрос на обновление определенногоtoken
нужен соответствующийrefresh_token
,еслиtoken
Если он терпит неудачу, его сопровождающийrefresh_token
тоже должен быть недоступен) - Старый
token
Не истек, сохраните его сноваtoken
(Поскольку лицевая и обратная стороны могут быть сгенерированы с помощью разных режимов авторизацииtoken
, соответствующая логика сохранения также будет другой), и напрямую вернутьtoken
, метод заканчивается.
если не изtokenStore
нашел в старомtoken
, то новыйtoken
,СохранитьtokenStore
внутрь и обратно.
резюме
Интегрируйте имя пользователя и пароль, чтобы получить токен
Хотя фреймворк помог нам инкапсулировать 4 режима авторизации, требуемых сервером аутентификации, он, как правило, является внешним (внешние приложения не могут считывать информацию о пользователе нашей системы) и используется для создания открытой платформы. Для внутренних приложений нам по-прежнему необходимо предоставить имя пользователя и пароль для входа в систему, код подтверждения номера мобильного телефона и т. д., чтобы получитьtoken
. Во-первых, фреймворк проходит весь путь доTokenGranter
Мы не можем продолжать использовать эту часть компонента, потому что она былаOAuth
Процесс затвердевает. Все, что мы можем использовать, это сервис генерации токенов.AuthorizationServerTokenServices
, Но для этого требуетсяOAuth2Authentication
, пока мы строимOAuth2Authentication
нужно сноваtokenRequest
а такжеauthentication
.
Основываясь на исходной логике входа, мы можем изменить обработчик успеха входа, в котором мы можем получить успешную аутентификацию.authentication
и из заголовка запросаAuthorization
получено вclientId
введенный вызовClientDetailsService
выяснитьclientDetails
и построитьtokenRequest
, чтобы можно было вызвать службу создания маркера для создания маркера и ответа.
Вызвать службу токенов в обработчике успешного входа в систему
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("请求头中必须附带 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("无效的clientId");
} else if (!StringUtils.equals(clientSecret, clientDetails.getClientSecret())) {
throw new UnapprovedClientAuthenticationException("错误的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) };
}
}
Унаследуйте ResourceServerConfigurerAdapter для реализации конфигурации безопасности.
мы будемBrowserSecurityConfig
среда дляsecurity
скопируйте конфигурацию вResourceServerConfig
, чтобы включить только форму входа с паролем:
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() {
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() {
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 {
// 启用表单密码登录过滤器
http.formLogin()
.loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
.successHandler(appAuthenticationSuccessHandler)
.failureHandler(appAuthenticationFailureHandler);
http
// // 启用验证码校验过滤器
// .apply(verifyCodeValidatorConfig).and()
// // 启用短信登录过滤器
// .apply(smsLoginConfig).and()
// // 启用QQ登录
// .apply(qqSpringSocialConfigurer).and()
// // 浏览器应用特有的配置,将登录后生成的token保存在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()
// 浏览器应用特有的配置
.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的授权机制没有登录/注销的概念,只有token申请和过期的概念
.csrf().disable();
}
}
Таким образом, клиент внутреннего приложения может получить имя пользователя и пароль через имя пользователя и пароль пользователя.token
сейчас:
-
Заголовок запроса по-прежнему должен сопровождаться информацией о клиенте.
-
Параметры запроса передают логин и пароль для входа в требуемые параметры
-
Получить после успешного входа
token
-
пройти через
token
служба доступаиз-за
Postman
Запись и чтение на стороне сервера по-прежнему поддерживаютсяCookie
чтобы избежать
Session/Cookie
Влияние метода входа в систему, каждый раз, когда нам нужно очиститьcookie
Отправьте запрос еще раз.Первый не включен
token
, обнаружил, что запрос был перехвачен:а затем прикрепить
token
Запрос доступа:
На этом этапе получается логин и пароль для входа.token
Интеграция прошла успешно!
Процесс интеграции кода подтверждения и входа по SMS аналогичен и здесь повторяться не будет. Стоит отметить, что на основе
token
способ отказаться отSession/Cookie
операции, вы можете поместить информацию, которая будет сохранена на сервере, например,Redis
в слое сохранения.
Интегрируйте социальный логин, чтобы получить токен
В этом разделе мы реализуем способ, которым внутреннее приложение использует социальный вход для получения с внутреннего сервера аутентификации.token
.
легкий режим
Анализ процесса
Если внутреннее приложение принимает简易模式
, после того, как пользователь соглашается авторизоваться, пользователь может напрямую получить информацию, предоставленную внешним поставщиком услуг.token
, то у нас нет возможности получить этоtoken
Чтобы получить доступ к серверу внутренних ресурсов, вам нужно взять этотtoken
Перейти на внутренний сервер аутентификации в обмен на внутренний пропуск нашей системыtoken
.
В обмен на идею, если вход пользователя в социальную сеть прошел успешно, то внутреннее приложение может получитьproviderUserId
(упоминается внешними поставщиками услуг какopenId
),а такжеUserConnection
В таблице должна быть запись (userId,providerId,providerUserId
), внутреннее приложение просто заменяетproviderId
а такжеproviderUserId
Передайте его внутреннему серверу аутентификации, и внутренний сервер аутентификации проверитUserConnection
таблица проверяется иuserId
ПостроитьAuthentication
генерироватьaccessToken
.
Для этого нам нужно написать набор внутренних серверов аутентификацииproviderId+openId
Процесс сертификации:
вUserConnectionRepository
,CustomUserDetailsService
,AppAuthenticationSuccessHandler
Все готовые и могут быть использованы непосредственно.
SecurityProperties
добавить основу обработкиopenId
братьtoken
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;
}
индивидуальный запросAuthenticationToken
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 {
// 作为请求认证的token时存储providerId,作为认证成功的token时存储用户信息
private final Object principal;
// 作为请求认证的token时存储openId,作为认证成功的token时存储用户密码
private Object credentials;
// 请求认证时调用
public OpenIdAuthenticationToken(Object providerId, Object openId) {
super(null);
this.principal = providerId;
this.credentials = openId;
setAuthenticated(false);
}
// 认证通过后调用
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() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}
Перехватчик аутентификацииOpenIdAuthenticationFilter
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
return getAuthenticationManager().authenticate(authRequest);
}
}
Сотрудник по практической сертификацииOpenIdAuthenticationProvider
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 (!(authentication instanceof OpenIdAuthenticationToken)) {
throw new IllegalArgumentException("不支持的token认证类型:" + 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("无效的providerId和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() {
return usersConnectionRepository;
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
Класс конфигурации процесса аутентификации OpenIdOpenIdAuthenticationConfig
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 = new OpenIdAuthenticationProvider();
openIdAuthenticationProvider.setUsersConnectionRepository(usersConnectionRepository);
openIdAuthenticationProvider.setUserDetailsService(customUserDetailsService);
builder
.authenticationProvider(openIdAuthenticationProvider)
.addFilterBefore(openIdAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
apply
применимый кSecurity
в основном классе конфигурации
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() {
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 {
// 启用表单密码登录获取token
http.formLogin()
.loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
.successHandler(appAuthenticationSuccessHandler)
.failureHandler(appAuthenticationFailureHandler);
// 启用社交登录获取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();
}
}
тестовое задание
текущее использованиеPostman
Моделирование доступа к внутреннему приложению/auth/openId
проситьtoken
:
и посетить/user
тестовое заданиеtoken
Действительность, посетите успех! Интегрированный вход через социальные сети выполнен успешно!
Режим кода авторизации
Если внутреннее приложение принимает режим кода авторизации, когда внешний поставщик услуг перезванивает с кодом авторизации, внутреннее приложение может напрямую перенаправить запрос обратного вызова на наш сервер аутентификации, поскольку мы ранее написали модуль входа в социальную сеть, поэтому бесшовное соединение может быть достигнут.
Или возьмем в качестве примера логин QQ, который мы реализовали ранее:
Внутри, когда пользователь соглашается на авторизацию и сервер аутентификации QQ перенаправляется на домен обратного вызова внутреннего приложения, запрос обратного вызова может быть перенаправлен на сервер аутентификации как есть, поскольку мы разработали его ранее./socialLogin
Интерфейс обрабатывает входы через социальные сети.
Для теста здесь мы не можем разработать приложение, мы можем использовать оригинальный разработанныйsecurity-browser
проект, а затем сделать точку останова, где получен код авторизации, и остановить службу после получения кода авторизации (чтобы не запрашивать код авторизации позжеtoken
привести к тому, что код авторизации станет недействительным). затем вPostman
Запрос кода авторизации Zhongnatoken
(Смоделированное приложение перенаправляет домен обратного вызова на/socialLogin/qq
)
первый вsecurity-demo
средняя нотаsecurity-app
при включенииsecurity-browser
<dependency>
<groupId>top.zhenganwen</groupId>
<artifactId>security-browser</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>top.zhenganwen</groupId>-->
<!-- <artifactId>security-app</artifactId>-->
<!-- <version>1.0-SNAPSHOT</version>-->
<!-- </dependency>-->
БудуCustomUserDetailsService
перейти кsecurity-core
в, потому чтоbrowser
а такжеapp
Оба полезны:
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("登录用户名: " + username);
// 实际项目中你可以调用Dao或Repository来查询用户是否存在
if (Objects.equals(username, "admin") == false) {
throw new UsernameNotFoundException("用户名不存在");
}
// 假设查出来的密码如下
String pwd = passwordEncoder.encode("123");
return new SocialUser(
"admin", pwd, AuthorityUtils.commaSeparatedStringToAuthorityList("user,admin")
);
}
// 根据用户唯一标识查询用户, 你可以灵活地根据用户表主键、用户名等内容唯一的字段来查询
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
return buildUser(userId);
}
}
Затем установите порт80
Запустите сервис и получите код авторизации следующим образомtoken
установить точку останова перед (OAuth2AuthenticationService
):
доступwww.zhenganwen.top/login.html
Выполните авторизованный вход QQ (одновременно откройте консоль браузера), согласитесь на авторизацию для перехода, остановите службу после остановки в точке останова, найдите URL-адрес обратного вызова в консоли браузера и скопируйте его:
опять такиsecurity-demo
изpom
переключиться наapp
<!-- <dependency>-->
<!-- <groupId>top.zhenganwen</groupId>-->
<!-- <artifactId>security-browser</artifactId>-->
<!-- <version>1.0-SNAPSHOT</version>-->
<!-- </dependency>-->
<dependency>
<groupId>top.zhenganwen</groupId>
<artifactId>security-app</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
существуетSecurity
включено в основном файле конфигурацииQQ
Авторизоваться:
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() {
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 {
// 启用表单密码登录获取token
http.formLogin()
.loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
.successHandler(appAuthenticationSuccessHandler)
.failureHandler(appAuthenticationFailureHandler);
// 启用社交登录获取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();
}
}
Тогда мы можем использоватьPostman
Смоделированное приложение перенаправит обратный вызов полученного кода авторизации на сервер аутентификации для полученияtoken
сейчас:
Здесь сервер аутентификации получает код авторизацииtoken
вернуть информацию об исключенииcode is reused error
(Код авторизации используется повторно), само собой разумеется, что мы попали в точку останова и остановили службу вовремя в прошлый раз, но код авторизации не использовался для запросаtoken
Правильно, ошибку здесь еще предстоит исследовать.
режим процессора
На самом деле, даже еслиtoken
получить успех, также не будет реагировать на то, что мы хотимaccessToken
, т.к. ранее настроенныйSocialAuthenticationFilter
Для него нет обработчика успешной аутентификации, поэтомуAppAuthenticationSuccessHandler
Установите его в нем, чтобы после успешного входа в социальную сеть он генерировал и возвращалtoken
.
Далее мы воспользуемся простым, но практичным методом рефакторинга процессора, чтобыsecurity-app
Чжунвэйsecurity-core
изSocialAuthenticationFilter
Сделайте улучшение:
package top.zhenganwen.security.core.social;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
/**
* @author zhenganwen
* @date 2019/9/15
* @desc 认证过滤器后置处理器
*/
public interface AuthenticationFilterPostProcessor<T extends AbstractAuthenticationProcessingFilter> {
/**
* 对认证过滤器做一个增强,例如替换默认的认证成功处理器等
* @param filter
*/
void process(T filter);
}
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) // 不是必需的
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;
}
}
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);
}
}
Интегрировать функцию привязки социальных аккаунтов
Временное хранение сторонней пользовательской информации
Раньше, когда пользователь впервые использовал вход через социальную сеть,UserConnection
Нет соответствующей связанной записи в (userId->providerId-providerUserId
), логика в то время заключалась в том, чтобы поместить запрошенную информацию о сторонних пользователях вSession
, а затем перейдите на страницу управления социальной учетной записью, чтобы помочь пользователю установить связь с социальной учетной записью.ProviderSignInUtils
инструменты изSession
Он передается, когда сторонняя информация о пользователе извлекается и пользователь подтверждает ассоциацию.userId
составить ассоциацию (вставить вUserConnection
)середина. ноSecurity
который предоставилProviderSignInUtils
основывается наSession
, на основеtoken
Механизм аутентификации не работает.
В этот момент мы можемOAuth
Сторонняя пользовательская информация, полученная после завершения процесса в виде пользовательского оборудования.deviceId
так какkey
кешировать вRedis
, когда пользователь подтверждает ассоциацию,Redis
выведены и соединены сuserId
Вставить как записьUserConnection
середина. По сути, это процесс смены способа хранения (на памятиSession
изменить на кешredis
).
вести перепискуProviderSignInUtils
Мы инкапсулируемRedisProviderSignInUtils
Просто замените его.
Помогите пользователям связать учетные записи социальных сетей
Следующие интерфейсы могут быть реализованы во всехbean
Вызывается до завершения инициализацииpostProcessBeforeInitialization
,bean
Вызывается после инициализацииpostProcessAfterInitialization
, если вы не хотите улучшать его, вы можете вернуть входящийbean
, если вы хотите целенаправленное улучшение, вы можетеbeanName
фильтровать.
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
Мы можем реализовать класс реализации этого интерфейсаSpringSocialConfigurerPostProcessor
существуетQQSpringSocialConfigurer bean
сброс после инициализацииconfigure.signupUrl
,когдаUserConnection
нет перепискиConnection
Перейти, когда запись связанаsignupUrl
соответствующую службу.
В этой службе должен быть возвращен JSON, указывающий, что внешний интерфейс должен связать социальную учетную запись (и будет идти доOAuth
Полученная информация о сторонних пользователях предоставленаProviderSignInUtils
отSession
выньте и используйтеRedisProviderSignInUtils
подготовка кRedis
), вместо перехода на страницу ассоциации социальной учетной записи, как было установлено ранее. Формат возвращаемой информации следующий: