Spring Security概念与应用

一、概念

Spring Security 是一个基于 Spring 框架的安全性框架,它提供了许多针对身份验证(Authentication)
和访问控制(Authorization)方面的功能,帮助开发人员保护和管理应用程序的安全性。

二、核心思想与核心组件

1. 核心思想

Spring Security 的核心思想是通过将安全性视为应用程序的一个关注点来实现,
而不是将其视为一个单独的策略或框架。使用 Spring Security 可以轻松实现以下安全性功能:

1、身份验证和授权

2、记住我(Remember me)

3、CSRF 攻击防护

4、会话管理和过期处理

5、安全日志记录

6、与其他认证和授权机制的集成

2. 核心组件

2.1 SecurityContextHolder

SecurityContextHolder 是一个存储当前安全上下文信息的中心化存储区。
安全上下文是一个包含了认证和授权信息的对象,它通常包含了当前登录用户的信息,
例如用户名、密码和角色等。在整个应用程序中,SecurityContextHolder 对象可以被多个线程共享和使用

在这里插入图片描述

2.2 AuthenticationManager

AuthenticationManager 是 Spring Security 的核心接口之一。它处理来自用户的身份验证请求,并基于这些请求返回认证对象。

AuthenticationManager 主要通过 AuthenticationProvider或者 UserDetailsService 来完成这个工作。

在一个 Web 应用中,当用户尝试登录时,通常会提交一个包含用户名和密码等信息的表单。

在使用 Spring Security 进行身份验证时,这个表单数据会被传递到 AuthenticationManager 的 authenticate() 方法中。

AuthenticationManager 将根据传入的身份验证信息,通过相应的 AuthenticationProvider 或者UserDetailsService 

获取用户的详细信息,并进行身份验证。如果身份验证通过,则 AuthenticationManager

会创建一个 Authentication 对象,其中包含了该用户的身份验证信息以及相关的角色和权限等信息。	

在这里插入图片描述

2.3 认证对象-Authentication

它代表了经过身份验证的用户。

在认证成功之后,认证对象会被存储在 SecurityContextHolder 中,供整个应用程序共享和使用。

通过认证对象,应用程序可以访问用户的详细信息,例如用户名、角色和权限等,并根据这些信息做出相应的授权决策.

2.4 AuthenticationProvider

AuthenticationProvider 接口实现了具体的身份验证机制,例如表单身份验证、LDAP 身份验证等。

通常情况下,每个 AuthenticationProvider 组件对应一个身份验证机制。

AuthenticationProvider 组件会从用户提供的身份验证信息中获取所需信息,并生成一个 Authentication 对象返回AuthenticationManager.

2.5 UserDetailsService

UserDetailsService 接口定义了从内存、关系数据库或其他数据源中获取用户详细信息的方法。

对于基于用户名和密码进行身份验证的应用程序-UserDetailsService 可以返回一个 UserDetails 实例,

其中包含用户名、加密后的密码和该用户拥有的权限和角色信息.

2.6 FilterChainProxy

FilterChainProxy 是 Spring Security 最重要的组件之一。它将请求传递给一个或多个 SecurityFilterChain 实例,

这些实例负责检查身份验证和授权规则,并在需要时重定向或返回错误响应。

FilterChainProxy 的主要作用是提供了一组过滤器链,这些过滤器链会被按顺序执行,直到找到能够处理请求的过滤器为止。

2.7 SecurityMetadataSource

SecurityMetadataSource 接口定义了资源路径与相应的安全元数据之间的映射。

例如,它可以指定哪些 URL 需要身份验证,哪些 URL 可以匿名访问等。

2.8 AccessDecisionManager

AccessDecisionManager 是 Spring Security 的核心组件之一,在用户进行操作时,判断用户是否有足够的权限。

其主要的方法是 decide() 方法,它包含了一个 Authentication 对象、一个要访问的资源对象和代表当前用户的权限信息。

通过配置合适的 AccessDecisionManager,可以在应用程序中实现访问控制的功能。

三、什么是 Spring Security认证和授权

在这里插入图片描述

1.认证

验证用户身份的过程-比如登录:根据账号和密码判断当前用户是否是已存在的用户,如果不存在则跳转到注册或者登录界面.

