想要深入spring security的authentication (身份验证)和access-control(访问权限控制)工作流程,必须清楚spring security的主要技术点包括关键接口、类以及抽象类如何协同工作进行authentication 和access-control的实现。
常见认证和授权流程可以分成:
上述前三点为spring security认证验证环节:
根据上述描述的过程,我们接下来主要去分析其中涉及的一下Component、Service、Filter。
SecurityContextHolder提供对SecurityContext的访问,存储security context(用户信息、角色权限等),而且其具有下列储存策略即工作模式:
修改SecurityContextHolder的工作模式有两种方法 :
在默认ThreadLocal策略中,SecurityContextHolder为静态方法获取用户信息为:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
但是一般不需要自身去获取。
其中getAuthentication()返回一个Authentication认证主体,接下来分析Authentication、UserDetails细节。
Spring Security使用一个Authentication对象来描述当前用户的相关信息,其包含用户拥有的权限信息列表、用户细节信息(身份信息、认证信息)。Authentication为认证主体在spring security中时最高级别身份/认证的抽象,常见的实现类UsernamePasswordAuthenticationToken。Authentication接口源码:
public interface Authentication extends Principal, Serializable {
//权限信息列表,默认GrantedAuthority接口的一些实现类
Collection<? extends GrantedAuthority> getAuthorities();
//密码信息
Object getCredentials();
//细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值
Object getDetails();
//通常返回值为UserDetails实现类
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
前面两个组件都涉及了UserDetails,以及GrantedAuthority其到底是什么呢?2.3小节分析。
UserDetails提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息,包含GrantedAuthority。其官方实现类为User,开发者可以实现其接口自定义UserDetails实现类。其接口源码:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetails与Authentication接口功能类似,其实含义即是Authentication为用户提交的认证凭证(账号密码),UserDetails为系统中用户正确认证凭证,在UserDetailsService中的loadUserByUsername方法获取正确的认证凭证。
其中在getAuthorities()方法中获取到GrantedAuthority列表是代表用户访问应用程序权限范围,此类权限通常是“role(角色)”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。GrantedAuthority接口常见的实现类SimpleGrantedAuthority。
AuthenticationManager是认证相关的核心接口,是认证一切的起点。但常见的认证流程都是AuthenticationManager实现类ProviderManager处理,而且ProviderManager实现类基于委托者模式维护AuthenticationProvider 列表用于不同的认证方式。例如:
AuthenticationProvider为
ProviderManager源码分析:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
//AuthenticationProvider列表依次认证
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
//每个AuthenticationProvider进行认证
result = provider.authenticate(authentication)
if (result != null) {
copyDetails(authentication, result);
break;
}
}
....
catch (AuthenticationException e) {
lastException = e;
}
}
//进行父类AuthenticationProvider进行认证
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication信息,则直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
//清除密码
((CredentialsContainer) result).eraseCredentials();
}
//发布登录成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
//如果都没认证成功,抛出异常
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
ProviderManager 中的AuthenticationProvider列表,会依照次序去认证,默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功,而且AuthenticationProvider认证成功后返回一个Authentication实体,并为了安全会进行清除密码。如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。
UserDetailsService接口作用是从特定的地方获取认证的数据源(账号、密码)。如何获取到系统中正确的认证凭证,通过loadUserByUsername(String username)获取认证信息,而且其只有一个方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
其常见的实现类从数据获取的JdbcDaoImpl实现类,从内存中获取的InMemoryUserDetailsManager实现类,不过我们可以实现其接口自定义UserDetailsService实现类,如下:
public class CustomUserService implements UserDetailsService {
@Autowired
//用户mapper
private UserInfoMapper userInfoMapper;
@Autowired
//用户权限mapper
private PermissionInfoMapper permissionInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfoDTO userInfo = userInfoMapper.getUserInfoByUserName(username);
if (userInfo != null) {
List<PermissionInfoDTO> permissionInfoDTOS = permissionInfoMapper.findByAdminUserId(userInfo.getId());
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
//组装权限GrantedAuthority object
for (PermissionInfoDTO permissionInfoDTO : permissionInfoDTOS) {
if (permissionInfoDTO != null && permissionInfoDTO.getPermissionName() != null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(
permissionInfoDTO.getPermissionName());
grantedAuthorityList.add(grantedAuthority);
}
}
//返回用户信息
return new User(userInfo.getUserName(), userInfo.getPasswaord(), grantedAuthorityList);
}else {
//抛出用户不存在异常
throw new UsernameNotFoundException("admin" + username + "do not exist");
}
}
}
AccessDecisionManager是由AbstractSecurityInterceptor调用,负责做出最终的访问控制决策。
AccessDecisionManager接口源码:
//访问控制决策
void decide(Authentication authentication, Object secureObject,Collection<ConfigAttribute> attrs)
throws AccessDeniedException;
//是否支持处理传递的ConfigAttribute
boolean supports(ConfigAttribute attribute);
//确认class是否为AccessDecisionManager
boolean supports(Class clazz);
SecurityMetadataSource包含着AbstractSecurityInterceptor访问授权所需的元数据(动态url、动态授权所需的数据),在AbstractSecurityInterceptor授权模块中结合AccessDecisionManager进行访问授权。其涉及了ConfigAttribute。
SecurityMetadataSource接口:
Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException;
Collection<ConfigAttribute> getAllConfigAttributes();
boolean supports(Class<?> clazz);
我们还可以自定义SecurityMetadataSource数据源,实现接口FilterInvocationSecurityMetadataSource。例:
public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
public List<ConfigAttribute> getAttributes(Object object) {
FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
String httpMethod = fi.getRequest().getMethod();
List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();
// Lookup your database (or other source) using this information and populate the
// list of attributes
return attributes;
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
为了存储安全,一般要对密码进行算法加密,而spring security提供了加密PasswordEncoder接口。其实现类有使用BCrypt hash算法实现的BCryptPasswordEncoder,SCrypt hashing 算法实现的SCryptPasswordEncoder实现类,实现类内部实现可看源码分析。而PasswordEncoder接口只有两个方法:
public interface PasswordEncoder {
//密码加密
String encode(CharSequence rawPassword);
//密码配对
boolean matches(CharSequence rawPassword, String encodedPassword);
}
FilterSecurityInterceptor是Spring security授权模块入口,该类根据访问的用户的角色,权限授权访问那些资源(访问特定路径应该具备的权限)。
FilterSecurityInterceptor封装FilterInvocation对象进行操作,所有的请求到了这一个filter,如果这个filter之前没有执行过的话,那么首先执行其父类AbstractSecurityInterceptor提供的InterceptorStatusToken token = super.beforeInvocation(fi),在此方法中使用AuthenticationManager获取Authentication中用户详情,使用ConfigAttribute封装已定义好访问权限详情,并使用AccessDecisionManager.decide()方法进行访问权限控制。
FilterSecurityInterceptor源码分析:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
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);
}
//回调其继承的抽象类AbstractSecurityInterceptor的方法
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
AbstractSecurityInterceptor源码分析:
protected InterceptorStatusToken beforeInvocation(Object object) {
....
//获取所有访问权限(url-role)属性列表(已定义在数据库或者其他地方)
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
....
//获取该用户访问信息(包括url,访问权限)
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
//进行授权访问
this.accessDecisionManager.decide(authenticated, object, attributes);
}catch
....
}
UsernamePasswordAuthenticationFilter使用username和password表单登录使用的过滤器,也是最为常用的过滤器。其源码:
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//获取表单中的用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
...
username = username.trim();
//组装成username+password形式的token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//交给内部的AuthenticationManager去认证,并返回认证信息
return this.getAuthenticationManager().authenticate(authRequest);
}
其主要代码为创建UsernamePasswordAuthenticationToken的Authentication实体以及调用AuthenticationManager进行authenticate认证,根据认证结果执行successfulAuthentication或者unsuccessfulAuthentication,无论成功失败,一般的实现都是转发或者重定向等处理,不再细究AuthenticationSuccessHandler和AuthenticationFailureHandle。兴趣的可以研究一下其父类AbstractAuthenticationProcessingFilter过滤器。
AnonymousAuthenticationFilter是匿名登录过滤器,它位于常用的身份认证过滤器(如UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter)之后,意味着只有在上述身份过滤器执行完毕后,SecurityContext依旧没有用户信息,AnonymousAuthenticationFilter该过滤器才会有意义——基于用户一个匿名身份。
AnonymousAuthenticationFilter源码分析:
public class AnonymousAuthenticationFilter extends GenericFilterBean implements
InitializingBean {
...
public AnonymousAuthenticationFilter(String key) {
this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//创建匿名登录Authentication的信息
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
...
}
chain.doFilter(req, res);
}
//创建匿名登录Authentication的信息方法
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
}
SecurityContextPersistenceFilter的两个主要作用便是request来临时,创建SecurityContext安全上下文信息和request结束时清空SecurityContextHolder。源码后续分析。
. AbstractAuthenticationProcessingFilter:主要处理登录
. FilterSecurityInterceptor:主要处理鉴权
经过上面对核心的Component、Service、Filter分析,初步了解了Spring Security工作原理以及认证和授权工作流程。Spring Security认证和授权还有很多负责的过程需要深入了解,所以下次会对认证模块和授权模块进行更具体工作流程分析以及案例呈现。最后以上纯粹个人结合博客和官方文档总结,如有错请指出!
最后可关注公众号【Ccww笔记】,一起学习。加群,每天会分享干货,还有学习视频领取!