предисловие
Эта статья является продолжением предыдущей главыАнализ исходного кода Spring Security 1: процесс аутентификации Spring Securityдальнейший анализSpring Security
Как достигается авторизация при входе в систему с использованием имени пользователя и пароля?
Диаграмма классов
процесс отладки
Начните с отладкиGitHub.com/Longfeizhen…элемент, ввод браузераhttp://localhost:8080/persons, имя пользователя не является обязательным, а пароль — 123456;
Анализ исходного кода
Как показано на рисунке, он показываетfilters
Для связанного процесса вызова автор выделил несколько фильтров, которые он считает важными.
Порядок выполнения виден из рисунка. Рассмотрим логику обработки нескольких фильтров, которые автор считает более важными.UsernamePasswordAuthenticationFilter
,AnonymousAuthenticationFilter
,ExceptionTranslationFilter
,FilterSecurityInterceptor
И соответствующий поток обработки выглядит следующим образом;
UsernamePasswordAuthenticationFilter
Весь процесс вызова состоит в том, чтобы сначала вызвать метод родительского класса AbstractAuthenticationProcessingFilter.doFilter(), а затем выполнить метод UsernamePasswordAuthenticationFilter.attemptAuthentication() для проверки;
AbstractAuthenticationProcessingFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
#1.判断当前的filter是否可以处理当前请求,不可以的话则交给下一个filter处理
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
#2.抽象方法由子类UsernamePasswordAuthenticationFilter实现
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
#2.认证成功后,处理一些与session相关的方法
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
#3.认证失败后的的一些操作
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
#3. 认证成功后的相关回调方法 主要将当前的认证放到SecurityContextHolder中
successfulAuthentication(request, response, chain, authResult);
}
Последовательность выполнения всей программы выглядит следующим образом:
- Определить, может ли фильтр обработать текущий запрос, если нет, передать его следующему фильтру.
- вызов абстрактного метода
attemptAuthentication
Для проверки этот метод используется подклассамиUsernamePasswordAuthenticationFilter
выполнить - После успешной аутентификации вызовите некоторые методы, связанные с сеансом;
- После успешной аутентификации, соответствующий метод обратного вызова после успешной аутентификации, после успешной аутентификации, соответствующий метод обратного вызова после успешной аутентификации;
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
1. 将当前认证成功的 Authentication 放置到 SecurityContextHolder 中;
2. 将当前认证成功的 Authentication 放置到 SecurityContextHolder 中;
3. 调用其它可扩展的 handlers 继续处理该认证成功以后的回调事件;(实现`AuthenticationSuccessHandler`接口即可)
UsernamePasswordAuthenticationFilter
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
#1.判断请求的方法必须为POST请求
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
#2.从request中获取username和password
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
#3.构建UsernamePasswordAuthenticationToken(两个参数的构造方法setAuthenticated(false))
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
#4. 调用 AuthenticationManager 进行验证(子类ProviderManager遍历所有的AuthenticationProvider认证)
return this.getAuthenticationManager().authenticate(authRequest);
}
- Метод аутентификации запроса должен быть
POST
- Получить логин и пароль из запроса
- упаковка
Authenticaiton
класс реализацииUsernamePasswordAuthenticationToken
, (UsernamePasswordAuthenticationToken
Вызов конструктора с двумя параметрами setAuthenticated(false)) - перечислить
AuthenticationManager
изauthenticate
метод проверки см.ProviderManagerчасть;
AnonymousAuthenticationFilter
Как видно из схемы последовательности выполнения фильтра на рисунке вышеAnonymousAuthenticationFilter
фильтр находится вUsernamePasswordAuthenticationFilter
После ожидания фильтра, если ни один из предыдущих фильтров не был успешно аутентифицирован,Spring Security
электрический токSecurityContextHolder
добавитьAuthenticaiton
Анонимный класс реализацииAnonymousAuthenticationToken
;
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
#1.如果前面的过滤器都没认证通过,则SecurityContextHolder中Authentication为空
if (SecurityContextHolder.getContext().getAuthentication() == null) {
#2.为当前的SecurityContextHolder中添加一个匿名的AnonymousAuthenticationToken
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
if (logger.isDebugEnabled()) {
logger.debug("Populated SecurityContextHolder with anonymous token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
chain.doFilter(req, res);
}
#3.创建匿名的AnonymousAuthenticationToken
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
/**
* Creates a filter with a principal named "anonymousUser" and the single authority
* "ROLE_ANONYMOUS".
*
* @param key the key to identify tokens created by this filter
*/
##.创建一个用户名为anonymousUser 授权为ROLE_ANONYMOUS
public AnonymousAuthenticationFilter(String key) {
this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
- судить
SecurityContextHolder中Authentication
пусто; - Если воздух течет
SecurityContextHolder
добавить анонимAnonymousAuthenticationToken
(с именем пользователя анонимный пользовательAnonymousAuthenticationToken
)
ExceptionTranslationFilter
ExceptionTranslationFilter
Фильтр обработки исключений, который используется для обработки исключений, возникающих во время системной аутентификации и авторизации (то есть следующий фильтрFilterSecurityInterceptor
), в основном обработкаAuthenticationException
иAccessDeniedException
.
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
#.判断是不是AuthenticationException
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
#. 判断是不是AccessDeniedException
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
FilterSecurityInterceptor
Этот фильтр является последним фильтром в цепочке фильтров аутентификации и авторизации, после этого фильтра выполняется реальный запрос./persons
Служить
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
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) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
#1. before invocation重要
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
#2. 可以理解开始请求真正的 /persons 服务
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
#3. after Invocation
super.afterInvocation(token, null);
}
}
- перед вызовом важно
- Запросить услугу реального / человека
- after Invocation
Из трех частей наиболее важной является № 1, которая вызываетAccessDecisionManager
Чтобы проверить, есть ли у текущего аутентифицированного пользователя разрешение на доступ к ресурсу;
before invocation: AccessDecisionManager
protected InterceptorStatusToken beforeInvocation(Object object) {
...
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
...
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
#1.重点
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));
throw accessDeniedException;
}
...
}
authenticated
В настоящее время сертифицированоAuthentication
,Такobject
иattributes
Что это?
Что такое атрибуты и объекты?
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
мы обнаруживаемobject
для текущего запросаurl:/persons
, ТакgetAttributes
Метод заключается в использовании текущего пути к ресурсу доступа к匹配
Правила соответствия мы определяем сами.
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//使用表单登录,不再使用默认httpBasic方式
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//如果请求的URL需要认证则跳转的URL
.loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//处理表单中自定义的登录URL
.and()
.authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM,
SecurityConstants.DEFAULT_REGISTER_URL,
"/**/*.js",
"/**/*.css",
"/**/*.jpg",
"/**/*.png",
"/**/*.woff2")
.permitAll()//以上的请求都不需要认证
.anyRequest()//剩下的请求
.authenticated()//都需要认证
.and()
.csrf().disable()//关闭csrd拦截
;
}
0-7
возвращениеpermitALL
который не требует аутентификации,8
соответствоватьanyRequest
возвращениеauthenticated
I.e. текущий запрос требует аутентификации;
Вы можете увидеть текущийauthenticated
быть анонимнымAnonymousAuthentication
Имя пользователяanonymousUser
Как осуществляется авторизация AccessDecisionManager?
Spring Security
Использовать по умолчаниюAffirmativeBased
выполнитьAccessDecisionManager
изdecide
способ реализации авторизации
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
#1.调用AccessDecisionVoter 进行vote(投票)
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
#1.1只要有voter投票为ACCESS_GRANTED,则通过 直接返回
case AccessDecisionVoter.ACCESS_GRANTED://1
return;
@#1.2只要有voter投票为ACCESS_DENIED,则记录一下
case AccessDecisionVoter.ACCESS_DENIED://-1
deny++;
break;
default:
break;
}
}
if (deny > 0) {
#2.如果有两个及以上AccessDecisionVoter(姑且称之为投票者吧)都投ACCESS_DENIED,则直接就不通过了
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
- Позвоните AccessDecisionVoter, чтобы проголосовать (проголосовать)
- Пока есть голосование за проход (ACCESS_GRANTED), он напрямую считается пройденным.
- Если не проголосовал, то
deny++
, окончательный приговорif(deny>0
бросатьAccessDeniedException
(несанкционированный)
WebExpressionVoter.vote()
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
assert authentication != null;
assert fi != null;
assert attributes != null;
WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
if (weca == null) {
return ACCESS_ABSTAIN;
}
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
fi);
ctx = weca.postProcess(ctx, fi);
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;
}
в это местоauthentication
актуальная информация о пользователе,fl
Текущий путь к ресурсу иattributes
Решение о текущем пути к ресурсу (т. е. требуется ли аутентификация). Остальное определить роль текущего пользователяAuthentication.authorites
Решение о разрешении доступа для доступа к текущему ресурсуfi
.