认证成功后,Spring Security会创建一个认证对象-Authentication,并将其存储在安全上下文中,以便后续的授权和访问控制使用。

2.授权

确认当前登录用户可访问的资源权限,不同的角色可访问的资源范围不同.

在Spring Security中,授权通常基于用户的角色或权限进行。角色是一组权限的集合,用于描述用户可以执行的操作或访问的资源。

3.总结

认证关注用户的身份验证,确保用户是他们所声称的身份;而授权关注已认证用户是否有权执行特定操作或访问特定资源。

在Spring Security中,认证和授权是两个独立但相互关联的过程。认证为授权提供了基础,授权则根据认证结果和用户的角色/权限做出访问决策。

四、Spring Security过滤器链定义及其工作原理

Spring的安全性是通过Web应用程序的servlet过滤器驱动的.它在请求到达真正的接口之前对请求进行拦截。

通过token令牌完成身份认证和授权目的.

在这里插入图片描述

五、认证流程

在这里插入图片描述

1.用户在登录页面输入用户名和密码,提交表单。

2.Spring Security的UsernamePasswordAuthenticationFilter拦截表单提交的请求,并将用户名和密码封装成一个Authentication对象。
// 源码位置:ProviderManager.authenticate(),行56 ==》 行73  ==》 AbstractUserDetailsAuthenticationProvider.authenticate(),行54 ===》 retrieveUser判断用户,additionalAuthenticationChecks判断密码
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
3.AuthenticationManager接收到Authentication对象后,会根据用户名和密码查询用户信息(从数据库或者缓存中),并将用户信息封装成一个UserDetails对象。

4.如果查询到用户信息,则将UserDetails对象封装成一个已认证的Authentication对象并返回,如果查询不到用户信息,则抛出相应的异常。(用户名或者密码错误/用户名不存在等).

5.如果认证成功,则认证结果会存储在SecurityContextHolder中。其它过滤器可以从SecurityContextHolder中获取用户信息
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
AuthenticationUtil.setAuthentication(authentication);

// 将用户信息存储到自己创建的threadLocal中,使得程序中可以随时获取用户信息
public static void setAuthentication( Authentication authentication ){
authenticationThreadLocal.set(authentication);

Object principal = authentication.getPrincipal();
try {
	if(principal instanceof UserDetails) {
		UserDetails ud = (UserDetails)principal;
		String json = JsonUtil.toJson(ud);
		JsonNode jsonNode = JsonUtil.toJsonNode(json);
		userThreadLocal.set(jsonNode);
	}
} catch (Exception e) {
	e.printStackTrace();
	throw new RuntimeException(e.getMessage());
}
}

六、授权流程

1.配置 Spring Security

1.自定义配置类继承该类--WebSecurityConfigurerAdapter

2.添加注解@EnableWebSecurity-开启security认证授权
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {}

2.配置过滤器、拦截器、服务器

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
				// 1、配置自定义未授权异常处理器,当过滤器抛出异常时,跳转到异常处理器中.
                httpSecurity.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler).and()
		
				JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenHandler, jwtConfig.getHeader());
 				// 2.配置认证过滤器       
 				httpSecurity
         		.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
				// 3.配置授权拦截器
				httpSecurity
     			.addFilterBefore(htFilterSecurityInterceptor, FilterSecurityInterceptor.class);
	}
}

3.授权异常过滤器

配置自定义未授权异常处理器,当过滤器抛出异常时,跳转到异常处理器中
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
					   AccessDeniedException accessDeniedException) throws IOException, ServletException {
		//设置响应状态码
		response.setStatus(HttpServletResponse.SC_FORBIDDEN);
		//设置响应数据格式
		response.setContentType("application/json;charset=utf-8");
		//输入响应内容
		PrintWriter writer = response.getWriter();
		CommonResult<String> commonResult = new CommonResult<String>( ResponseErrorEnums.ACCESS_DENIED_EXCEPTION);
		writer.write(JsonUtil.toJson(commonResult));
		writer.flush();
	}
}

3.授权认证过滤器

