首页 > 编程语言 >SpringSecurity系列,第四章:源码分析

SpringSecurity系列,第四章:源码分析

时间:2024-01-17 15:26:45浏览次数:33  
标签:登录 用户 SpringSecurity Authentication 源码 new 认证 public 第四章

源码分析

Spring Security 的核心功能即为:认证(Authentication)、授权(Authorization)

一、概览

1、在 Spring Security 中,用户的认证信息主要由 Authentication 的实现类来保存,

Authentication 接口定义如下:【保存用户认证信息】
    
public interface Authentication extends Principal, Serializable {
     Collection<? extends GrantedAuthority> getAuthorities();//获取用户权限
     Object getCredentials();//获取用户凭证,一般是密码
     Object getDetails();//获取用户携带的详细信息
     Object getPrincipal();//获取当前用户
     boolean isAuthenticated();//当前用户是否认证成功
     void setAuthenticated(boolean isAuthenticated);
}
2、Spring Security 中的认证工作主要由 AuthenticationManager 接口来负责

public interface AuthenticationManager {
  Authentication authenticate(Authentication authentication)   //用于做认证
      throws AuthenticationException;
}

AuthenticationManager 最主要的实现类是 ProviderManager,  
providerManager里有 List<AuthenticationProvider> providers,用于管理多个authenticationProvider.
    
在一次完整的认证流程中,可能会同时存在多个 AuthenticationProvider(例如,项目同时
支持 form 表单登录和短信验证码登录),多个 AuthenticationProvider 统一由 ProviderManager
来管理。同时,ProviderManager 具有一个可选的 parent,如果所有的 AuthenticationProvider
都认证失败,那么就会调用 parent 进行认证
3、Spring Security 的授权体系中,有两个关键接口
	AccessDecisionManager 是一个决策器,来决定此次访问是否被允许。
	AccessDecisionVoter  是一个投票器,投票器会检查用户是否具备应有的角色。
    
在 AccessDecisionManager中会挨个遍历 AccessDecisionVoter,进而决定是否允许用户访问。

二、认证

1、核心类

①、AuthenticationManager与Authentication

AuthenticationManager作为整个身份验证核心最外层的封装负责与外部使用者进行交互。
AuthenticationManager接口有且仅有一个对外的服务便是“身份验证”。

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

外部使用者通过将身份验证的必要信息,比如用户名和密码封装一个Authentication传递、调用AuthenticationMananger的
authenticate方法。如果没有返回异常和null值,验证服务便是完成。
完成身份验证的Authentication不仅包含用户的身份验证信息,比如用户名,还会将该用户身份下所有对应的权限列表也一并封装返回。

ProviderManager是AuthenticationManager的一个重要实现。

为了向外部提供身份验证服务,Spring Security通过ProviderMananger实现AuthenticationManager的身份验证接口。
【ProviderManager中管理了 多个 AuthenticationProvider】


认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。

AuthenticationManager 中可以定义有多个 AuthenticationProvider。
当我们使用 authenticationProvider 元素来定义一个 AuthenticationProvider 时需要实现AuthenticationProvider接口,
如果没有指定对应关联的 AuthenticationProvider 对象,Spring Security 【默认会使用 DaoAuthenticationProvider】。

DaoAuthenticationProvider 在进行认证的时候需要一个 UserDetailsService 来获取用户的信息 
UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证的方式,
我们可以实现自己的 AuthenticationProvider;
如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。

Authentication的职责有两个:
· 第一个是封装验证请求的参数
· 第二个便是封装用户的权限信息


principal用于存放用户的身份标识信息,比如用户名,
credentials用于存放用户的验证凭证比如密码,
authorities用于存放用户的权限列表
details则存放除了用户名和密码其他可能会被用于身份验证的信息,比如应用限定用户的使用ip范围场景下,ip信息可能便会被存放在details做辅
       助的验证信息使用。
##下面我们重写一个用户认证逻辑

@Service
public class AuthenticationProviderService implements AuthenticationProvider {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private SCryptPasswordEncoder sCryptPasswordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        //根据用户名从数据库中获取CustomUserDetails
        CustomUserDetails user = userDetailsService.loadUserByUsername(username);

        //根据所配置的密码加密算法来分别验证用户密码
        switch (user.getUser().getPasswordEncoderType()) {
            case BCRYPT:
                return checkPassword(user, password, bCryptPasswordEncoder);
            case SCRYPT:
                return checkPassword(user, password, sCryptPasswordEncoder);
        }

        throw new  BadCredentialsException("Bad credentials");
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
    }

    private Authentication checkPassword(CustomUserDetails user, String rawPassword, PasswordEncoder encoder) {
        if (encoder.matches(rawPassword, user.getPassword())) {
            return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
        } else {
            throw new BadCredentialsException("Bad credentials");
        }
    }
}

###重写用户查询校验逻辑
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public CustomUserDetails loadUserByUsername(String username) {
		
        //JPA实现
        Supplier<UsernameNotFoundException> s =
                () -> new UsernameNotFoundException("Username" + username + "is invalid!");

        User u = userRepository.findUserByUsername(username).orElseThrow(s);

        return new CustomUserDetails(u);
    }
}

②、UserDetails

UserDetails 接口来规范开发者自定义的用户对象

