spring_security
介绍
spring_security 的分析主要包括认证和授权两个部分, 并且站在分析源码的角度探究整个执行过程以及原理。
- 认证
- 授权
组件版本
- spring boot 2.2.4.RELEASE
- springfox-swagger 2 2.4.0
- spring-boot-starter-data-jpa 2.2.4.RELEASE
- mysql 5.7
认证
认证
认证就是判断当前用户是否是一个合法用户, 即对比用户输入的用户名密码和数据库中的是否一致,但是这个查询过程是交由security来处理的,并且security会把当前的登陆用户保存到 中以便我们在全局都可以直接得到当前的登陆用户。
登陆分析
1、依赖
<!-- 以下是spring security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、配置
security 中的认证过程主要是帮助我们校验登录用户的合法性,其中框架本身已经帮助我们做出了大量的封装,登录界面以及校验逻辑都已经做完了,我们需要做的就是简单的配置使得security集成到当前的项目中~具体的集成方法不会描述,本文笔记重在描述整个认证的流程,**我们将从登录界面开始说起一点点深入。**前置的知识点,认证管理器、配置密码加密规则、认证。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置用户的服务信息
* // manager.createUser(User.withUsername("cyt").password("$2a$10$I9pgiXNbkct5cPlhHMvXfe4J7xk1akU6mIWArNTTAihvHUn1jSkpK").authorities("vip").build());
* // manager.createUser(User.withUsername("ccc").password("$2a$10$I9pgiXNbkct5cPlhHMvXfe4J7xk1akU6mIWArNTTAihvHUn1jSkpK").authorities("v").build());
* @return
*/
/* @Override
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
return manager;
}*/
/**
* 配置密码的规则
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()//开启登录配置
.antMatchers("/test").hasAnyAuthority("vip1") //表示访问 /test 这个接口,需要具备 vip1 这个角色
.antMatchers("/cyt").hasAnyAuthority("vip2")
.antMatchers("/cyt/test").hasAnyAuthority("vip3")
.anyRequest().permitAll() //表示剩余的其他接口,随便访问
.and()
.formLogin() // 登陆表单
.successForwardUrl("/success"); // 成功后的路由跳转
}
.formLogin() // 登陆表单
点开此源码
- getOrApply中传入的是一个配置, 然后本身是一个过滤器链 DefaultSecurityFilterChain
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
return getOrApply(new FormLoginConfigurer<>());
}
- 在这个配置中调用了父类了构造器并且传递一个认证过滤器
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
- UsernamePasswordAuthenticationFilter 中继续调用父类,并传递 AntPathRequestMatcher
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
- AbstractAuthenticationProcessingFilter 是一个过滤器, 看他的doFilter
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 authResult;
try {
// 尝试认证
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
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);
}
开始逐一分析上述的几个重要方法
attemptAuthentication
- UsernamePasswordAuthenticationFilter 实际调用子类
- 从请求中得到用户名和密码
- 封装成一个用户名密码认证token
- 放入 setDetails
- 交由认证管理器认证
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);
}
ProviderManager
认证管理器使用子类ProviderManager 中的认证方法, result = provider.authenticate(authentication); 在此方法中执行主要逻辑。其中ProviderManager 的认证方法会遍历所有的 AuthenticationProvider 子类然后逐一认证。这里我们关注 AbstractUserDetailsAuthenticationProvider
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider 本身是一个抽象类,方法会有子类实现, 在它的认证方法中先去缓存中获取用户,如果没有的话会 调用 retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);其中的authentication 就是之前认证管理器中传递的authRequest = new UsernamePasswordAuthenticationToken( username, password);
username 也就是UsernamePasswordAuthenticationToken 中的username.
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 关键代码
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);
// 关键代码
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();
}
// 关键代码
return createSuccessAuthentication(principalToReturn, authentication, user);
retrieveUser
DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
会默认调用DaoAuthenticationProvider 中的实现 在其方法中只是实现了一个逻辑: 根据用户名获得其用户信息返回一个 UserDetailss
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
loadUserByUsername
UserDetailsService 是一个接口 其中是有一个 loadUserByUsername方法, 查看到其子类的实现,有基于内存、基于jdbc以及缓存等,慢慢的就和上面的分析对上了。当然还有我们自己定义的实现。这里就找到了从数据库中查询用户信息交由security做认证的第一步。接下来还需要对比数据库中查询到的密码是否和用户在表单中输入的密码一致
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
- user loadUserByUsername 方法根据表单中的用户名从数据库中查询到的用户信息
- authentication security根据表单中的用户名密码封装成的token
这个方法才是对用户进行认证的,毕竟表单中输入的密可能是错误的噢
又回到了这个 AbstractUserDetailsAuthenticationProvider 然后调用子类 DaoAuthenticationProvider
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// 当前 token 无凭证
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
// 得到表单中的用户密码
String presentedPassword = authentication.getCredentials().toString();
// 更具之前配置的加密规则进行密码对比
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"));
}
}
这个方法执行完毕之后,会依次检查当前用户是否可以、是否过期信息、是否放入缓存。最后返回认证完毕的
UsernamePasswordAuthenticationToken
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
认证成功!!!此时还记得我们从那个地方开始认证的吗? ** 目前已经执行到了AbstractAuthenticationProcessingFilter** 中的 authResult = attemptAuthentication(request, response);
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
此时回到 AbstractAuthenticationProcessingFilter
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) {
// 这个 continueChainBeforeSuccessfulAuthentication 是个false
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
successfulAuthentication
- SecurityContextHolder.getContext().setAuthentication(authResult);
把认证的用户存入到SecurityContext 中,以便可以全局获取当前的用户 , 注意 authResult中是包含了我们当前的用户信息的。
- rememberMeServices.loginSuccess(request, response, authResult);
把认证的用户存入到记住我中
- successHandler.onAuthenticationSuccess(request, response, 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);
}
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);
}
认证时序图
重点过程
对于整个的认证过程中需要注意的有一下几个:
- UserDetails
- 其中存放的都是security 定义的当前用户信息。
- UsernamePasswordAuthenticationToken
- Authentication
- UserDetailsService
- UsernamePasswordAuthenticationFilter
图示