对所有请求进行认证,判断该请求是否是合法请求
// 对所有请求进行认证,判断该请求是否是合法请求
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

	// 认证过滤器执行的方法
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
		// 1.从请求头中获取token信息,并通过jwtTokenHandler根据请求头中的token信息获取用户名
		if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
			authToken = requestHeader.substring(7);
			try {
				username = jwtTokenHandler.getUsernameFromToken(authToken);
			} catch (Exception e) {
				// 根据token获取用户名异常,抛出401异常-未授权
				logger.warn("the token valid exception", e);
				send401Error(response, e.getMessage());
				return;
			}
		}
		// 登录认证时,会将authentication对象保存到SecurityContext中,用于后续过滤器从中获取用户信息				
		if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
			// securityContext中不存在认证对象,则自己创建一个认证对象的子类,将其保存到SecurityContext中
			同时保存一份到用户信息线程变量与认证对象线程变量中
			UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
			authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
			SecurityContextHolder.getContext().setAuthentication(authentication);
			AuthenticationUtil.setAuthentication(authentication);
		}
	}

	// 用户信息的线程变量
	private static  ThreadLocal<JsonNode> userThreadLocal = new ThreadLocal<JsonNode>();
	// security认证对象的线程变量 
	private static ThreadLocal<Authentication> authenticationThreadLocal = new ThreadLocal<Authentication>();

4.授权拦截器

授权拦截器用于判断当前请求的权限,即当前用户所属角色是否可访问当前请求
public class HtFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
	// 注入该服务,获取可访问当前请求方法的角色集合
	@Resource
	private FilterInvocationSecurityMetadataSource securityMetadataSource;
	// 注入该服务,判断当前用户能否访问当前请求方法
	@Resource
	private AccessDecisionManager accessDecisionManager;


	/**
	 * 过滤接口授权
	 * 1、检测访问的接口地址数据是否在portal_sys_method表内
	 * 2、存在则判断该接口地址是否绑定对应的角色,数据不存在则执行下一个拦截器
	 * 3、存在角色则与当前登录用户的角色进行对比,不存在角色绑定则执行下一个拦截器
	 * 4、角色相同则有此接口访问权限,不同则抛异常,webSecurity中配置的未授权异常处理器收到异常后给浏览器响应403
	 *
	 * @param request
	 * @param response
	 * @param chain
	 * @throws IOException
	 * @throws ServletException
	 */
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		//fi里面有一个被拦截的url
		// 获取可访问当前请求的角色集合
		Collection<ConfigAttribute> attributes = securityMetadataSource.getAttributes(fi);
		// 校验当前用户角色与请求方法被授权的角色是否匹配
		accessDecisionManager.decide(SecurityContextHolder.getContext().getAuthentication(), null, attributes);
		InterceptorStatusToken token = super.beforeInvocation(fi);
		try {
			//执行下一个拦截器
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		} finally {
			super.afterInvocation(token, null);
		}
	}
}

5.授权数据资源服务器

授权资源服务器主要用于判断当前请求可访问的角色集合
@Service
public class HtInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource, ContextThread {
	// 接口权限的线程变量<String-方法名,Collection-可访问该方法的角色集合>
	private static ThreadLocal<HashMap<String, Collection<ConfigAttribute>>> mapThreadLocal = new ThreadLocal<HashMap<String, Collection<ConfigAttribute>>>();