public interface UserDetails extends Serializable {
     Collection<? extends GrantedAuthority> getAuthorities(); //返回当前账户所具备的权限
     String getPassword();//返回当前账户的密码
     String getUsername();//返回当前账户的用户名。
     boolean isAccountNonExpired();//返回当前账户是否未过期。
     boolean isAccountNonLocked();//返回当前账户是否未锁定。
     boolean isCredentialsNonExpired();//返回当前账户凭证(如密码)是否未过期。
     boolean isEnabled();//返回当前账户是否可用。
}

③、UserDetailsService

##UserDetailsService 负责提供【用户数据源】的接口.

public interface UserDetailsService {
     UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

loadUserByUsername 有一个参数是 username,这是用户在认证时传入的用户名,这里拿到用户名之后,再去数据库中查询用户,
最终返回一个 UserDetails 实例。

【一般需要开发者 自行实现 UserDetailsService 的loadUserByUsername方法。如果未实现,走SpringSecurity提供的默认实现】
【默认的实现是InMemoryUserDetailsManager,它继承自UserDetailsManager】

其中JdbcUserDetailsManagerr 实现对用户的增删改查操作,这些操作都会持久化到数据库中。不过 JdbcUserDetailsManager 
有一个局限性,就是操作数据库中用户的 SQL都是提前写好的,不够灵活,因此在实际开发中 JdbcUserDetailsManager 使用幵不多。
【它定义了 很多 数据库的字段,都是写死的,对一些已经定义好数据结构的系统不太友好】

针对 UserDetailsService 的自动化配置类是 UserDetailsServiceAutoConfiguration。
UserDetailsServiceAutoConfiguration中


@ConditionalOnMissingBean(
    value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class}
)
public class UserDetailsServiceAutoConfiguration {
  
    @Bean
    @ConditionalOnMissingBean(
        type = {"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"}
    )
    @Lazy
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(
							SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
 			//.....
    }

    private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
            //....
    }
}
                                
可以看出当,系统默认没有实现AuthenticationManager, AuthenticationProvider, UserDetailsService,ClientRegistrationRepository
的时候会默认执行InMemoryUserDetailsManager基于内存的用户实现。
                                
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
     private User user = new User();
    
     public User getUser() {
     	return this.user;
     }
                                
     public static class User {
                                
     	private String name = "user";
                                
     	private String password = UUID.randomUUID().toString();
                                
     	private List<String> roles = new ArrayList<>();
     		//省略 getter/setter
     	}
}
                                
默认用户是user,默认密码是 UUID,即控制台 输出的那个UUID。

我们可以通过在配置文件中增加下面的内容,来修改默认的用户名和密码以及角色
spring.security.user.name=admin
spring.security.user.password=admin123
spring.security.user.roles=admin,user

④、DefaultLoginPageGeneratingFilter

DefaultLoginPageGeneratingFilter指定和生成默认登录页面

public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
   
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        boolean loginError = this.isErrorPage(request); //登录错误
        boolean logoutSuccess = this.isLogoutSuccess(request); //登出
        if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
            chain.doFilter(request, response);
        } else {
            String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess); //生成一个登录页面的html
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().write(loginPageHtml);
        }
    }

}

⑤、DefaultLogoutPageGeneratingFilter

DefaultLogoutPageGeneratingFilter指定和生成默认的登出页面

public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter {
   
   private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET");

   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain){
        if (this.matcher.matches(request)) {
            this.renderLogout(request, response); //生成一个登出页面
        } else {
            filterChain.doFilter(request, response);
        }
   }

   
	private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String page = "<!DOCTYPE html>xxxx登出页面xxxx</html>";
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(page);
    }

}

⑥、Spring Security的配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
             .anyRequest().authenticated()
             .and()
             .formLogin()
             .loginPage("/login.html")
             .loginProcessingUrl("/doLogin")
             .defaultSuccessUrl("/index")
              //.successForwardUrl("/index")
             .failureUrl("/login.html")
             .usernameParameter("uname")
             .passwordParameter("passwd")
             .permitAll()
             .and()
             .csrf().disable();
    }
}

## authorizeRequests()方法表示开启权限配置
## anyRequest().authenticated()表示所有的请求都要认证之后才能访问
## and()方法会返回 HttpSecurityBuilder 对象的一个子类(实际上就是 HttpSecurity)。所以 and()方法相当于又回到 HttpSecurity 实例,
   重新开启新 一 轮 的 配 置。
   如果觉得and()方法很难理解 , 也可以不用and()方法 ,在.anyRequest().authenticated()配置完成后直接用分号(;)结束,
   然后通过 http.formLogin()继续配置表单登录。
## formLogin() 表 示 开 启 表单 登录 配 置
## loginPage 用 来配 置 登录页 面 地址
## loginProcessingUrl 用来配置登录接口地址

## defaultSuccessUrl 和 successForwardUrl 表示登录成功后的跳转地址
   defaultSuccessUrl 表示当用户登录成功之后,会自动重定向到登录之前的地址上。
      如果用户本身就是直接访问的登录页面,则登录成功后就会重定向到 defaultSuccessUrl 指定的页面中。
   successForwardUrl 则不会考虑用户之前的访问地址,只要用户登录成功,就会通过服务器端跳转到 successForwardUrl 所指定的页面。

