之前我们讲过了spring security的基本用法,我们应该也能大致了解他的作用,这里在重复一遍他的作用
- 认证,也就是去确认这个用户在我们系统中是否存在。
- 授权,这个可能有点混淆,但是总的来说就是用户是否有权限操作某个接口
对于第二个大家应该知道在security中有个叫hasrole的东西,也就是我们可以控制接口的权限,当我们为某个接口设置了admin的角色的权限时候,这时候只有这个用户为admin 的角色才能访问,如果不是admin的角色,即使登录成功后也无法访问。好像有点扯远了,直接进入主题吧
spring security过滤链
我们知道spring security是通过过滤链来实现认证和授权的,如果我们自己需要定义我们自己的过滤器,那么就有必要了解他内部的原理及实现,这样的话我们才能更好的实现自己的业务逻辑,spring security工作的过程如下图所示:
我们可以看到从username到最后的filtersecurity都是这个过滤链里面的一个。看到我们这个过滤链,可能就看到了里面的过滤器类,我们下面就会说下这些过滤器的作用:
- SecurityContextPersistenceFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- BasicAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- AnonymousAuthenticationFilter
-
SecurityContextPersistenceFilter 类
这个过滤器类的主要作用是SecurityContext的组装,供后续的过滤器链来使用,如果用户已经登录,再次访问其他资源时,会根据sessionId在session中将以前保存的SecurityContext取出,SecurityContext保存有用户的登录信息,那么就不需要用户再次登录了,只需要验证该用户是否有权限访问该资源即可。 通过他的源码可以看出
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
//他会通过request的请求获取当前的session,如果session不为空的话,就证明是已经登陆过的,这个时候就只需要判断他的权限就可以了,这边就直接放行。
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}
final boolean debug = logger.isDebugEnabled();
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//这个默认是为false
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
//如果是第一个次登录的即session为null,那么一定会走到这里,然后loadContext取出SecurityContext ,并将我们的数据存到这个SecurityContextHolder中
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
//执行完后将我们的数据给清空
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
从我们的代码描述应该知道他的作用吧。还有就是HttpSessionSecurityContextRepository 这个类,因为我们知道通过他获取SecurityContext 。
LogoutFilter类
这个过滤器类其实没要将,因为一看他的名字就知道他是个退出的过滤器
protected boolean requiresLogout(HttpServletRequest request,
HttpServletResponse response) {
return logoutRequestMatcher.matches(request);
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//通过判断这个请求是否是退出的请求,不是的话直接放行,如果是退出的请求的话就只想这里面的数据
if (requiresLogout(request, response)) {
//首先通过SecurityContextHolder拿取到上下文的Authentication 信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (logger.isDebugEnabled()) {
logger.debug("Logging out user '" + auth
+ "' and transferring to logout destination");
}
//此处的handler是一个SecurityContextLogoutHandler的实例
for (LogoutHandler handler : handlers) {
handler.logout(request, response, auth);
}
//这个的目的主要是退出成功后要做什么,自定义的LogoutHandler一定要实现LogoutSuccessHandler接口. logout成功后的具体操作写在onLogoutSuccess()方法里.
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
}
其实Authentication 这个接口类主要是关于以下的方法
而handler.logout(request, response, auth);方法主要是做了如下的事情
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
//默认为true,这里就是先清空session,然后在将SecurityContextHolder的数据清空
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
logger.debug("Invalidating session: " + session.getId());
session.invalidate();
}
}
//默认也为true,先将Authentication置位null,在清空
if (clearAuthentication) {
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
}
SecurityContextHolder.clearContext();
}
UsernamePasswordAuthenticationFilter过滤器类:
这个类的看名字也大致知道他是干嘛的,他主要用于验证用户名和密码的,也就是对我们登录传进来的用户名和密码进行验证。这个类非常重要,同时这个类里面的验证过程也稍微复杂一些,这边会一步一步讲解,首先还是先进入这个UsernamePasswordAuthenticationFilter的源码去看
//这个类中的这个方法是非常重要的,
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//通过obtain方法拿取到相应的用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//这边将用户名和密码进行进一步封装为UsernamePasswordAuthenticationToken,
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//然后将这个拿给AuthenticationManager(这个类是验证中很重要的一个)进行验证,并返回认证信息
return this.getAuthenticationManager().authenticate(authRequest);
}
而我们的验证则是通过AuthenticationManager的authenticate来实现的。我们可以发现其实AuthenticationManager是个接口类,他里面就只有这个authenticate方法,我们的验证就是在这个方法里面去执行的。而我们又有必要去看一下这个authenticate这个方法里面是怎么实现的,我们去查看的时候发现ProviderManager这个类实现了我们的AuthenticationManager接口,我们找到这个ProviderManager中的authenticate方法,这个方法虽然有点长,但是里面的逻辑却不是很难,我会在代码里面一一标注注释
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
//这里有个AuthenticationProvider接口类,这个接口类的作用主要是用来具体的校验,因为可能我们有多种验证方式,用户名和密码,邮箱和密码,手机号码和密码,等等,这里我们统统交给AuthenticationProvider来验证,这里我们有多少中验证方式就可能循环多少次(最差的),只要其中一种验证成功就ok
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
//这边通过验证后传出来的结果,如果结果不为空,那么就将结果copyDetails放到这个方法中,然后跳出循环
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, 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;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
//如果所有的验证都验证完,发现restult为空的话,那么
if (result == null && parent != null) {
// Allow the parent to try.
//没有验证通过,则使用父类型AuthenticationManager进行验证
try {
result = 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 = e;
}
}
//如果结果不为空,也就是认证成功了的话,则需要擦除敏感信息,用户密码
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
//然后发布验证成功的信息
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}"));
}
prepareException(lastException, authentication);
throw lastException;
}
通过上面的讲解应该大致知道他的运行了吧,但是我们还没有看到具体的验证方式,这时候我们需要去查看
result = provider.authenticate(authentication);这个方法,我们会找一个实列来讲,首先还是需要找到实现这个AuthenticationProvider接口的类,
我们直接那第二个类来看,我们直接看他的authenticate方法:
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;
//通过这个getUserFromCache方法获取用户信息,默认情况下从缓存中(UserCache接口实现)取出用户信息
UserDetails user = this.userCache.getUserFromCache(username);
//通过返回的user来看,如果user为null,证明内存中是没有数据的,那么就将cacheWasUsed 设置为false,供后面使用,因为我们知道我们那边的验证是通过for循环来验证,所以这边就有用了。
if (user == null) {
cacheWasUsed = false;
try {
//retrieveUser是抽象方法,通过子类来实现获取用户的信息,以UserDetails接口形式返回,这个方法之后我会继续讲一下,因为我们通过数据库方法获取的也就是这里。
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");
}
try {
//验证帐号是否锁定\是否禁用\帐号是否到期,这个是我们security自带的
preAuthenticationChecks.check(user);
//由子类来完成更进一步的验证
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
//下面这段代码体现了老外的细腻之处,意思是说如果在调用某个UserDetailsChecker接口的实现类验证失败后,就判断下用户信息是否从内存中得到,如果之前是从内存中得到的用户信息,那么考虑到可能数据是不实时的,就重新通过retrieveUser方法去取出用户信息,再次重复进行检查验证
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);
//如果没有缓存则进行缓存,则处的userCache是由NullUserCache类实现的,名如其义,该类的putUserInCache没做任何事
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//以下代码主要是把用户的信息和之前用户提交的认证信息重新组合成一个authentication实例返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
上面我们说了验证是先从内存中验证的,如果没有的话就通过retrieveUser方法去验证,我们看一下这个方法,他的实现类是DaoAuthenticationProvider,一看就是关于数据操作的(毕竟是dao吗),我们看他的里面的方法
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
//这个是具体的实现,其实也是最重要的,他通过loadUserByUsername去查数据,我们继续往里面看
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
这个方法其实最主要的也就这一句loadedUser = this.getUserDetailsService().loadUserByUsername(username);,我们继续往里面看,会发现loadUserByUsername其实是UserDetailsService接口下的一个方法,到这里我们就明白了,如果之后我们要自己查询数据,那么我们就必须要实现这个接口,然后重新写这个loadUserByUsername方法并返回UserDetails,其实这个UserDetails接口又被User给实现了,到这里我们的验证方法也就大致讲完了。,剩下得几个就直接在下一篇讲吧。