SpringSecurity安全框架学习
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、springsecurity的过滤器链
对于以上的每个过滤器的作用,可参看spring security的官方文档spring security的官方文档进行学习。
3、用户认证流程
3.1 AbstractAuthenticationProcessingFilter过滤器
package org.sicFilterBean;
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
......
//用户认证成功后的结果处理
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
//用户认证失败后的结果处理
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
//对请求路径进行匹配,用户可在配置类中自定义
protected AbstractAuthenticationProcessingFilter(
RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher,
"requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
//核心代码
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//进行请求过滤地址的匹配
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
//Authentication:该对象为Spring Security方式的认证结果主体类
Authentication authResult;
try {
//最为核心的代码,其认证过程主要是依靠具体的子类实现,包括两部分,认证(常用的用户名+密码,手机号+验证码,邮箱+验证码),授权(用户权限的判定)
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
//认证结果放置于session会话中
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
//认证成功后的处理逻辑
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);
}
//将认证结果放置于SecurityContex及安全的上下文环境中
SecurityContextHolder.getContext().setAuthentication(authResult);
//记住我功能
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//调用AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler()的成功处理逻辑
successHandler.onAuthenticationSuccess(request, response, authResult);
}
//认证失败后的处理逻辑
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
//记住我
rememberMeServices.loginFail(request, response);
//调用AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler()的失败处理逻辑
failureHandler.onAuthenticationFailure(request, response, failed);
}
//使用认证管理器
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
//使用记住我服务
public void setRememberMeServices(RememberMeServices rememberMeServices) {
Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
this.rememberMeServices = rememberMeServices;
}
//session环境的管理
public void setSessionAuthenticationStrategy(
SessionAuthenticationStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
//认证成功的处理者
public void setAuthenticationSuccessHandler(
AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
//认证失败的处理者
public void setAuthenticationFailureHandler(
AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
}
AbstractAuthenticationProcessingFilter源码分析:
AbstractAuthenticationProcessingFilter 抽象类中的doFilter方法定义了一个完整的用户认证授权流程:
- 对请求的地址进行匹配,地址匹配则进行相应的认证权限校验,不匹配直接返回
//进行请求过滤地址的匹配
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
- 对请求进行认证及授权处理 ,这一步是由对应的子类实现去处理的,默认实现是UsernamePasswordAuthenticationFilter
authResult = attemptAuthentication(request, response);
- 将认证结果放置于session会话中
//认证结果放置于session会话中
sessionStrategy.onAuthentication(authResult, request, response);
- 如果在调用认证授权过程中出现任何异常,则会调用相应的认证失败的业务逻辑,unsuccessfulAuthentication,并且默认调用SimpleUrlAuthenticationFailureHandler接口对象的onAuthenticationFailure方法。
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
===================================================================================================================================
//InternalAuthenticationServiceException 异常,属于网络连接的异常信息,主要是用于 OAuth 认证服务器与业务服务器通信异常
//AuthenticationException 主要是指认证权限不通过的异常信息,springsecurity框架已经有部分实现,可通过继承关系进行查看,所以在我们代码过程中我们可以直接使用
===================================================================================================================================
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
//取消记住我
rememberMeServices.loginFail(request, response);
//调用AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler()的失败处理逻辑
failureHandler.onAuthenticationFailure(request, response, failed);
}
===========================================================================================================================
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
else {
saveException(request, exception);
if (forwardToDestination) {
logger.debug("Forwarding to " + defaultFailureUrl);
request.getRequestDispatcher(defaultFailureUrl)
.forward(request, response);
}
else {
logger.debug("Redirecting to " + defaultFailureUrl);
redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
}
}
}
- 如果在调用认证授权成功,则会调用相应的认证成功的业务逻辑,successfulAuthentication*,并且默认调用SavedRequestAwareAuthenticationSuccessHandler接口对象的onAuthenticationSuccess方法,因此用户可以自定义认证成功的处理结果业务逻辑,实现SavedRequestAwareAuthenticationSuccessHandler即可。
//认证成功后的处理逻辑
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);
}
//将认证结果放置于SecurityContex及安全的上下文环境中
SecurityContextHolder.getContext().setAuthentication(authResult);
//记住我功能
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//调用AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler()的成功处理逻辑
successHandler.onAuthenticationSuccess(request, response, authResult);
}
==========================================================================================================================
//将本次的request请求从会话缓存中取出
private RequestCache requestCache = new HttpSessionRequestCache();
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request
.getParameter(targetUrlParameter)))) {
requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
3.1 UsernamePasswordAuthenticationFilter过滤器(springsecurity认证授权的核心过滤器)
package org.springframework.security.web.authentication;
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// 默认页面表单取值为username和password,
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
// 默认的登录页面请求地址为login,post请求
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
//核心方法:用户名密码的请求认证
// ========================================================================================================
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
//用户名首尾去掉空格
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
//从页面参数中获取用户密码
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
//从页面参数中获取用户名
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
//设置用户的token信息
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
//给用户自定义用户名参数的方法
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
//给用户自定义用户密码参数的方法
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
//对于http请求参数的自定义
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
UsernamePasswordAuthenticationFilter源码分析
- 对于用户认证和权限的校验 ,该核心过滤器给定了请求参数的默认值,如url="/login",用户名和用户密码的取值参数,http的请求方式等,但都给用户自定义的空间。
- 核心代码:
username = username.trim();
//根据用户名和密码,生产权限认证的主要核心类
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 将本次请求的详细信息,包括用户访问协议、路径以及IP地址等放置在认证主体中供下一级的调用
setDetails(request, authRequest);
//真正的权限校验的方法
return this.getAuthenticationManager().authenticate(authRequest);
- UsernamePasswordAuthenticationToken的解读
看一下他的继承体系
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken
public abstract class AbstractAuthenticationToken implements Authentication,CredentialsContainer
所以说:Authentication 是一个接口,用来表示用户认证信息的,在用户登录认证之前相关信息会封装为一个 Authentication 具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。
- 深入理解(默认机制):
//认证开始之前,我们调用的是该构造器,因此默认的principa(需要认证的用户对象)为我们自己所实现的UserDetils
//而我们自定义的UserDetils实现了,其实主要也就是两个组成部分,一个是用户详情,一个是权限范围
//credentials(认证凭据)默认给定的就是用户密码
//认证状态给定的是false,未认证
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
- 深入理解(自定义):
//我们使用的是
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
//及我们已经通过密码规则的校验,现在进行权限的校验及分配
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
userDetails.getPassword(), userDetails.getAuthorities());
- AuthenticationManager 和 AuthenticationProvider
AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。
Authentication authenticate(Authentication authentication) throws AuthenticationException;
在 Spring Security 中,AuthenticationManager 的默认实现是 ProviderManager,而且它不直接自己处理认证请求,而是委托给其所配置的 AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果。如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException。校验认证请求最常用的方法是根据请求的用户名加载对应的 UserDetails,然后比对 UserDetails 的密码与认证请求的密码是否一致,一致则表示认证通过。Spring Security 内部的 DaoAuthenticationProvider 就是使用的这种方式。其内部使用 UserDetailsService 来负责加载 UserDetails,在认证成功以后会使用加载的 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext 中。
默认情况下,在认证成功后 ProviderManager 将清除返回的 Authentication 中的凭证信息,如密码。所以如果你在无状态的应用中将返回的 Authentication 信息缓存起来了,那么以后你再利用缓存的信息去认证将会失败,因为它已经不存在密码这样的凭证信息了。所以在使用缓存的时候你应该考虑到这个问题。一种解决办法是设置 ProviderManager 的 eraseCredentialsAfterAuthentication 属性为 false,或者想办法在缓存时将凭证信息一起缓存。
- AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
response.getWriter().flush();
}
- AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
response.getWriter().flush();
}
- 加入自定义的认证处理
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);