## failureUrl 表示登录失败后的跳转地址
## usernameParameter 表示登录用户名的参数名称,不设置就会用默认的用户名
## passwordParameter 表示登录密码的参数名称
## permitAll 表示跟登录相关的页面和接口不做拦截,直接通过。

## csrf().disable()表示禁用 CSRF 防御功能

⑦、AuthenticationSuccessHandler 登录成功

Spring Security 中专门提供了 AuthenticationSuccessHandler 接口用来处理【登录成功】事项。
无论是 defaultSuccessUrl 还是 successForwardUrl,最终所配置的都是 AuthenticationSuccessHandler 接口的实例。

6-1、当通过defaultSuccessUrl设置登陆成功后跳转地址时,实际上对应的实现类就是
    【SaveRequestAwareAuthenticationSuccessHandler】

public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private RequestCache requestCache = new HttpSessionRequestCache();

    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        SavedRequest savedRequest = this.requestCache.getRequest(request, response);
        if (savedRequest == null) {
            super.onAuthenticationSuccess(request, response, authentication);
        } else {
            String targetUrlParameter = this.getTargetUrlParameter();
            if (!this.isAlwaysUseDefaultTargetUrl() && (targetUrlParameter == null || !StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
                this.clearAuthenticationAttributes(request);
                String targetUrl = savedRequest.getRedirectUrl();
                this.logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
                this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
            } else {
                this.requestCache.removeRequest(request, response);
                super.onAuthenticationSuccess(request, response, authentication);
            }
        }
    }
  //...
}

###代码分析
1、首先从requestCache中获取缓存下来的请求,如果requestCache为空,表明用户访问登录页面之前没有访问其他页面。
   此时直接调通super.onAuthenticationSuccess方法处理,会直接跳转至defaultSuccessUrl指定的地址。

2、targetUrlParameter 是用户显式指定的 登录成功后访问的地址。如果targetUrlParameter存在且 用户设置了
   alwaysUseDefaultTargetUrl为TRUE,则重定向到defaultSuccessUrl指定的地址。

3、如果前面条件都不满足,则从requestCache中获取之前缓存下来的请求地址,然后进行重定向。
6-2、当通过successForwardUrl设置登陆成功后跳转地址时,实际上对应的实现类就是
    【ForwardAuthenticationSuccessHandler】

public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private final String forwardUrl;

    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        request.getRequestDispatcher(this.forwardUrl).forward(request, response);
    }
}

##代码分析:
1、代码特别简单就是request,getRequestDispatcher(),一个服务转发。登录成功后直接跳转至susccessForwardUrl指定地址。
6-3、AuthenticationSuccessHandler默认的三个实现类都是用来处理页面跳转的。

   但是在前后端分离的开发中,用户登录成功后就不需要再转发页面跳转了,只需要给前端返回一个json数据即可。告诉前端登录成功或者失败,
由前端自行处理跳转的页面。
   【这样我们就需要自行定义AuthenticationSuccessHandler的实现类了】。

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
 
  @Override
  public void onAuthenticationSuccess(HttpServletRequest request,  HttpServletResponse response,         
     Authentication authentication) throws IOException, ServletException {
         response.setContentType("application/json;charset=utf-8");
         Map<String, Object> resp = new HashMap<>();
         resp.put("status", 200);
         resp.put("msg", "登录成功!");
         ObjectMapper om = new ObjectMapper();
         String s = om.writeValueAsString(resp);
         response.getWriter().write(s);
     }
  }
             
在自定义的 MyAuthenticationSuccessHandler 中,重写 onAuthenticationSuccess 方法,在该方法中,通过HttpServletResponse
对象返回一段登录成功的 JSON 字符串给前端即可。最后,在 SecurityConfig 中配置自定义的 MyAuthenticationSuccessHandler.
             
             
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
         .anyRequest().authenticated()
         .and()
         .formLogin()
         .loginPage("/login.html")
         .loginProcessingUrl("/doLogin")
         .successHandler(new MyAuthenticationSuccessHandler()) //登录成功handler
         .failureUrl("/login.html")
         .usernameParameter("uname")
         .passwordParameter("passwd")
         .permitAll()
         .and()
         .csrf().disable();
    }
}

⑧、AuthenticationFailureHandler登录失败

上面SecurityConfig.configure配置中failureUrl 表示登录失败后重定向到 mylogin.html 页面.
重定向是一种客户端行为不方便携带失败的异常信息(如果我们要把失败信息显式到登录页面上的话)。
我们可以 使用 .failureForwardUrl("/mylogin.html")来代替.failureUrl("/login.html")。failureForwardUrl跳转是一种
服务端行为。好处是可以携带登录异常信息。

无论是 failureForwardUrl 还是 failureUrl,最终所指向的都是AuthenticationFailureHandler. springSecurity指定
AuthenticationFailureHandler来规范登录失败的实现。

public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException ex);
}

##其中 AuthenticationException 表示登录失败的异常信息

AuthenticationFailureHandler共有五个实现类:

1> SimpleUrlAuthenticationFailureHandler 默认的处理逻辑就是通过重定向跳转到登录页面,当然也可以通过配置
forwardToDestination属性将重定向改为服务器端跳转,failureUrl方法的底层实现逻辑就是SimpleUrlAuthenticationFailureHandler。

