Spring Security 配置和源码解析

Spring Security 配置和源码解析

背景:spring-boot-starter-security:2.3.9.RELEASE

在微服务中,整合Spring Security功能。将系统模块(涵盖用户、菜单等功能的模块)与Spring Security进行分离,使Spring Security作为一个单独的依赖存在,使用自动配置的方式配置进使用该依赖的模块当中。

思路

业务服务认证服务前端业务服务认证服务前端JWT携带用户Id。具体用户信息存放进Redis(userId:User),其中用户信息包含着用户的基础信息和权限信息alt[JWT != null][JWT == null]alt[有权限][无权限]携带用户名密码访问登录接口与数据库中的用户名密码对比如果对比成功,生成JWT响应JWT访问其他接口获取请求头中的JWT对JWT进行解析获取用户Id,然后从Redis中获取具体的用户信息响应未登录获取权限列表执行后续操作响应结果响应无权限

处理流程

Spring Security的原理是一个过滤器链,内部包含了提供各种功能的过滤器

APIFilterSecurityInterceptorExceptionTranslationFilter……UsernamePasswordAuthenticationFilter……APIFilterSecurityInterceptorExceptionTranslationFilter……UsernamePasswordAuthenticationFilter……

可以从Ioc中获取SecurityFilterChain对象,调用getFilters方法得到过滤器链(15个)

其中:

  1. UsernamePasswordAuthenticationFilter:负责处理在登陆页面的登陆请求
  2. ExceptionTranslationFilter:处理过滤器链中抛出的任何异常,然后经过一系列逻辑处理,抛出AccessDeniedExceptionAuthenticationException(一般不会直接抛出在此之前过滤器中出现的异常),或是拒绝访问处理
  3. FilterSecurityInterceptor:负责权限校验的过滤器

认证流程

InMemoryUserDetailsManager(UserDetailsService)DaoAuthenticationProvider(AbstractUserDetailsAuthenticationProvider)(AuthenticationProvider)ProviderManager(ProviderManager)UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter)用户InMemoryUserDetailsManager(UserDetailsService)DaoAuthenticationProvider(AbstractUserDetailsAuthenticationProvider)(AuthenticationProvider)ProviderManager(ProviderManager)UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter)用户提交用户名密码1进入doFilter,调用attemptAuthentication2authenticate3authenticate4retrieveUser5loadUserByUsername6获取数据库中的用户信息、权限信息,最后封装进Authentication对象7

说明

  1. ……

  2. attemptAuthentication方法由UsernamePasswordAuthenticationFilter实现

  3. 调用的是AbstractAuthenticationProcessingFilter.authenticate,目的是封装了一个Authentication对象

  4. 调用的是AuthenticationProvider.authenticate,而AuthenticationProvider的实现关系是:AuthenticationProvider - AbstractUserDetailsAuthenticationProvider - DaoAuthenticationProvider

  5. 调用的是AbstractUserDetailsAuthenticationProvider.retrieveUserretrieveUser是一个抽象方法,由DaoAuthenticationProvider实现

  6. 调用的是UserDetailsService.loadUserByUsername

  7. ……

整合 Spring Security

实现UserDetailsService接口

重写loadUserByUsername方法

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库获取用户信息
        ...
        if (Objects.isNull(user)) {
            throw new BadCredentialsException("用户不存在");
        }
        // 获取权限列表
        ...
        return new LoginUser(user, new ArrayList<>(list));
    }
}

实现UserDetails接口

用户对象用户权限作为成员变量。其中GrantedAuthority无法被序列化,所以需要使用额外的对象来存储权限信息:permissions,并且重写getAuthorities方法

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private UserEntity user;
    private List<String> permissions;
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    public LoginUser(UserEntity user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) {
            return authorities;
        }
        authorities = permissions.stream().map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getBcPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }
    ...
}

完成上述操作,就已经可以在Spring Security的原本配置中,使用/login接口进行登录验证了


自定义登录接口

