• SpringSecurity源码学习三:认证


    1. 认证步骤

    Spring Security实现认证的主要步骤如下:

    1. 配置用户存储:您可以选择将用户信息存储在内存中、数据库中或其他外部身份验证源中。通过配置UserDetailsService或AuthenticationProvider,Spring Security可以获取用户的凭据和权限信息。

    2. 用户认证:当用户尝试登录时,Spring Security会验证用户提供的凭据(例如用户名和密码)。它使用AuthenticationManager来处理认证过程。AuthenticationManager会调用配置的AuthenticationProvider来验证用户凭据的有效性。

    3. 身份验证过滤器:Spring Security使用身份验证过滤器来拦截登录请求并进行身份验证。通常使用UsernamePasswordAuthenticationFilter来处理基于用户名和密码的认证请求。该过滤器会验证用户提供的凭据,并将认证结果封装成一个Authentication对象。

    4. 认证管理器:AuthenticationManager是Spring Security的核心接口之一,用于管理和执行身份验证过程。它负责调用配置的AuthenticationProvider来验证用户凭据的有效性。

    5. 认证成功处理器:当认证成功时,可以配置一个认证成功处理器来处理成功的认证请求。该处理器可以执行一些自定义逻辑,例如生成和返回访问令牌或重定向到特定页面。

    6. 认证失败处理器:当认证失败时,可以配置一个认证失败处理器来处理失败的认证请求。该处理器可以返回错误消息或重定向到登录页面等。

    2. 认证

    2.1 WebSecurityConfigurerAdapter配置介绍

    WebSecurityConfigurerAdapter是Spring Security提供的一个方便的基类,用于自定义安全配置。通过继承WebSecurityConfigurerAdapter类,并重写其中的方法,可以实现对Spring Security的自定义配置。 通过对configure方法的重新,我们可以做很多配置。

    下面是configure方法中常用配置的含义和对应的代码示例(Java):

    1. authorizeRequests(): 配置URL的访问权限规则。可以通过antMatchers()指定URL模式,通过permitAll()允许所有用户访问,通过authenticated()要求进行身份验证。
    protected void configure(HttpSecurity http) throws Exception {
           http
               .authorizeRequests()
                   .antMatchers("/public").permitAll()
                   .anyRequest().authenticated();
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. formLogin(): 配置表单登录相关的设置,包括登录页面、登录请求的URL、登录成功和失败的处理等。
    protected void configure(HttpSecurity http) throws Exception {
           http
               .formLogin()
                   .loginPage("/login")
                   .loginProcessingUrl("/authenticate")
                   .defaultSuccessUrl("/home")
                   .failureUrl("/login?error=true");
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. logout(): 配置登出相关的设置,包括登出URL、登出成功的处理等。
    protected void configure(HttpSecurity http) throws Exception {
           http
               .logout()
                   .logoutUrl("/logout")
                   .logoutSuccessUrl("/login?logout=true");
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. antMatchers(): 配置URL模式和对应的访问权限。
    protected void configure(HttpSecurity http) throws Exception {
           http
               .authorizeRequests()
                   .antMatchers("/public").permitAll()
                   .antMatchers("/admin").hasRole("ADMIN")
                   .anyRequest().authenticated();
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. permitAll(): 允许所有用户访问指定的URL。
    protected void configure(HttpSecurity http) throws Exception {
           http
               .authorizeRequests()
                   .antMatchers("/public").permitAll();
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. authenticated(): 要求用户进行身份验证才能访问指定的URL。
    protected void configure(HttpSecurity http) throws Exception {
           http
               .authorizeRequests()
                   .anyRequest().authenticated();
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 设置自定义过滤器,用于token校验,并把自定义的过滤器加入到过滤器链。我们用addFilterBefore()做例子,还有其他的api方法可以使用
     @Component
    public class CustomFilter implements GenericFilterBean{
         @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            // 过滤器逻辑,在此可以做接口的token校验
    
            chain.doFilter(request, response);
        }
    }
    
    //把自定义的过滤器加入到过滤器链中
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
         @Autowired
        private CustomFilter customFilter;
         @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                    .antMatchers("/public").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .and()
                .logout();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    1. 自定义accessDeniedHandler和authenticationEntryPoint
      accessDeniedHandler是对授权失败的处理接口;authenticationEntryPoint是对认证失败的处理。
    @Component
    public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response,
                           org.springframework.security.access.AccessDeniedException accessDeniedException)
                throws IOException, ServletException {
            // 在这里自定义处理访问被拒绝的情况
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
        }
    }
    
    @Component
    public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                             AuthenticationException authException) throws IOException, ServletException {
            // 在这里自定义处理未经身份验证的情况
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
        }
    }
    
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final AccessDeniedHandler accessDeniedHandler;
        private final AuthenticationEntryPoint authenticationEntryPoint;
    
        @Autowired
        public SecurityConfig(CustomAccessDeniedHandler accessDeniedHandler,
                              CustomAuthenticationEntryPoint authenticationEntryPoint) {
            this.accessDeniedHandler = accessDeniedHandler;
            this.authenticationEntryPoint = authenticationEntryPoint;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .exceptionHandling()
                    .accessDeniedHandler(accessDeniedHandler) // 设置自定义的accessDeniedHandler
                    .authenticationEntryPoint(authenticationEntryPoint) // 设置自定义的authenticationEntryPoint
                    .and()
                // 其他的配置...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    这些只是一些常用的配置示例,实际使用中可能需要根据需求进行更复杂的配置和自定义。

    2.2 使用UsernamePasswordAuthenticationFilter登录认证

    下面是使用UsernamePasswordAuthenticationFilter进行登录认证,并将账号密码存储在数据库中的Java代码示例:

    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private DataSource dataSource;
         @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .antMatchers("/login").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .and()
                .logout()
                    .permitAll();
        }
         @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery("SELECT username, password, enabled FROM users WHERE username = ?")
                .authoritiesByUsernameQuery("SELECT username, authority FROM authorities WHERE username = ?")
                .passwordEncoder(passwordEncoder());
        }
         @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
         @Bean
        public UsernamePasswordAuthenticationFilter authenticationFilter() throws Exception {
            UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter();
            filter.setAuthenticationManager(authenticationManagerBean());
            filter.setFilterProcessesUrl("/login");
            return filter;
        }
         @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                    .antMatchers("/login").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .and()
                .logout()
                    .permitAll();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    在上面的代码中,我们首先配置了一个DataSource,用于连接数据库。然后在SecurityConfig类中,通过configure方法配置了HttpSecurity,定义了登录页面、注销页面,以及哪些请求需要进行身份验证。接着,我们通过configure方法配置了AuthenticationManagerBuilder,使用jdbcAuthentication()方法来指定使用数据库进行身份验证,并提供了查询用户信息和权限的SQL语句。我们还使用了passwordEncoder()方法来指定密码的加密方式,这里使用了BCryptPasswordEncoder。最后,我们通过@Bean注解定义了一个UsernamePasswordAuthenticationFilter,并将其添加到了过滤器链中,用于拦截登录请求并进行身份验证。

    这只是简单的代码示例,主要是为了理解原理,我们真正使用的时候还是要自定义一个登录过滤器和token校验过滤器。下一章我们会举一个前后端分离,自定义登录认证的代码示例。

    2.2.1 UsernamePasswordAuthenticationFilter源码

    UsernamePasswordAuthenticationFilter是Spring Security框架中的一个过滤器,用于处理基于用户名和密码的身份验证。它是Spring Security核心过滤器链中的一部分,负责拦截用户的登录请求,并将用户名和密码交给AuthenticationManager进行身份验证处理。如果身份验证成功,该过滤器会创建一个包含用户信息和权限的认证对象,并将其交给SecurityContextHolder进行管理。如果身份验证失败,则会返回错误信息给用户。通过该过滤器,可以实现基于表单的登录认证

     */
    public class UsernamePasswordAuthenticationFilter extends org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter {
    
    	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    
    	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    
    	//此过滤器拦截的接口
    	private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
    			"POST");
    
    	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    
    	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    
    	private boolean postOnly = true;
    
    	//无参构造器,使用默认的AuthenticationManager实现
    	public UsernamePasswordAuthenticationFilter() {
    		super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    	}
    
    	//有参构造器,使用方可自定义AuthenticationManager,通过实现AuthenticationManager接口
    	public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
    		super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    可以看到此过滤器拦截自带的/login接口
    UsernamePasswordAuthenticationFilter继承自AbstractAuthenticationProcessingFilter,
    UsernamePasswordAuthenticationFilter没有重写doFilter()方法,我们看AbstractAuthenticationProcessingFilter的doFilter()方法。

    	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    			throws IOException, ServletException {
    		if (!requiresAuthentication(request, response)) {
    			chain.doFilter(request, response);
    			return;
    		}
    		try {
    			//attemptAuthentication是抽象方法,可被子类重写
    			Authentication authenticationResult = attemptAuthentication(request, response);
    			if (authenticationResult == null) {
    				// return immediately as subclass has indicated that it hasn't completed
    				return;
    			}
    			//成功后会
    			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
    			// Authentication success
    			if (this.continueChainBeforeSuccessfulAuthentication) {
    				chain.doFilter(request, response);
    			}
    			//保存用户信息等
    			successfulAuthentication(request, response, chain, authenticationResult);
    		}
    		catch (InternalAuthenticationServiceException failed) {
    			this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
    			unsuccessfulAuthentication(request, response, failed);
    		}
    		catch (AuthenticationException ex) {
    			// Authentication failed
    			unsuccessfulAuthentication(request, response, ex);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    认证逻辑主要在attemptAuthentication,attemptAuthentication是抽象方法,被子类UsernamePasswordAuthenticationFilter覆写,我们看下覆写逻辑。

    	//将request中的参数提取出来保存到 Authentication 中, 返回给认证器认证, 就这么一步, 如果你是前后端分离, 就有可能需要重写该方法
    	@Override
    	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    			throws AuthenticationException {
    		if (this.postOnly && !request.getMethod().equals("POST")) {
    			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    		}
    		//请求中获取用户名
    		String username = obtainUsername(request);
    		username = (username != null) ? username : "";
    		username = username.trim();
    		//从请求中获取密码
    		String password = obtainPassword(request);
    		password = (password != null) ? password : "";
    		//构建个UsernamePasswordAuthenticationToken令牌,继承自Authentication
    		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    		// Allow subclasses to set the "details" property
    		setDetails(request, authRequest);
    		//把令牌传入AuthenticationManager,进行认证
    		return this.getAuthenticationManager().authenticate(authRequest);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    主要逻辑是this.getAuthenticationManager().authenticate(authRequest);可以看到是把UsernamePasswordAuthenticationToken从传入到AuthenticationManager。AuthenticationManager的接口的主要实现类是ProviderManager,核心认证逻辑就在ProviderManager类中。

    2.2.1.1 ProviderManager源码

    我们看下ProviderManager类的authenticate()方法,代码过长,主要看下主要逻辑。

    	//认证器集合
    	private List<org.springframework.security.authentication.AuthenticationProvider> providers = Collections.emptyList();
    
    	@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 (org.springframework.security.authentication.AuthenticationProvider provider : getProviders()) {
    			//匹配当前的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 {
    				//执行匹配到的AuthenticationProvider逻辑
    				result = provider.authenticate(authentication);
    				if (result != null) {
    					copyDetails(authentication, result);
    					break;
    				}
    			}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    我们根据supports()方法,匹配到正确的AuthenticationProvider是DaoAuthenticationProvider。DaoAuthenticationProvider继承AbstractUserDetailsAuthenticationProvider。
    核心逻辑:

    	//校验用户密码并返回用户信息
    	@Override
    	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
    				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
    						"Only UsernamePasswordAuthenticationToken is supported"));
    		String username = determineUsername(authentication);
    		boolean cacheWasUsed = true;
    		UserDetails user = this.userCache.getUserFromCache(username);
    		if (user == null) {
    			cacheWasUsed = false;
    			try {
    				//返回本地用户信息
    				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
    			}
    			catch (UsernameNotFoundException ex) {
    				this.logger.debug("Failed to find user '" + username + "'");
    				if (!this.hideUserNotFoundExceptions) {
    					throw ex;
    				}
    				throw new BadCredentialsException(this.messages
    						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    			}
    			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    		}
    		try {
    			this.preAuthenticationChecks.check(user);
    			//请求密码和本地密码匹配
    			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    DaoAuthenticationProvider中覆写的方法。

    	//从缓存或者数据库查询用户信息
    	@Override
    	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
    			throws AuthenticationException {
    		prepareTimingAttackProtection();
    		try {
    			//从缓存或者数据库查询用户信息
    			//默认spring security保存在内存中, 如果你需要改从数据库中拿到用户, 就需要重写UserDetailsService
    			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);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    	//密码验证方法
    	@Override
    	@SuppressWarnings("deprecation")
    	protected void additionalAuthenticationChecks(UserDetails userDetails,
    			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    		if (authentication.getCredentials() == null) {
    			this.logger.debug("Failed to authenticate since no credentials provided");
    			throw new BadCredentialsException(this.messages
    					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    		}
    		String presentedPassword = authentication.getCredentials().toString();
    		//比较密码是否相同
    		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
    			this.logger.debug("Failed to authenticate since password does not match stored value");
    			throw new BadCredentialsException(this.messages
    					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    可以看到,这段逻辑是:比较用户输入的账号密码和数据库或缓存中的用户密码是否匹配。如果匹配成功,认证通过,存储用户信息到上下文中,发送监听事件。认证失败,报错返回前端。

    2.2.2 认证流程总结

    Spring Security认证流程总结如下:

    1. 用户发起登录请求,提交用户名和密码。
    2. UsernamePasswordAuthenticationFilter拦截登录请求,获取到用户名和密码。
    3. UsernamePasswordAuthenticationFilter将用户名和密码封装成一个Authentication对象。
    4. AuthenticationManager负责对Authentication对象进行身份验证。
    5. AuthenticationManager选择合适的AuthenticationProvider进行身份验证,通常使用DaoAuthenticationProvider。
    6. DaoAuthenticationProvider使用UserDetailsService从数据库或其他数据源中获取用户信息。
    7. DaoAuthenticationProvider使用PasswordEncoder对输入的密码进行加密,然后与数据库中的密码进行比对。
    8. 如果密码匹配成功,DaoAuthenticationProvider将创建一个包含用户信息和权限的认证对象。
    9. 认证对象将被存储在SecurityContextHolder中,以便在整个请求过程中进行访问控制。
    10. 如果密码匹配失败,DaoAuthenticationProvider将抛出异常,登录失败。
    11. 认证成功后,用户将被重定向到登录成功的页面,或者继续访问原始请求的页面。

    这是一个简化的Spring Security认证流程,具体的流程可能会根据配置和需求有所不同。但总体来说,Spring Security提供了一个灵活且可定制的认证框架,可以满足各种身份验证需求。

    2.3 自定义登录认证代码示例

    1. 创建一个名为 User 的实体类,表示用户信息,包含用户名、密码和角色等属性。
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    @Entity
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String username;
        private String password;
        private String role;
        // 构造函数、getter和setter方法省略
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 创建一个名为 UserRepository 的接口,用于操作用户信息的数据库存储。
    public interface UserRepository {
    	//查询数据库用户信息,接口实现类自定义编写
        User findByUsername(String username);
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 创建一个名为 UserDetailsServiceImpl 的类,实现Spring Security的 UserDetailsService 接口,用于从数据库中加载用户信息。
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Service;
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
        private final UserRepository userRepository;
        @Autowired
        public UserDetailsServiceImpl(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userRepository.findByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("User not found");
            }
            return org.springframework.security.core.userdetails.User
                    .withUsername(user.getUsername())
                    .password(user.getPassword())
                    .roles(user.getRole())
                    .build();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    返回对象是org.springframework.security.core.userdetails.User,这是springSecurity内部对象,如果不满足我们可以自定义返回对象,自定义返回对象要继承springSecurity的UserDetails。

    1. 创建一个名为 SecurityConfig 的配置类,继承自 WebSecurityConfigurerAdapter ,用于配置Spring Security。
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        private final UserDetailsServiceImpl userDetailsService;
        @Autowired
        public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .antMatchers("/admin/**").hasRole("ADMIN") // 需要ADMIN角色才能访问
                    .antMatchers("/user/**").hasAnyRole("ADMIN", "USER") // 需要ADMIN或USER角色才能访问
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .and()
                .logout()
                    .logoutSuccessUrl("/")
                    .and()
                .csrf().disable();
        }
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/resources/**");
        }
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    
    	//密码加密方法
        @Bean
        public BCryptPasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    可以根据需求修改configure()方法。

    1. 创建一个名为 UserController 的控制器类,用于处理登录请求。
    @RestController
    public class UserController {
        private final UserRepository userRepository;
        private final PasswordEncoder passwordEncoder;
        private final AuthenticationManager authenticationManager;
        @Autowired
        public UserController(UserRepository userRepository, PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager) {
            this.userRepository = userRepository;
            this.passwordEncoder = passwordEncoder;
            this.authenticationManager = authenticationManager;
        }
        @PostMapping("/login")
        public String login(@RequestBody User user) {
            User storedUser = userRepository.findByUsername(user.getUsername());
            if (storedUser != null && passwordEncoder.matches(user.getPassword(), storedUser.getPassword())) {
                Authentication authentication = authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())
                );
                SecurityContextHolder.getContext().setAuthentication(authentication);
                return "登录成功";
            } else {
                return "用户名或密码错误";
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    1. 设置自定义过滤器,用于token校验,并把自定义的过滤器加入到过滤器链。我们用addFilterBefore()做例子,还有其他的api方法可以使用
     @Component
    public class CustomFilter implements GenericFilterBean{
         @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            // 过滤器逻辑,在此可以做接口的token校验
    
            chain.doFilter(request, response);
        }
    }
    
    //把自定义的过滤器加入到过滤器链中
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
         @Autowired
        private CustomFilter customFilter;
         @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                    .antMatchers("/public").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .and()
                .logout();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    UsernamePasswordAuthenticationToken 是一个基于用户名和密码的身份验证令牌。
    此令牌作为参数传入AuthenticationManager。在AuthenticationManager中,和2步骤中我们自定义的 loadUserByUsername()方法返回的用户信息做匹配,匹配成功认证成功,把认证信息设置到SecurityContextHolder中,失败返回错误信息。

    其他接口请求后端接口的时候,会通过我们的自定义过滤器CustomFilter 校验token。

    以上只是简单的代码示例,主要是帮助我们理解认证流程。真正的项目代码要更完善,细节点与功能点要更多,也会用到其他框架的东西,比如JWT。

    2.3.1 认证流程总结

    Spring Security认证流程总结如下:

    1. 用户发起登录请求,提交用户名和密码,根据用户名和密码封装成一个Authentication对象。
    2. AuthenticationManager负责对Authentication对象进行身份验证。
    3. AuthenticationManager选择合适的AuthenticationProvider进行身份验证,通常使用DaoAuthenticationProvider。
    4. DaoAuthenticationProvider使用UserDetailsService从数据库或其他数据源中获取用户信息。
    5. DaoAuthenticationProvider使用PasswordEncoder对输入的密码进行加密,然后与数据库中的密码进行比对。
    6. 如果密码匹配成功,DaoAuthenticationProvider将创建一个包含用户信息和权限的认证对象。
    7. 认证对象将被存储在SecurityContextHolder中,以便在整个请求过程中进行访问控制。 登录接口返货token给调用方,方便后续接口调用。
    8. 如果密码匹配失败,DaoAuthenticationProvider将抛出异常,登录失败。
    9. 认证成功后,用户将被重定向到登录成功的页面,或者继续访问原始请求的页面。
      10.其他页面访问, 校验token是否有效。有效正常访问接口,无效返回错误,让用户登录。
  • 相关阅读:
    基于C++11的数据库连接池实现
    开箱即用轻量级雪花算法id生成器Java工具类
    达观RPA实战-编码与解码
    java计算机毕业设计springboot+vue企业的信息管理系统
    Web会话跟踪:Cookie与Session
    Linux与Shell学习--shell系列4--常用的数据类型(数字、字符串和数组)
    Vue2笔记_03配置项
    mysql进程信息出现大量Waiting for table level lock信息的原因,怎么处理?
    【C#】抽象方法、接口、虚方法
    web前端面试题
  • 原文地址:https://blog.csdn.net/qq_27586963/article/details/133849598