2> ExceptionMappingAuthenticationFailureHandler 可以实现根据不同的异常类型,映射到不同的路径。

3> ForwardAuthenticationFailureHandler 表示通过服务器端跳转来重新回到登录页面,failureForwardUrl 方法的底层实现逻辑就是
ForwardAuthenticationFailureHandler。

4> AuthenticationEntryPointFailureHandler 是 Spring Security5.2 新引进的处理类,可以通过 AuthenticationEntryPoint 来处理登录异常。

5> DelegatingAuthenticationFailureHandler 可以实现为不同的异常类型配置不同的登录失败处理回调。
如果是前后端分离开发,登录失败时就不需要页面跳转了,只需要返回 JSON 字符串给前端即可,此时可以通过自定义 AuthenticationFailureHandler 的实现类来完成。

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
 
 @Override
 public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse             
    resp,AuthenticationException ex)  throws IOException, ServletException {

         response.setContentType("application/json;charset=utf-8");
         Map<String, Object> resp = new HashMap<>();
         resp.put("status", 500);
         resp.put("msg", "登录失败!" + exception.getMessage());
         ObjectMapper om = new ObjectMapper();
         String s = om.writeValueAsString(resp);
         response.getWriter().write(s);
             
     }

}
##然后在 SecurityConfig 中进行配置即可
@Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
             http.authorizeRequests()
             .anyRequest().authenticated()
             .and()
             .formLogin()
             .loginPage("/mylogin.html")
             .loginProcessingUrl("/doLogin")
             .defaultSuccessUrl("/index.html")
             .failureHandler(new MyAuthenticationFailureHandler()) //登录失败handler
             .usernameParameter("uname")
             .passwordParameter("passwd")
             .permitAll()
             .and()
             .csrf().disable();
     }
}            

⑨、logoutSuccessHandler注销登录

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
             http.authorizeRequests()
             .anyRequest().authenticated()
             .and()
             .formLogin()
             //省略其他配置
             .and()
             .logout()
             .logoutUrl("/logout")
             .invalidateHttpSession(true)
             .clearAuthentication(true)
             .logoutSuccessUrl("/mylogin.html")
             .and()
             .csrf().disable();
     }
}

1>、通过logout方法开启注销登录配置
2>、logoutUrl指定了注销登录请求地址,默认是GET请求,路径为/logout
3>、invalidateHttpSession标识是否使Session失效,默认true
4>、clearAuthentication是否清除认证信息,默认true
5>、logoutSuccessUrl标识注销成功后的跳转地址

在浏览器中输入 http://localhost:8080/logout 就可以发起注销登录请求了。注销成功后,会自动跳转到 mylogin.html 页面。
如果项目有需要,开发者也可以配置多个注销登录的请求,同时还可以指定请求的方法:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
         .anyRequest().authenticated()
         .and()
         .formLogin()
         //省略其他配置
         .and()
         .logout()
         .logoutRequestMatcher(new OrRequestMatcher(
         		new AntPathRequestMatcher("/logout1", "GET"),
        		new AntPathRequestMatcher("/logout2", "POST")
             )
		   )
         .invalidateHttpSession(true)
         .clearAuthentication(true)
         .logoutSuccessUrl("/mylogin.html")
         .and()
         .csrf().disable();
     }
}
上面这个配置表示注销请求路径有两个:
· 第一个是/logout1,请求方法是 GET。
· 第二个是/logout2,请求方法是 POST。
使用任意一个请求都可以完成登录注销。

##如果开发者希望为不同的注销地址返回不同的结果,也是可以的,配置如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
             http.authorizeRequests()
             .anyRequest().authenticated()
             .and()
             .formLogin()
             //省略其他配置
             .and()
             .logout()
             .logoutRequestMatcher(new OrRequestMatcher(
                     new AntPathRequestMatcher("/logout1", "GET"),
                     new AntPathRequestMatcher("/logout2", "POST")
				)
              )
             .invalidateHttpSession(true)
             .clearAuthentication(true)
             .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                     resp.setContentType("application/json;charset=utf-8");
                     Map<String, Object> result = new HashMap<>();
                     result.put("status", 200);
                     result.put("msg", "使用 logout1 注销成功!");
                     ObjectMapper om = new ObjectMapper();
                     String s = om.writeValueAsString(result);
                     resp.getWriter().write(s);
             },new AntPathRequestMatcher("/logout1","GET"))
             
             .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                     resp.setContentType("application/json;charset=utf-8");
                     Map<String, Object> result = new HashMap<>();
                     result.put("status", 200);
                     result.put("msg", "使用 logout2 注销成功!");
                     ObjectMapper om = new ObjectMapper();
                     String s = om.writeValueAsString(result);
                     resp.getWriter().write(s);
             },new AntPathRequestMatcher("/logout2","POST"))
             .and()
             .csrf().disable();
     }
}
通过 defaultLogoutSuccessHandlerFor 方法可以注册多个不同的注销成功回调函数,该方法第一个参数是注销成功回调,
第二个参数则是具体的注销请求
前后端分离的架构,注销成功后就不需要页面跳转了,只需将注销成功的信息返回给前端即可。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
         .anyRequest().authenticated()
         .and()
         .formLogin()
         //省略其他配置
         .and()
         .logout()
         .logoutRequestMatcher(new OrRequestMatcher(
                 new AntPathRequestMatcher("/logout1", "GET"),
                 new AntPathRequestMatcher("/logout2", "POST")
			)
		 )
         .invalidateHttpSession(true)
         .clearAuthentication(true)
         .logoutSuccessHandler((req,resp,auth)->{
                 resp.setContentType("application/json;charset=utf-8");
                 Map<String, Object> result = new HashMap<>();
                 result.put("status", 200);
                 result.put("msg", "注销成功!");
                 ObjectMapper om = new ObjectMapper();
                 String s = om.writeValueAsString(result);
                 resp.getWriter().write(s);
          })
         .and()
         .csrf().disable();
     }
}

