因为看了很多博客,发现很多人的使用风格都不一样,有点懵。感觉最好的方法就是多看一些源码,然后自己选择想要的使用方式。
spring security版本:2.1.6.RELEASE
一 认证三元素
这里先讲这三种的关系
AuthenticationManager、ProviderManager、AuthenticationProvider
Authentication
封装了用户身份信息
public interface Authentication extends Principal, Serializable {
//#1.权限结合,可使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_ADMIN")返回字符串权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//#2.用户名密码认证时可以理解为密码
Object getCredentials();
//#3.认证时包含的一些信息。
Object getDetails();
//\#4.用户名密码认证时可理解时用户名
Object getPrincipal();
#5.是否被认证,认证为true
boolean isAuthenticated();
#6.设置是否能被认证
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
AuthenticationManager
一个接口,只有一个方法
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
ProviderManager
ProviderManager是AuthenticationManager的实现类,提供了基本认证实现逻辑和流程;
先看这个方法这个方法,是用来认证
的方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();//获取authentication的类型
AuthenticationException lastException = null;
Authentication result = null;//默认null
boolean debug = logger.isDebugEnabled();
//一个ProviderManager有一个AuthenticationProvider的集合列表
for (AuthenticationProvider provider : getProviders()) {
//当前AuthenticationProvider是否支持认证这种类型的authentication
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);//真正的认证是AuthenticationProvider完成的
if (result != null) {//不等于null,说明认证成功了
copyDetails(authentication, result);//将authentication的detail信息设置到result上
break;
}
}
catch (AccountStatusException e) {//认证过程中如果出现了异常便捕获
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;//这种异常的处理方式是抛出,所以这种异常出现了。不会继续用其他provider继续认证。就是认证失败了
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;//这种异常没有抛出,捕获住了。并继续for循环,尝试交给其他的provider尝试认证。
}
}
//如果所有的认证都不通过,并且当前ProviderManager存在父AuthenticationManager
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);//注意这里赋了两次值
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication 认证完成,清除敏感信息
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);//放着重复发布成功事件
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it 防止重复
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;//抛出异常
}
关键点
- lastException:在认证过程中,可能会由多个组件来尝试认证并认证失败。但只记录最后一次认证失败得异常
- 因为可能会有
父AuthenticationManager
参与认证,所以这里面做了一些处理。防止重复发布成功/失败得事件
AuthenticationProvider
AuthenticationProvider
本身也就是一个接口
,它有实现类AbstractUserDetailsAuthenticationProvider
和AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider
只有两个
方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);//检查是否支持认证指定类型的Authentication
AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);//先根据用户名从缓存中找
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);//找到正确得用户身份,抽象方法待实现
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
//不管怎么样,只要查找用户失败都会抛出异常。只是有隐藏不隐藏用户没找到的信息区别
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");//null判断
}
try {
preAuthenticationChecks.check(user);//预先检查返回的身份信息,比如账号锁定等等属性
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);//检查用户的登陆信息
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;//如果缓存的身份比对失败,则查一次最新的再进行比对
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);//后置检查,执行到这步说明上路认证成功了,没有抛出异常,导致中止
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);//加入缓存
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);//创建一个新的身份信息,并返回
}
总结
这是一个抽象类,是模板模
式。定义好了认证流程,我们去实现流程中的一些环节
方法。达到了自定义认证
的效果
DaoAuthenticationProvider
AbstractUserDetailsAuthenticationProvider
的子类,实现了一些抽象方法
retrieveUser
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//通过UserDetailsService去查找用户
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
主要就是用UserDetailsService
去查找用户
additionalAuthenticationChecks
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {//用户的认证信息没有密码
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();//提交的密码
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {//加密对象会自动加密然后比较
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
createSuccessAuthentication
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
本质还是调用了父类的方法,就是新建了一个Authentication,然后将UserDetails
的信息
传递过去
UserDetailsService
UserDetailsService是一个接口,提供了一个方法
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetails
public interface UserDetails extends Serializable {
#1.权限集合
Collection<? extends GrantedAuthority> getAuthorities();
#2.密码
String getPassword();
#3.用户民
String getUsername();
#4.用户是否过期
boolean isAccountNonExpired();
#5.是否锁定
boolean isAccountNonLocked();
#6.用户密码是否过期
boolean isCredentialsNonExpired();
#7.账号是否可用(可理解为是否删除)
boolean isEnabled();
}
UserDetailsManager
public interface UserDetailsManager extends UserDetailsService {
/**
* Create a new user with the supplied details.
*/
void createUser(UserDetails user);
/**
* Update the specified user.
*/
void updateUser(UserDetails user);
/**
* Remove the user with the given login name from the system.
*/
void deleteUser(String username);
/**
* Modify the current user's password. This should change the user's password in the
* persistent user repository (datbase, LDAP etc).
*
* @param oldPassword current password (for re-authentication if required)
* @param newPassword the password to change to
*/
void changePassword(String oldPassword, String newPassword);
/**
* Check if a user with the supplied login name exists in the system.
*/
boolean userExists(String username);
}
感觉这个接口没什么用,就是用来管理用户类的
二 过滤器原理
SS在http后台中起作用主要是基于Servlet Filters
的,我们先来看看什么是 Filter 是如何作用在 Servlet 中的。
可以看到不同的过滤器作用在 Servlet 之前,多个形成的就是一条
过滤器链( Filters Chain )
,每个Filter 有个Order
顺序,可以通过 @Orde
r 来设置Filter 的 Order ,设置前后顺序。SS本身也是一个 Filter
,使用一个代理
,委托了一个 Filter Chain
,如下图 :image.png
springSecurityFilterChain 是个接口
,DefaultSecurityFilterChain
是它的实现类,而DefaultSecurityFilterChain 内部存在这一个 Filters 列表
,关于SS中的过滤器和他们的执行顺序(Order)可以查看 官方文档,当我们需要自定义Filter的时候就会用到。 当请求到来时,在 ss 里边的 Filter就会作用请求,如下图 :
三 授权过程
spring security的过滤器链
在上面我们说到了SS有自己的一条过滤器链
,下面就是截图:(执行顺序就是集合中的顺序)
下面说一下几个比较重要的
Filter 的处理逻辑
UsernamePasswordAuthenticationFilter
整个调用流程是,先调用其父类 AbstractAuthenticationProcessingFilter.doFilter() 方法,然后再执行 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法进行验证;
AbstractAuthenticationProcessingFilter
父类是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 (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
#2.抽象方法由子类来实现
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
#2.认证成功后,处理一些与session相关的方法
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
#3.认证失败后的的一些操作
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
#3. 认证成功后的相关回调方法 主要将当前的认证放到SecurityContextHolder中
this.successfulAuthentication(request, response, chain, authResult);
}
}
public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;
requiresAuthentication
判断是否需要当前filter处理,就是判断路径、请求方法等等
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return this.requiresAuthenticationRequestMatcher.matches(request);
}
以UsernamePasswordAuthenticationFilter的构造方法为例
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
successfulAuthentication
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);
}
- 将当前认证成功的 Authentication 放置到
SecurityContextHolder
中; - 记住我功能
- 调用其它可扩展的
handlers
继续处理该认证成功以后的回调事件;(实现AuthenticationSuccessHandler
接口即可)
UsernamePasswordAuthenticationFilter
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
#1.判断请求的方法必须为POST请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
#2.从request中获取username和password
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
#3.构建UsernamePasswordAuthenticationToken(两个参数的构造方法setAuthenticated(false))
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
#4. 调用 AuthenticationManager 进行验证(子类ProviderManager遍历所有的AuthenticationProvider认证)
return this.getAuthenticationManager().authenticate(authRequest);
}
}
这里的认证就是第一节的内容(this.getAuthenticationManager().authenticate(authRequest)
)
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
(用户名为 anonymousUser 的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);
this.logger.debug("Chain processed normally");
#捕获的是下一个过滤器中抛出的异常,前面抛出的异常不归它管
} catch (IOException var9) {
throw var9;
} catch (Exception var10) {
#.判断是不是AuthenticationException
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
#. 判断是不是AccessDeniedException
ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (ase == null) {
if (var10 instanceof ServletException) {
throw (ServletException)var10;
}
if (var10 instanceof RuntimeException) {
throw (RuntimeException)var10;
}
throw new RuntimeException(var10);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var10);
}
this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
}
}
FilterSecurityInterceptor
此过滤器为认证授权过滤器链中最后一个过滤器
,该过滤器之后就是请求真正的/xx服务
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
if (fi.getRequest() != null && this.observeOncePerRequest) {
fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
#1. before invocation重要
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
#2. 可以理解开始请求真正的 /xx服务
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.finallyInvocation(token);
}
#3. after Invocation
super.afterInvocation(token, (Object)null);
}
}
-
before invocation重要
(调用AccessDecisionManage
r 来验证当前已认证成功的用户是否有权限
访问该资源)
2 .请求真正的/xx服务
- after Invocation
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:使用当前的访问资源路径去匹配我们
自己定义的匹配规则
。
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
进行vote(投票) - 只要有投通过(
ACCESS_GRANTED
)票,则直接判为通过。 - 如果没有投通过则
deny++
,最后判断if(deny>0 抛出AccessDeniedException(未授权)
AccessDecisionVoter的实现类WebExpressionVoter
public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
assert authentication != null;
assert fi != null;
assert attributes != null;
WebExpressionConfigAttribute weca = this.findConfigAttribute(attributes);
if (weca == null) {
return 0;
} else {
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, fi);
ctx = weca.postProcess(ctx, fi);
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? 1 : -1;
}
}
到此位置authentication
当前用户信息,fl当前访问的资源路径及attributes当前资源路径的决策
(即是否需要认证)。剩下就是判断
当前用户的角色Authentication.authorites是否权限
访问决策访问当前资源fi
网友评论