	// 加载权限表中的所有权限,因为一个方法可能多个角色都具有访问权限,所以组装成map对象
	// map.put(方法名,角色集合);
	public void loadResourceDefine() {
		HashMap<String, Collection<ConfigAttribute>> map = getMapThreadLocal();
		Collection<ConfigAttribute> array;
		ConfigAttribute cfg;
		List<HashMap<String, String>> methodAuth = methodAuthService.getMethodAuth();
		if (BeanUtils.isEmpty(methodAuth)) {
			return;
		}
		for (HashMap<String, String> mapAuth : methodAuth) {
			array = new ArrayList<ConfigAttribute>();
			String roleAlias = mapAuth.get("roleAlias");
			String key = mapAuth.get("methodRequestUrl");
			if (StringUtil.isEmpty(roleAlias) || StringUtil.isEmpty(key)) {
				continue;
			}
			cfg = new SecurityConfig(roleAlias);

			if (map.containsKey(key)) {
				array = map.get(key);
			}
			array.add(cfg);
			map.put(key, array);
		}



		/**
 * 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
 * 返回空 , 则不用经过 decide 方法 判断权限, 直接具有访问权限了
 */
		@Override
		public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
			//5、接口授权:这里主要是判断该请求中的接口地址是否在权限表中以及是否有角色绑定接口地址,转该类行110
			AntPathRequestMatcher matcher;
			String resUrl;
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			// 匿名访问不需要获取权限信息
			if (AuthenticationUtil.isAnonymous(authentication)) return null;

			// object 中包含用户请求的request 信息
			HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
			// 判断请求是否从feign过来的
			String requestHeader = request.getHeader("Proxy-Authorization");
			if (StringUtil.isNotEmpty(requestHeader) && requestHeader.startsWith("Bearer ") && jwtTokenHandler.validateFeignToken(requestHeader.substring(7))) {
				return null;
			}

			if (object instanceof FilterInvocation) {
				FilterInvocation filterInvocation = (FilterInvocation) object;
				String method = filterInvocation.getRequest().getMethod();
				resUrl = filterInvocation.getRequestUrl();
				// options 返回null
				if (HttpMethod.OPTIONS.matches(method)) {
					return null;
				}
			}

			loadResourceDefine();
			// 从线程中获取保证线程安全
			HashMap<String, Collection<ConfigAttribute>> map = getMapThreadLocal();
			for (Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
				resUrl = iter.next();
				matcher = new AntPathRequestMatcher(resUrl);
				//6、判断该接口地址request是在授权地址map.matcher.resUrl,是则转类HtDecisionManager行22的decide()  (此时底层实则还是在AbstractSecurityInterceptor.beforeInvocation()方法)
				if (matcher.matches(request)) {
					return map.get(resUrl);
				}
			}
			return null;
		}
	}
}

6.授权权限管理服务器

 对认证对象与当前请求可访问的角色集合进行判断-如果角色相等,说明该方法可以访问;如果不等,说明当前用户无权访问该方法
public class HtDecisionManager implements AccessDecisionManager {
	// 对认证对象与当前请求可访问的角色集合进行判断-如果角色相等,说明该方法可以访问;如果不等,说明当前用户无权访问该方法
	@Override
	public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
	throws AccessDeniedException, InsufficientAuthenticationException {
		try {
			if (BeanUtils.isEmpty(configAttributes))
				return;

			Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) authentication
														.getAuthorities();

			//7、判断当前角色与授权角色
			for (GrantedAuthority grantedAuthority : authorities) {
				// 超级管理员
				if (PlatformConsts.ROLE_SUPER.equals(grantedAuthority.getAuthority())) {
					return;
				}
			}

			for (ConfigAttribute configAttribute : configAttributes) {
				if (BeanUtils.isEmpty(configAttribute)) continue;
				String configVal = configAttribute.toString();
				// 匿名资源允许访问
				if (PlatformConsts.PERMIT_All.equals(configVal)) {
					return;
				}
					// 受权限控制的资源
				else if (PlatformConsts.AUTHENTICATED.equals(configVal)) {
					// 匿名访问时抛出 401异常
					if (AuthenticationUtil.isAnonymous(authentication)) {
						throw new InsufficientAuthenticationException("需要提供jwt授权码");
					} else {
						// 如果该资源未加入权限管理列表,则允许访问
						String attribute = configAttribute.getAttribute();
						if (StringUtil.isEmpty(attribute)) {
							return;
						}
					}
				}
			}

			for (GrantedAuthority grantedAuthority : authorities) {
				for (ConfigAttribute configAttribute : configAttributes) {
					if (grantedAuthority.getAuthority().equals(
						configAttribute.getAttribute())) {
						// 有权限
						return;
					}
				}
			}

			throw new AccessDeniedException("您没有权限, 请联系系统管理员");

		} finally {
			// 完成权限校验后,清理线程变量中的权限数据。
			HtInvocationSecurityMetadataSourceService.clearMapThreadLocal();
		}
	}
}

六、RBAC权限模型

在这里插入图片描述

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值