2、登录用户数据获取

①、相关对象和概念

登录成功之后,在后续的业务逻辑中,开发者可能还需要获取登录成功的用户对象,如果不使用任何安全管理框架,那么可以将用户信息保存在
HttpSession 中,以后需要的时候直接从 HttpSession 中获取数据。在 Spring Security 中,用户登录信息本质上还是保存在
HttpSession 中,但是为了方便使用,Spring Security 对 HttpSession 中的用户信息进行了封装,封装之后,开发者若再想获取用户登录
数据就会有两种不同的思路:
(1)从 SecurityContextHolder 中获取。
(2)从当前请求对象中获取。

无论是哪种获取方式,都离不开一个重要的对象:Authentication。
在 Spring Security 中,Authentication 对象主要有两方面的功能:
(1)作为 AuthenticationManager 的输入参数,提供用户身份认证的凭证,当它作为一个输入参数时,它的 isAuthenticated 方法返回 
    false,表示用户还未认证。
(2)代表已经经过身份认证的用户,此时的 Authentication 可以从 SecurityContext 中获取。

②、Authentication

public interface Authentication extends Principal, Serializable {
	
    Collection<? extends GrantedAuthority> getAuthorities();//获取用户权限

    Object getCredentials();//获取用户凭证,一般是密码

    Object getDetails();//获取用户的详细信息,可能是当前请求之类

    Object getPrincipal();//获取当前用户信息,可以是一个用户名、也可以是一个用户对象

    boolean isAuthenticated();//当前用户是否认证成功

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

##Authentication最常用的两个实现类是 UsernamePasswordAuthenticationToken 和 RememberMeAuthenticationToken

③、SecurityContextHolder

我们可以从Security上下文中(SecurityContextHolder)获取Authentication对象:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

## SecurityContextHolder中 存储的是SecurityContext, SecurityContext中存储的才是Authentication。
##源码如下:

public class SecurityContextHolder {

    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
	
	
    public static final String SYSTEM_PROPERTY = "spring.security.strategy";
    private static String strategyName = System.getProperty("spring.security.strategy");
    private static SecurityContextHolderStrategy strategy;
    private static int initializeCount = 0;

    public SecurityContextHolder() {
    }

    public static void clearContext() {
        strategy.clearContext();
    }

    public static SecurityContext getContext() {
        return strategy.getContext();
    }

    public static int getInitializeCount() {
        return initializeCount;
    }

    private static void initialize() {
        //设置strategy  存储策略,默认使用MODE_THREADLOCAL
    }

    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }

    public static void setStrategyName(String strategyName) {
        SecurityContextHolder.strategyName = strategyName;
        initialize();
    }

    public static SecurityContextHolderStrategy getContextHolderStrategy() {
        return strategy;
    }

    public static SecurityContext createEmptyContext() {
        return strategy.createEmptyContext();
    }

    static {
        initialize();
    }
}

##三种存储策略

(1)MODE_THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal中。【默认存储策略】
   意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。
    【ThreadLocalSecurityContextHolderStrategy】

(2)MODE_INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境。存放在 InheritableThreadLocal 中。
   如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
    【InheritableThreadLocalSecurityContextHolderStrategy】


(3)MODE_GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,
   在 JavaWeb 开发中,这种模式很少使用到。
    【GlobalSecurityContextHolderStrategy】

④、SecurityContextHolderStrategy存储策略

为了规范存储策略的实现,security定义了 SecurityContextHolderStrategy 接口:

public interface SecurityContextHolderStrategy {

    void clearContext(); //用来 清除存储的SecurityContext对象

    SecurityContext getContext();//用来 获取存储的SecurityContext对象

    void setContext(SecurityContext var1);//用来设置存储的SecurityContext对象

    SecurityContext createEmptyContext();//创建一个空的SecurityContext对象
}


如果想在子线程中获取到登录用户信息,可以在启动参数中修改 存储策略:
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

⑤、SecurityContextRepository

将 SecurityContext 存入 HttpSession,或者从 HttpSession 中加载数据并转为 SecurityContext 对象,
这些事情都是由 SecurityContextRepository 接口的实现类完成的

public interface SecurityContextRepository {

    //加载SecurityContext对象,没有登录的用户会返回一个没有Authentication的"空"SecurityContext对象。
    SecurityContext loadContext(HttpRequestResponseHolder holder);

