1. 简介
spring security的核心功能:认证和授权,认证就是身份验证(你是谁?),授权就是访问控制(你可以做什么?),同时他还提供很多安全管理的周边功能。在Spring Security的架构设计中,认证(Authentication)和授权(Authorization)是分开的,本篇文章主要涉及认证相关的功能和原理介绍。
2.认证核心组件
2.1 Authentication
当用户登录后,都会对应一个不同的Authentication实例,最常用的实现类是UsernamePasswordAuthenticationToken和RememberMeAuthenticationToken
public interface Authentication extends Principal, Serializable {
//获取用户权限列表
Collection<? extends GrantedAuthority> getAuthorities();
//获取用户凭证,一般来说就是密码,认证成功后密码一般会被删除
Object getCredentials();
//获取用户的详细信息,可能是当前请求之类等
Object getDetails();
//获取当前用户,例如一个用户名或者一个用户对象
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
2.2 AuthenticationManager
authenticate方法用来做认证,返回Authentication表示认证成功,抛出异常表示用户输入无效的凭证,返回null表示不能确定
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
2.3 ProviderManager
ProviderManager是AuthenticationManager的实现类,ProviderManager管理众多AuthenticationProvider实例(详见2.4节),AuthenticationProvider会有一个supports方法,用来判断当前AuthenticationProvider是否支持此次认证请求。
ProviderManager的大致认证流程就是遍历众多AuthenticationProvider实例(例如有的是表单登陆,有的是短信验证码登陆)。
//省略部分代码
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private AuthenticationEventPublisher eventPublisher;
private List<AuthenticationProvider> providers;
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication;
public ProviderManager(AuthenticationProvider... providers) {
this(Arrays.asList(providers), (AuthenticationManager)null);
}
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, (AuthenticationManager)null);
}
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
}
//核心部分,省略部分代码
public Authentication authenticate(Authentication authentication) throws AuthenticationExceptio {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
//如果providers都不能处理,由parent处理
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
//擦除密码,发布事件
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
}
}
2.4 AuthenticationProvider
AuthenticationProvider有点类似AuthenticationManager,但是多了一个supports方法用来判断是否支持给定的Authentication类型
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
2.5 UserDetails
规范开发者自定义用户对象
public interface UserDetails extends Serializable {
//返回当前账户具备的权限
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
2.6 UserDetailsService
负责提供用户数据源,提供查询用户的方法
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
3. Filter
在Spring Security中,认证和授权都是基于过滤器来完成的,过滤器通过FilterChainProxy来统一代理,然后将FilterChainProxy嵌入到web项目的原生过滤器中。FilterChainProxy本身通过Spring框架提供的DelegatingFilterProxy整合到原生过滤器链中。
3.1 默认的Filter顺序
其次序定义在FilterOrderRegistration类中
类 | 次序 |
DisableEncodeUrlFilter | 100 |
ForceEagerSessionCreationFilter | 200 |
ChannelProcessingFilter | 300 |
WebAsyncManagerIntegrationFilter | 500 |
SecurityContextHolderFilter | 600 |
SecurityContextPersistenceFilter | 700 |
HeaderWriterFilter | 800 |
CorsFilter | 900 |
CsrfFilter | 1000 |
LogoutFilter | 1100 |
OAuth2AuthorizationRequestRedirectFilter | 1200 |
Saml2WebSsoAuthenticationRequestFilter | 1300 |
X509AuthenticationFilter | 1400 |
AbstractPreAuthenticatedProcessingFilter | 1500 |
CasAuthenticationFilter | 1600 |
OAuth2LoginAuthenticationFilter | 1700 |
Saml2WebSsoAuthenticationFilter | 1800 |
UsernamePasswordAuthenticationFilter | 1900 |
DefaultLoginPageGeneratingFilter | 2100 |
DefaultLogoutPageGeneratingFilter | 2200 |
ConcurrentSessionFilter | 2300 |
DigestAuthenticationFilter | 2400 |
BearerTokenAuthenticationFilter | 2500 |
BasicAuthenticationFilter | 2600 |
RequestCacheAwareFilter | 2700 |
SecurityContextHolderAwareRequestFilter | 2800 |
JaasApiIntegrationFilter | 2900 |
RememberMeAuthenticationFilter | 3000 |
AnonymousAuthenticationFilter | 3100 |
OAuth2AuthorizationCodeGrantFilter | 3200 |
SessionManagementFilter | 3300 |
ExceptionTranslationFilter | 3400 |
FilterSecurityInterceptor | 3500 |
AuthorizationFilter | 3600 |
SwitchUserFilter | 3700 |
4. 获取用户登录信息
如果不使用安全框架,可以通过HttpSession读写用户数据,Spring Security对HttpSession的用户信息进行了封装,开发者获取用户信息的方式有两种:
- 从SecurityContextHoler中获取
- 从当前请求对象中获取
4.1 从SecurityContextHoler中获取
@Controller
public class HelloController {
@RequestMapping(method = RequestMethod.GET, path = "principal")
@ResponseBody
public Object principal() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Object principal = authentication.getPrincipal();
return principal;
}
}
4.2 从当前请求对象中获取
@RequestMapping("/authentication")
public void authentication(Authentication authentication) {
System.out.println("authentication = " + authentication);
}
@RequestMapping("/principal")
public void principal(Principal principal, HttpServletRequest req) {
System.out.println("req.getClass() = " + req.getClass());
System.out.println("principal = " + principal);
}
@RequestMapping("/info")
public void info(HttpServletRequest req) {
String remoteUser = req.getRemoteUser();
Authentication auth = ((Authentication) req.getUserPrincipal());
boolean admin = req.isUserInRole("admin");
System.out.println("remoteUser = " + remoteUser);
System.out.println("auth.getName() = " + auth.getName());
System.out.println("admin = " + admin);
}