提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
关于SpringSecurity这方面网上的资源不是很多,而SpringSecurity流程比较复杂,参考了部分文章和自己看源码写了一篇笔记
提示:以下是本篇文章正文内容,下面案例可供参考
一、SpringSecurity是什么?
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。简单来说,就是一个使用了大量过滤器的安全认证框架
二、执行流程
大致流程:
1.拦截请求
在认证阶段最重要的一个过滤器是UsernamePasswordAuthenticationFilter
,通过他的构造方法可以拦截到所有路径为login,请求方法为post的请求
而这个方法最终请求到UsernamePasswordAuthenticationFilter
的父类AbstractAuthenticationProcessingFilter
(抽象类)中的一个方法
这个方法的底层调用了RequestMatcher
进行路劲的对比
2.封装对象并传递至AuthenticationManager
路径比对成功后在AbstractAuthenticationProcessingFilter
的doFilter方法(过滤器逻辑实现方法)中会调用attemptAuthentication方法,而该方法则是security认证中最重要逻辑
源码:
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
/**
obtainUsername和obtainPassword都是在请求中根据参数名(request.getParameter)
获取到账户和密码
*/
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
/**
将账户和密码封装为Authentication对象,里面有两个重要参数
Principal:相当于账户
Credential:相当于密码
**/
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
经过这个方法后,会将账户和密码封装为一个UsernamePasswordAuthenticationToken对象,最后把这个对象交给AuthenticationManager,并调用他的authenticate进行身份认证
3.加载用户信息及密码校验
经过封装后并传递至AuthenticationManager后,AuthenticationManager会调用他的服务提供者AbstractUserDetailsAuthenticationProvider
这里有几个重要的方法,主要的逻辑在AbstractUserDetailsAuthenticationProvider
的子类DaoAuthenticationProvider
中实现
- additionalAuthenticationChecks:进行密码比对
- authenticate:身份认证主要方法
- retrieveUser:获取用户
3.1 additionalAuthenticationChecks
additionalAuthenticationChecks执行的是密码比对,他有两个参数
- UserDetails:数据库中查询出来的用户数据封装而成
- authentication:前端传过来的参数经过
attemptAuthentication
封装而成的Authentication对象
如果想实现自己的密码校验逻辑可以继承并重写这个方法
源码
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"));
}
//authentication.getCredentials()用于获取前端传来的密码
String presentedPassword = authentication.getCredentials().toString();
//使用passwordEncoder中的matches方法进行密码比对,密码在数据库中是加密存储
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"));
}
}
3.2 retrieveUser
retrieveUser是用于获取数据库中的用户,他的主要执行逻辑在DetailsService中的loadUserByUsername方法中,在一般项目中我们是继承UserDetailsService并重写他的loadUserByUsername方法,将数据库中的User对象封装成security中的UserDetails对象,该对象将用于additionalAuthenticationChecks方法进行密码对比
源码:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//主要逻辑,获取数据库中的用户并封装为UserDetails
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);
}
}
3.3 authenticate
以上两个方法的逻辑都是在AbstractUserDetailsAuthenticationProvider
的子类DaoAuthenticationProvider
中实现的,而进行认证的主体部分是在AbstractUserDetailsAuthenticationProvider
中的authenticate方法实现
源码解读:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
//从authentication中获取用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
//该表示用于判断是否将用户信息存入了缓存
boolean cacheWasUsed = true;
//先从缓存中尝试获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
//缓存中没有用户信息
if (user == null) {
cacheWasUsed = false;
try {
/**
使用`AbstractUserDetailsAuthenticationProvider`
的子类`DaoAuthenticationProvider`中实现的retrieveUser方法加载数据库中的用户
主要逻辑在改方法中的loadUserByUsername方法
**/
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 {
//密码比对前检查用户信息
preAuthenticationChecks.check(user);
/**
使用`AbstractUserDetailsAuthenticationProvider`
的子类`DaoAuthenticationProvider`中实现的
additionalAuthenticationChecks方法进行密码比对
可重写`DaoAuthenticationProvider`中的
additionalAuthenticationChecks方法实现自己的密码比对逻辑
**/
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();
}
//对比成功返回该Authentication对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}
4.认证成功后的操作
至此认证成功,AbstractAuthenticationProcessingFilter
中的attemptAuthentication(此方法的逻辑在AbstractAuthenticationProcessingFilter的子类UsernamePasswordAuthenticationFilte中实现)执行完毕
校验成功后调用AbstractAuthenticationProcessingFilter中successfulAuthentication方法,此方法的作用是将用户信息存入security的上下文中(基于ThreadLoacl实现)方便后续调用
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);
}
三、总结
在security中最重要的方法莫过于DaoAuthenticationProvider中的additionalAuthenticationChecks方法(密码比对)和UserDetailsService中的loadUserByUsername方法(加载用户信息),要想完成自己的逻辑只需重写这些比较重要的方法即可