    //保存一个SecurityContext对象
    void saveContext(SecurityContext context, HttpServletRequest req, HttpServletResponse resp);

    //判断SecurityContext对象是否存在
    boolean containsContext(HttpServletRequest req);
}

【HttpSessionSecurityContextRepository是默认的实现类】

⑥、SecurityContextPersistenceFilter

在 Spring Boot 中不同的请求都是由不同的线程处理的,那为什么每一次请求都还能从 SecurityContextHolder 中获取到登录用户信息呢?
这就不得不提到 Spring Security 过滤器链中重要的一环 : SecurityContextPersistenceFilter

从名字上可以看出,SecurityContextPersistenceFilter就是为了【持久化(存储)SercurityContext】而生的。

SecurityContextPersistenceFilter主要做两件事:

1>、当一个请求到来时,从 HttpSession 中获取 SecurityContext 并存入 SecurityContextHolder 中,这样在同一个请求的后续处理过
    程中,开发者始终可以通过 SecurityContextHolder获取到当前登录用户信息。

2>、当一个请求处理完毕时,从 SecurityContextHolder中获取 SecurityContext 并存入HttpSession 中(主要针对异步 Servlet),
    方便下一个请求到来时,再从 HttpSession 中拿出来使用,同时擦除 SecurityContextHolder 中的登录用户信息。

【再次存储是为了防止securityContext发生变化】(在程序运行的过程中开发者可能会修改SecurityContext中的Authentication对象)
##拓展:

Servlet规范中,最早又三个和安全管理相关的方法:【HttpServletRequest】

  ·String getRemoteUser();//获取登录用户名

  ·boolean isUserInRole(String role);//判断当前登录用户是否具备某个角色

  ·Principal getUserPrincipal();//获取当前认证主体

从Servlet3.0之后,在上面三个方法的基础上,又增加了三个安全管理相关的方法
  
   ·boolean authenticate(HttpServletResponse resp);//判断当前请求是否认证成功

   ·void login(String username,String password);//执行登录操作

   ·void logout();//执行注销操作

如果是一个普通的web项目,不使用你和框架,HttpSevletRequest的默认实现类是Tomcat的RequestFacade.

如果使用了Spring security框架,那么我们拿到的httpServletRequest实例是:
       【Servlet3SecurityContextHolderAwareRequestWrapper】

因此我们在使用SpringSecurity之后就可以通过HttpServletRequest获取到登录的用户信息啦:

@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);
}

3、用户定义

用户自定义其实就是使用了UserDetailsService的不同实现类来提供的用户数据。

同时将配置好的UserDetailsService配置给AuthenticationManagerBuilder,
系统再将UserDetailsService提供给AuthenticationProvider使用。

①、基于内存InMemoryUserDetailsManager

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

     InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
     manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
     manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
     auth.userDetailsService(manager);

}

InMemoryUserDetailsManager 的实现原理很简单,它间接实现了 UserDetailsService 接口并重写了它里边的 loadUserByUsername 
方法,同时它里边维护了一个 HashMap 变量,Map 的key 就是用户名,value 则是用户对象,createUser 就是往这个 Map 中存储数据,
loadUserByUsername 方法则是从该 Map 中读取数据

②、基于数据库JdbcUserDetailsManager

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
     @Autowired
     DataSource dataSource;
     @Override
     protected void configure(AuthenticationManagerBuilder auth)  throws Exception {
         JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
         if (!manager.userExists("javaboy")) {
         	manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
         }
         if (!manager.userExists("sang")) {
         	manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
         }
         auth.userDetailsService(manager);
     }


     @Override
     protected void configure(HttpSecurity http) throws Exception {
     		//省略
     }
}

//代码解析:
1、首先是创建了一个JdbcUserDetailsManager实例。
2、userExists(username) 方法会根据用户名去数据库里查找对应的 用户,如果不存在 则通过createUser()方法创建一个用户并插入数据库
3、将JdbcUserDetailsManager实例 设置到AuthenticationManagerBuilder中

##源码:
public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager, GroupManager {

   //初始化数据源
   public JdbcUserDetailsManager(DataSource dataSource) {
		setDataSource(dataSource);
   }

   //查询用户
   @Override
   protected List<UserDetails> loadUsersByUsername(String username) {
		return getJdbcTemplate().query(getUsersByUsernameQuery(), this::mapToUser, username);
   }
   
   //创建用户
   @Override
   public void createUser(final UserDetails user) {
       
   }
   
   //更新用户
   @Override
   public void updateUser(final UserDetails user) {
       
   }
   
   //判断用户是否存在
   @Override
   public boolean userExists(String username) {
       
   }
       
   //....

}
       
在 JdbcUserDetailsManager 的继承体系中,首先是 JdbcDaoImpl 实现了 UserDetailsService接 口, 并实现 了基 本 的 loadUserByUsername 方 法。
JdbcUserDetailsManager 则 继承 自JdbcDaoImpl,同时完善了数据库操作,又封装了用户的增删改查方法。

③、基于Mybatis

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Autowired
     private MyUserDetailsService myUserDetailsService;

     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
     }

     @Override
     protected void configure(HttpSecurity http) throws Exception {
     		http.authorizeRequests()
     		//省略
     }
}
@Slf4j
@Service
public class MyUserDetailsService implements UserDetailsService, Serializable {