登录成功之后将用户信息存入Redis

@Service
public class LoginServiceImpl implements LoginService {

    @Override
    public R login(UserEntity user) {
        // 创建Authentication对象,然后进行认证操作
        UsernamePasswordAuthenticationToken authenticationToken = new  UsernamePasswordAuthenticationToken(user.getUsername(),
                user.getBcPassword());
        Authentication authenticate = SpringUtil.getBean(AuthenticationManager.class).authenticate(authenticationToken);
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("用户名或密码错误");
        }
        // 获取用户Id,作为Redis的Key,User作为Value存入Redis中
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        ...
    }

    @Override
    public R logout() {
        // 从上下文中获取用户Id,然后从Redis中移除
        ...
    }
}

定义JWT过滤器

使用继承OncePreRequestFilter的方式。作用是获取Token并且解析,然后进行用户是否登录的验证

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取请求头中的JWT
        ...
        // 解析JWT,获取用户Id,从Redis中获取用户信息和权限列表
        ...
        // 完成认证,然后将认证信息存入上下文中,最后放行
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);		
    }
}

Spring Security核心配置

将登录接口进行放行。重写WebSecurityConfigurerAdapter中的configure方法

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityAutoConfiguration extends WebSecurityConfigurerAdapter {
    @Resource
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Resource
    public DefaultAuthenticationEntryPoint authenticationEntryPoint;
    @Resource
    public DefaultAccessDeniedHandler accessDeniedHandler;
    @Resource
    public SecurityProperties properties;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .cors()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler)
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        if (!CollectionUtils.isEmpty(properties.getAnonymous())) {
            for (String anonymous : properties.getAnonymous()) {
                http.authorizeRequests().antMatchers(anonymous).anonymous();
            }
        }
        http.authorizeRequests().anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

认证过程中出现的异常

异常处理流程

因为在过滤器中抛出的异常一般都由ExceptionTranslationFilter来处理,所以异常抛出后执行的入口就是ExceptionTranslationFilter.doFilter

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		...
		try {
			// 入口
			chain.doFilter(request, response);
			...
		} catch (IOException ex) {
			throw ex;
		} catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);

			if (ase == null) {
				// 这里一般只会有:AuthenticationException、AccessDeniedException
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}

			if (ase != null) {
				...
				// 最后执行到这里
				handleSpringSecurityException(request, response, chain, ase);
			} else {
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw new RuntimeException(ex);
			}
		}
	}

	private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		// 分别单独处理两种认证异常:AuthenticationException、AccessDeniedException
		if (exception instanceof AuthenticationException) {
			...
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {
			...
				sendStartAuthentication(
						request,
						response,
						chain,
						new InsufficientAuthenticationException(
							messages.getMessage(
								"ExceptionTranslationFilter.insufficientAuthentication",
								"Full authentication is required to access this resource")));
			}
			else {
				...
				// 其他异常走拒绝访问处理
				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			}
		}
	}

	protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		...
		// 关键出口
		authenticationEntryPoint.commence(request, response, reason);
	}

由源码可知:如果想要自定义认证异常的处理逻辑,可以从authenticationEntryPoint.commence入手;自定义其他异常的处理逻辑可以从accessDeniedHandler.handle入手

自定义认证失败处理器

实现AuthenticationEntryPoint接口

@Component
public class DefaultAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        HttpUtil.response(response, 50000 + HttpStatus.UNAUTHORIZED.value(), "认证失败");
    }
}
自定义拒绝访问处理器

实现AccessDeniedHandler接口

@Component
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        HttpUtil.response(response, 50000 + HttpStatus.FORBIDDEN.value(), accessDeniedException.getMessage());
    }
}

跨域

除了在configure中配置Spring Security的跨域问题:cors,还需要打开Servlet的跨域:

@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowCredentials(true);
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsWebFilter(source);
    }
}

或者使用注解:@CrossOrigin

posted @   紅豆DuoLaameng  阅读(200)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示