Если мы не можем говорить о любви, мы можем пожалеть себя.
клин
Предыдущая статьямы говорилиSpringSecurity
Процесс сертификации, я считаю, что внимательно прочитав его, вы обязательноSpringSecurity
Процесс аутентификации был понят на семь или восемь пунктов.Эта проблема связана с главой динамической аутентификации, которую мы прибыли по расписанию.Вам не нужно понимать знания предыдущей главы, чтобы прочитать эту статью, потому что основное внимание в описании не Вы можете использовать эти две статьи как две отдельные главы, из которых можно извлечь те части, которые вам нужны.
Желаю хорошего урожая.
Код для этой статьи: Адрес облака кода Адрес GitHub
1. 📖Принцип аутентификации Spring Security
Предыдущая статьяКогда мы говорили о сертификации, мы однажды поставили картинку, которая выглядит следующим образом:
Весь процесс аутентификации был сосредоточен на зеленой части цепочки фильтров на рисунке, а динамическая аутентификация, о которой мы сегодня поговорим, в основном вращается вокруг ее оранжевой части, которая отмечена на рисунке:FilterSecurityInterceptor
.
1. FilterSecurityInterceptor
Чтобы узнать, как выполнять динамическую аутентификацию, мы должны сначала понять логику аутентификации Spring Security, Мы также можем видеть на рисунке выше:FilterSecurityInterceptor
Это последнее звено в этой цепочке фильтров, и за аутентификацией следует аутентификация, поэтому нашFilterSecurityInterceptor
В основном отвечает за часть аутентификации.
Запрос прибудет после того, как он будет аутентифицирован без создания исключения.FilterSecurityInterceptor
Отвечает за часть аутентификации, то есть вход аутентификации находится вFilterSecurityInterceptor
.
Давайте сначала посмотримFilterSecurityInterceptor
Определение и основные методы:
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
}
Вышеприведенный код можно увидетьFilterSecurityInterceptor
это абстрактный класс, реализующийAbstractSecurityInterceptor
Класс реализации этогоAbstractSecurityInterceptor
Есть очень важный код, написанный заранее (будет упомянут позже).
FilterSecurityInterceptor
Основным методом являетсяdoFilter
метод, характеристики фильтра должны быть известны всем, это будет выполнено после того, как придет запросdoFilter
метод,FilterSecurityInterceptor
изdoFilter
Метод на удивление прост, всего две строки:
Первая строкасоздаетсяFilterInvocation
объект, этоFilterInvocation
Вы можете использовать объект, поскольку он инкапсулирует запрос, и его основная задача — получить информацию в запросе, такую как URI запроса.
вторая линияназывать своимinvoke
метод и воляFilterInvocation
переданный объект.
Так что наша основная логика должна быть в этомinvoke
Метод внутри, давайте откроем его и посмотрим:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 进入鉴权
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
invoke
метод имеет только одинif-else, вообще не выполняйте три условия в if, и тогда придет логика выполненияelse.
elseКод также можно разделить на две части:
- называется
super.beforeInvocation(fi)
. - После звонка фильтр продолжает опускаться.
Второй шаг можно пропустить, такой шаг есть в каждом фильтре, поэтому в основном рассмотрим егоsuper.beforeInvocation(fi)
, я уже говорил раньше,FilterSecurityInterceptor
абстрактный классAbstractSecurityInterceptor
,
Итак, в этомsuperНа самом деле это означаетAbstractSecurityInterceptor
,
Этот код на самом деле вызываетAbstractSecurityInterceptor.beforeInvocation(fi)
,
я сказал раньшеAbstractSecurityInterceptor
В этом разделе есть очень важный фрагмент кода,
Итак, давайте перейдем к этомуbeforeInvocation(fi)
Исходный код метода:
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
Authentication authenticated = authenticateIfRequired();
try {
// 鉴权需要调用的接口
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
}
Исходный код длинный, здесь я упростил среднюю часть, этот код можно условно разделить на три шага:
- есть один
Collection<ConfigAttribute>
объект, этот объект являетсяList, по сути, это правила фильтрации, которые мы настраиваем в конфигурационном файле. - понятно
Authentication
, вот звонокauthenticateIfRequired
Метод получается, на самом деле, это все еще черезSecurityContextHolder
Получить его, я рассказал о том, как его получить в предыдущей статье. - называется
accessDecisionManager.decide(authenticated, object, attributes)
, первые два шага верныdecide
Метод подготавливает параметры, и третий шаг — официально перейти к логике аутентификации, поскольку это реальная логика аутентификации, это означает, что аутентификация на самом делеaccessDecisionManager
делает.
2. AccessDecisionManager
Мы видели настоящий процессор аутентификации через исходный код ранее:AccessDecisionManager
, Вы чувствуете, что один слой за другим, как матрешка, не волнуйтесь, внизу есть еще. Давайте посмотрим на определение интерфейса исходного кода:
public interface AccessDecisionManager {
// 主要鉴权方法
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}
AccessDecisionManager
Это интерфейс, который объявляет три метода.В дополнение к первому методу аутентификации есть два вспомогательных метода, функции которых заключаются в идентификацииdecide
Валидность параметров в методе.
Поскольку это интерфейс, класс реализации, названный выше, должен быть его классом реализации.Давайте посмотрим на дерево структуры этого интерфейса:
Из рисунка видно, что он в основном имеет три класса реализации, которые представляют три разные логики аутентификации:
- AffirmativeBased: Принято одним голосом, если пройден один голос, он считается принятым по умолчанию.
- UnanimousBased: Один голос против, пока есть один голос против, он не пройдет.
- На основе консенсуса: голоса меньшинства подчиняются голосам большинства.
Зачем использовать билет для выражения здесь? Поскольку в классе реализации используется форма делегирования, запрос делегируется избирателю, и каждый голосующий берет этот запрос и вычисляет, может ли он пройти, а затем голосует в соответствии со своей логикой, поэтому будет приведенное выше выражение.
Другими словами, эти три класса реализации на самом деле не являются классами, которые действительно судят, может ли запрос пройти или нет. может пройти или нет.
Как я только что сказал, класс реализации объединяет результаты избирателя для принятия решения, что означает, что может быть размещено несколько избирателей.Количество избирателей в каждом классе реализации зависит от того, сколько избирателей вставлено во время построения.Мы Вы можно увидеть по умолчаниюAffirmativeBased
исходный код.
public class AffirmativeBased extends AbstractAccessDecisionManager {
public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
super(decisionVoters);
}
// 拿到所有的投票器,循环遍历进行投票
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
}
AffirmativeBased
Структура должна быть передана в список избирателей, и ее основная логика аутентификации передается избирателю для оценки, и избиратель возвращает разные числа для представления разных результатов, а затемAffirmativeBased
Решите, выпускать или генерировать исключение в соответствии с политикой, утвержденной одним голосом.
AffirmativeBased
По умолчанию передается только один конструктор ->WebExpressionVoter
, этот конструктор логически обработает результат голосования в соответствии с вашей конфигурацией в конфигурационном файле.
такSpringSecurity
Логика аутентификации по умолчанию заключается в аутентификации в соответствии с конфигурацией в файле конфигурации, что соответствует нашим существующим знаниям.
2. ✍Внедрение динамической аутентификации
Благодаря приведенному выше пошаговому описанию, я думаю, вы также должны понятьSpringSecurity
Что именно реализует аутентификацию, и что делать, если мы хотим динамически давать роли разные права доступа?
Поскольку это динамическая аутентификация, наш URI разрешения должен быть помещен в базу данных.Что нам нужно сделать, так это прочитать разрешения, соответствующие различным ролям в базе данных, в режиме реального времени и сравнить его с текущим вошедшим в систему пользователем.
Затем мы можем придумать некоторые решения для этого, например:
- непосредственно переписать
AccessDecisionManager
, используйте его по умолчаниюAccessDecisionManager
, и прописать логику аутентификации прямо в нем. - Другой пример — переписать избиратель и поместить его в значение по умолчанию.
AccessDecisionManager
Внутри используйте устройство для голосования для аутентификации, как и раньше. - Я вижу, что в Интернете есть несколько блогов, которые делают это напрямую.
FilterSecurityInterceptor
изменения.
Мне всегда нравился небольшой и красивый способ с небольшими изменениями, поэтому код, продемонстрированный здесь, будет основан на втором решении и слегка изменен.
Затем нам нужно написать нового голосующего, получить в этом голосующем роль текущего пользователя и сравнить его с той ролью, которую требует текущий запрос.
Одного этого недостаточно, потому что у нас также может быть какое-то разрешение на выпуск в файле конфигурации, например, URI входа в систему освобождается, поэтому нам нужно продолжать использовать вышеупомянутоеWebExpressionVoter
, то есть я хочу настроить разрешение + файл конфигурации двухстрочный режим, поэтому нашAccessDecisionManager
В нем будет два избирателя:WebExpressionVoter
и пользовательские опросы.
Затем нам нужно подумать, какую стратегию голосования использовать, здесь я используюUnanimousBased
Один голос против политики вместо использования политики пропуска одного голоса по умолчанию, поскольку другие запросы, отличные от запросов на вход, настроены в нашей конфигурации на требование аутентификации, эта логика будетWebExpressionVoter
Обработка, если используется стратегия голосования, то когда мы обращаемся к защищенному API,WebExpressionVoter
Когда обнаруживается, что текущий запрос аутентифицирован, он напрямую голосует за, и, поскольку это политика с одним голосом, этот запрос не может быть передан нашему пользовательскому избирателю.
Примечание. Вы также можете поместить свою пользовательскую конфигурацию разрешений в базу данных без конфигурации в файле конфигурации, а затем передать ее избирателю для обработки.
1. Реконструируйте AccessDecisionManager
Тогда мы можем отпустить его, сначала восстановитьAccessDecisionManager
,
Поскольку избиратель автоматически добавляется при запуске системы, мы должны пересобрать его самостоятельно, если хотим добавить еще один конструктор.AccessDecisionManager
и прописать в конфиге.
И наша стратегия голосования изменилась, чтобы определитьAffirmativeBased
заменитьUnanimousBased
, поэтому этот шаг необходим.
И нам также нужно настроить избиратель и зарегистрировать его как Bean,AccessDecisionProcessor
Вот что нам нужно для настройки избирателя.
@Bean
public AccessDecisionVoter<FilterInvocation> accessDecisionProcessor() {
return new AccessDecisionProcessor();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
// 构造一个新的AccessDecisionManager 放入两个投票器
List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(new WebExpressionVoter(), accessDecisionProcessor());
return new UnanimousBased(decisionVoters);
}
определенныйAccessDecisionManager
После этого ставим его в конфигурацию запуска:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 放行所有OPTIONS请求
.antMatchers(HttpMethod.OPTIONS).permitAll()
// 放行登录方法
.antMatchers("/api/auth/login").permitAll()
// 其他请求都需要认证后才能访问
.anyRequest().authenticated()
// 使用自定义的 accessDecisionManager
.accessDecisionManager(accessDecisionManager())
.and()
// 添加未登录与权限不足异常处理器
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint())
.and()
// 将自定义的JWT过滤器放到过滤链中
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
// 打开Spring Security的跨域
.cors()
.and()
// 关闭CSRF
.csrf().disable()
// 关闭Session机制
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
После этого,SpringSecurity
внутриAccessDecisionManager
будет заменен на наш пользовательскийAccessDecisionManager
.
2. Реализация пользовательской аутентификации
В приведенной выше конфигурации есть два избирателя, второй избиратель — это тот, который нам нужно создать, я назвал егоAccessDecisionProcessor
.
Vote также имеет спецификацию интерфейса, нам нужно только реализовать этоAccessDecisionVoter
Просто интерфейс, а затем реализовать его методы.
@Slf4j
public class AccessDecisionProcessor implements AccessDecisionVoter<FilterInvocation> {
@Autowired
private Cache caffeineCache;
@Override
public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {
assert authentication != null;
assert object != null;
// 拿到当前请求uri
String requestUrl = object.getRequestUrl();
String method = object.getRequest().getMethod();
log.debug("进入自定义鉴权投票器,URI : {} {}", method, requestUrl);
String key = requestUrl + ":" + method;
// 如果没有缓存中没有此权限也就是未保护此API,弃权
PermissionInfoBO permission = caffeineCache.get(CacheName.PERMISSION, key, PermissionInfoBO.class);
if (permission == null) {
return ACCESS_ABSTAIN;
}
// 拿到当前用户所具有的权限
List<String> roles = ((UserDetail) authentication.getPrincipal()).getRoles();
if (roles.contains(permission.getRoleCode())) {
return ACCESS_GRANTED;
}else{
return ACCESS_DENIED;
}
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
Общая логика такова: мы используем URI+METHOD в качестве ключа для поиска информации, связанной с разрешением в кеше, если URI не найден, это доказывает, что URI не защищен, и голосующий может напрямую воздержаться.
Если информация о разрешении, связанная с этим URI, найдена, она будет сравниваться с информацией о роли, предоставленной пользователем, и возвращаться в соответствии с результатом сравнения.ACCESS_GRANTED
илиACCESS_DENIED
.
Конечно, для этого есть предпосылка, то есть я помещаю все данные разрешения URI в кеш при запуске системы, а система вообще кладет данные хотспота в кеш, когда начинает повышать эффективность доступа система.
@Component
public class InitProcessor {
@Autowired
private PermissionService permissionService;
@Autowired
private Cache caffeineCache;
@PostConstruct
public void init() {
List<PermissionInfoBO> permissionInfoList = permissionService.listPermissionInfoBO();
permissionInfoList.forEach(permissionInfo -> {
caffeineCache.put(CacheName.PERMISSION, permissionInfo.getPermissionUri() + ":" + permissionInfo.getPermissionMethod(), permissionInfo);
});
}
}
Здесь я считаю, что авторитетных URI может быть много, поэтому я ставлю авторитетный URI в качестве ключа в кеш, потому что скорость чтения данных через ключ в общем кеше O(1), так что это будет очень быстрый.
Как поступить с логикой аутентификации, на самом деле определяют сами разработчики, это должно быть комплексно продумано в соответствии с системными требованиями и дизайном таблиц базы данных, это просто идея.
Если вам не понятна идея приведенного выше URI разрешения в качестве ключа, могу привести еще один простой пример:
НапримерВы также можете получить роль текущего пользователя, найти все доступные URI в рамках этой роли, а затем сравнить URI текущего запроса. Если они согласуются, это доказывает, что роль текущего пользователя содержит разрешения этого URI, поэтому Нет Если они непротиворечивы, это доказывает, что разрешение недостаточно и не может быть освобождено.
Таким образом, при сравнении URI вы можете столкнуться с такими проблемами:/api/user/**
, а URI моего запроса/user/get/1
, этот метод определения разрешений в стиле Ant можно сравнить с классом инструментов:
@Test
public void match() {
AntPathMatcher antPathMatcher = new AntPathMatcher();
// true
System.out.println(antPathMatcher.match("/user/**", "/user/get/1"));
}
Это то, что я сделал, чтобы проверить непосредственноnewвзял одинAntPathMatcher
, вы можете зарегистрировать его как Bean-компонент и внедрить вAccessDecisionProcessor
использоваться в.
Он также может сравнивать URI в стиле RESTFUL, например:
@Test
public void match() {
AntPathMatcher antPathMatcher = new AntPathMatcher();
// true
System.out.println(antPathMatcher.match("/user/{id}", "/user/1"));
}
Столкнувшись с реальной системой, эти инструменты и дизайнерские идеи часто используются в сочетании в соответствии с дизайном системы.
Примечание:ACCESS_GRANTED
,ACCESS_DENIED
иACCESS_ABSTAIN
даAccessDecisionVoter
Константы, переносимые в интерфейс.
постскриптум
Ладно, на этом все, у меня печень с воскресенья.
Я пишу статьи, как правило, три раза:
-
Первый проход — это первый набросок, после сортировки существующих идей и их преобразования в слова.
-
Второй раз — проверить пропуски и заполнить пустующие места, посмотреть, есть ли в исходной идее пропущенные места, которые можно заполнить.
-
Третий этап — изменение языковой структуры.
После этого три раза осмелился запостить, так что аутентификация и авторизация разделены на две части, одну можно написать отдельно, а другая в том, что на написание одной части уходит очень много времени.
Это все равно, что говорить себе, что ты должен запоминать по 1000 слов в день в первый раз, но, конечно же, ты не можешь их запомнить в конце, а потом ты винишь себя и попадаешь в петлю.
Ставить слишком большие цели на ранней стадии часто контрпродуктивно. На ранней стадии вы должны выбрать те, которые вам по силам, сначала испытать радость завершения, а затем постепенно увеличивать сложность. Это истина во многих вещах.
После окончания этой статьи аутентификация и авторизация Spring Security завершены, и я надеюсь, что каждый сможет что-то получить.
ПредыдущийПроцесс аутентификации Spring Security, вы также можете просмотреть его еще раз.
Я пока не думал о следующей статье.Предполагается, что я напишу некоторые общие инструменты или проблемы с конфигурацией, которые часто встречаются во время разработки.Расслабьтесь и расслабьтесь.Есть также планы на oauth2.Я не знаю, есть ли у кого-нибудь видел oauth2.
Если вы считаете, что текст неплох, вы можете поставить мне палец вверх, ведь мне тоже нужно обновиться 🚀
Каждый ваш лайк, подборка и комментарий - отличное подтверждение моих знаний.Если есть какие-то ошибки или сомнения в тексте, или какой-то совет для меня, вы можете оставить сообщение под областью комментариев и обсудить вместе.
Я ухо, человек, который всегда хотел заняться выводом знаний, увидимся в следующем выпуске.
Код для этой статьи:Адрес облака кода Адрес GitHub