    @Resource
    private UserInfoService userInfoService;


    @Resource
    private PermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String mobileOrEmail) throws UsernameNotFoundException {
        UserInfo userInfo = userInfoService.findUserByMobileOrEmail(mobileOrEmail);
        if (StringUtils.isNull(userInfo)) {
            log.info("登录用户:{} 不存在.", mobileOrEmail);
            throw new BizException("登录用户:" + mobileOrEmail + " 不存在");
        } else if (EnableEnum.DELETE.getEnableCode().equals(userInfo.getEnable())) {
            log.info("登录用户:{} 已被删除.", mobileOrEmail);
            throw new BizException("对不起,您的账号:" + mobileOrEmail + " 已被删除");
        } else if (EnableEnum.FORBIDDEN.getEnableCode().equals(userInfo.getEnable())) {
            log.info("登录用户:{} 已被停用.", mobileOrEmail);
            throw new BizException("对不起,您的账号:" + mobileOrEmail + " 已停用");
        }

        //验证密码
        userInfoService.pwdValidate(userInfo);
        //查询权限、返回登录信息
        return createLoginUser(userInfo);
    }

    public UserDetails createLoginUser(UserInfo userInfo) {
        return new LoginInfo(userInfo.getUserCode(), userInfo, 
					permissionService.getMenuPermission(userInfo));
    }
}

4、认证流程分析

要搞清楚SpringSecurity的认证流程,我们需要搞清楚对应的三个基本组件:
   ·AuthenticationManager
   ·ProviderManager
   ·AuthenticationProvider

①、认证管理器AuthenticationManager

![26](imgs/springsecurity/26.png)从名称上可以看出,AuthenticationManager 是一个认证管理器。它定义了SpringSecurity如何进行认证操作。

##源码如下:

public interface AuthenticationManager {
	//认证操作
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}


##解析:
AuthenticationManager 在认证成功后,会返回一个 Authentication对象,这个 Authentication 对象会被设置到
SecurityContextHolder 中。

传入的 Authentication 参数只有用户名/密码等简单的属性,如果认证成功,返回的 Authentication 的属性会得到完全填充,
包括用户所具备的角色信息等。

如果开发者不想用 SpringSecurity 提供的一套认证机制,那么也可以自定义认证流程,认证成功后,手动将 Authentication
存入 SecurityContextHolder 中。

在Spring Security 框架中,默认也是使用【 ProviderManager】。

②、AuthenticationProvider

AuthenticationProvider 就是针对不同的身份类型执行具体的身份认证。
例如,常见的DaoAuthenticationProvider 用来支持用户名/密码登录认证;
RememberMeAuthenticationProvider用来支持“记住我”的认证.


##源码如下:
public interface AuthenticationProvider {

    //执行具体的身份认证
	Authentication authenticate(Authentication authentication) throws AuthenticationException;

	//判断当前的AuthenticationProvider是否支持对应的身份类型
	boolean supports(Class<?> authentication);

}

当时用 用户名/密码  登录认证的时候,对应的实现类是DaoAuthenticationProvider

DaoAuthenticationProvider 继 承 自 AbstractUserDetailsAuthenticationProvider 并且没有重写 authenticate 方法,
所以具体的认证逻辑在 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中:

public abstract class AbstractUserDetailsAuthenticationProvider  implements AuthenticationProvider {

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		
        //从当前authentication中获取用户名
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
        //根据用户名从缓存中 查询用户详情
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
                //根据用户名 从 userDetailsService.loadByUsername中获取用户信息
                //由 DaoAuthenticationProvider实现
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}catch (UsernameNotFoundException ex) {
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("xxxxxxxxxxxx", "Bad credentials"));
			}
		}
		try {
            //进行用户状态校验
			this.preAuthenticationChecks.check(user);
            //进行密码的操作校验
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		} catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
        //检查密码是否过期
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
        //返回一个 UsernamePasswordAuthenticationToken  继承自 Authentication
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

}

③、ProviderManager

ProviderManager 是 AuthenticationManager 的一个重要实现类。

在 Spring Security 中,由于系统可能同时支持多种不同的认证方式,例如同时支持用户名/密码认证、RememberMe 认证、
手机号码动态认证等,而不同的认证方式对应了不同的AuthenticationProvider,
所以一个完整的认证流程可能由多个 AuthenticationProvider 来提供。

多个 AuthenticationProvider 将组成一个列表,这个列表将由 ProviderManager 代理。

ProviderManager就 管理了 多个 AuthenticationProvider。


ProviderManager 本身也可以再配置一个 AuthenticationManager 作为 parent,这样当
ProviderManager 认证失败之后,就可以进入到 parent 中再次进行认证。理论上来说,
ProviderManager 的 parent 可以是任意类型的 AuthenticationManager。

ProviderManager 本身也可以有多个,多个 ProviderManager 共用同一个 parent,当存在多
个过滤器链的时候非常有用。当存在多个过滤器链时,不同的路径可能对应不同的认证方式,
但是不同路径可能又会同时存在一些共有的认证方式,这些共有的认证方式可以在 parent 中统
一处理。
##源码如下:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    //管理多个AuthenticationProvider 实现类
	private List<AuthenticationProvider> providers = Collections.emptyList();

    //可以再配一个AuthenticationManager 作为parent
	private AuthenticationManager parent;
        
        
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    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();

    //循环执行所有的AuthenticationProvider
    for (AuthenticationProvider provider : getProviders()) {
        //该provider是否支持对应的Authentication
        if (!provider.supports(toTest)) {
            continue;
        }
        if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
                    provider.getClass().getSimpleName(), ++currentPosition, size));
        }
        try {
            //provider的认证,如DaoAuthenticationProvider的认证  或者RememberMeAuthenticationProvider的认证
            result = provider.authenticate(authentication);
            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        }
        catch (AccountStatusException | InternalAuthenticationServiceException ex) {
            prepareException(ex, authentication);
           
            throw ex;
        }
        catch (AuthenticationException ex) {
            lastException = ex;
        }
    }

    //所有的provider的认证方法返回的Authentication为空,且parent不为空
    if (result == null && this.parent != null) {
       
        try {
            //调用parent的authentication方法
            parentResult = this.parent.authenticate(authentication);
            result = parentResult;
        }
        catch (ProviderNotFoundException ex) {
            
        }
        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;
    }

   
    if (lastException == null) {
        lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
                new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
    }

    if (parentException == null) {
        prepareException(lastException, authentication);
    }
    throw lastException;
}

④、AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter可以用来处理 任何提交给他的身份认证。


标签:登录,用户,SpringSecurity,Authentication,源码,new,认证,public,第四章
From: https://www.cnblogs.com/pilotspeed/p/17970064

相关文章

  • HashMap & HashSet源码阅读
    目录简介模型代码分析成员变量方法参考链接本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则......
  • 【送酒小程序系统源码】/花店送花系统/蛋糕店系统/奶茶店系统源码
    前端uniapp+后端thinkphp+数据库mysql多门店外卖餐饮点餐系统预约点餐匹配附近店铺 堂食外卖带走  菜品管理.根据用户的位置匹配附近饭店 点餐后,可以在线等叫号餐时输入手机号并支付后,可以支持外支持多规格、备注等快捷功能,以吸多多门店管理 数据概览支持微信小程序 ......
  • 尚无忧【无人共享空间 saas 系统源码】无人共享麻将室系统源码共享自习室系统源码,共享
    可saas多开,非常方便,大大降低了上线成本UNIAPP+thinkphp+mysql独立开源!1、定位功能:可定位附近是否有店2、能通过关键字搜索现有的店铺3、个性轮播图展示,系统公告消息提醒4、个性化功能展示,智能排序,距离、价格排序5、现有店铺清单展示,订房可查看房间单价,根据日期、时间端订房,选择时......
  • PDF转图片-itextpdf-java源码
    提供PDF文件转图片的工具类。电子签章过程中存在着在网页上对签署文件进行预览、指定签署位置、文件签署等操作,由于图片在浏览器上的兼容性和友好性优于PDF文件,所以一般在网页上进行电子签章时,会先将PDF文件转换成图片,展示给用户。用户在页面上确定好签署位置,并进行签署时,后端服......
  • PDF转图片-itextpdf-java源码
    提供PDF文件转图片的工具类。电子签章过程中存在着在网页上对签署文件进行预览、指定签署位置、文件签署等操作,由于图片在浏览器上的兼容性和友好性优于PDF文件,所以一般在网页上进行电子签章时,会先将PDF文件转换成图片,展示给用户。用户在页面上确定好签署位置,并进行签署时,后......
  • 基于SpringBoot+Vue的校园招聘系统设计实现(源码+lw+部署文档+讲解等)
    (文章目录)前言:heartpulse:博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌:heartpulse:......
  • C#微信公众号HIS预约挂号系统源码
    微信公众号预约挂号系统、支付宝小程序预约挂号系统主要是让自费、医保患者在手机上就能实现就医全过程,实时预约挂号、自费、医保结算,同时还可以查询检查检验报告等就诊信息,真正实现了让信息“多跑路”,让群众“少跑腿”。系统与HIS对接,通过医院微信公众号,患者用身份证注册以后,可以......
  • 【深入挖掘Java技术】「源码原理体系」盲点问题解析之HashMap工作原理全揭秘(上)
    知识盲点概念介绍HashMap是基于Map接口构建的数据结构,它以键值对的形式存储元素,允许键和值都为null。由于键的唯一性,HashMap中只能有一个键为null。HashMap的特点是元素的无序性和不重复性。注意,HashMap并不是线程安全的。在多线程环境下,如果不进行适当的同步处理,可能会导致数据不......
  • 【源码系列#06】Vue3 Diff算法
    专栏分享:vue2源码专栏,vue3源码专栏,vuerouter源码专栏,玩具项目专栏,硬核......
  • 使用 Python 创造你自己的计算机游戏(游戏编程快速上手)第四版:致谢到第四章
    致谢原文:inventwithpython.com/invent4thed/chapter0.html译者:飞龙协议:CCBY-NC-SA4.0没有NoStarchPress团队的出色工作,这本书就不可能问世。感谢我的出版商BillPollock;感谢我的编辑LaurelChun、JanCash和TylerOrtman,在整个过程中给予我的难以置信的帮助;感谢我......