springboot整合SpringSecurity

文章目录

SpringSecurity介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

SpringSecurity入门

Spring Security主要jar包功能介绍

  • spring-security-core.jar:核心包,任何Spring Security功能都需要此包。
  • spring-security-web.jar:web工程必备,包含过滤器和相关的Web安全基础结构代码。
  • spring-security-config.jar:用于解析xml配置文件,用到Spring Security的xml配置文件的就要用到此包。
  • spring-security-taglibs.jar:Spring Security提供的动态标签库,jsp页面可以用
    spring-boot-starter-security 进行的集成

SpringSecurity常用过滤器介绍

过滤器是一种典型的AOP思想

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。

WebAsyncManagerIntegrationFilter

此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager

HeaderWriterFilter

向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制(仅限于JSP页面)

CsrfFilter

csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,
如果不包含,则报错。起到防止csrf攻击的效果。

LogoutFilter

匹配URL为/logout的请求,实现用户退出,清除认证信息。

UsernamePasswordAuthenticationFilter

认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。

DefaultLoginPageGeneratingFilter

如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。

DefaultLogoutPageGeneratingFilter

由此过滤器可以生产一个默认的退出登录页面

BasicAuthenticationFilter

此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。

RequestCacheAwareFilter

通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest

SecurityContextHolderAwareRequestFilter

针对ServletRequest进行了一次包装,使得request具有更加丰富的API

AnonymousAuthenticationFilter

当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。

当用户以游客身份登录的时候,也就是可以通过设置某些接口可以匿名访问

SessionManagementFilter

SecurityContextRepository限制同一用户开启多个会话的数量

ExceptionTranslationFilter

异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常

FilterSecurityInterceptor

获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权

该过滤器限制哪些资源可以访问,哪些不能够访问

SpringSecurity过滤器链加载原理

通过前面十五个过滤器功能的介绍,它们都是怎么被加载出来的?

DelegatingFilterProxy

入口为springSecurityFilterChain的过滤器DelegatingFilterProxy,
DelegatingFilterProxy通过springSecurityFilterChain这个名称,得到了一个FilterChainProxy过滤器
在这里插入图片描述
在这里插入图片描述

FilterChainProxy

在这里插入图片描述
在这里插入图片描述

SecurityFilterChain

最后看SecurityFilterChain,这是个接口,实现类也只有一个,这才是web.xml中配置的过滤器链对象!

在这里插入图片描述

springboot 整合spring-security 搭建工程

基于SpringBoot搭建web工程 ,项目名为“spring-security-demo”

导入依赖

项目依赖springboot 核心依赖:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

主启动类

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

Web控制器

当用户认证成功之后会重定向到该方法,返回“登录成功”给用户

@Controller
public class AuthController {
	//登录成功后重定向地址
    @RequestMapping("/loginSuccess")
    @ResponseBody
    public String loginSuccess(){
        return "登录成功";
} 
}

配置SpringSecurity

SpringSecurity提供了一个配置类WebSecurityConfigurerAdapter用来提供给程序员对SpringSecurity做自定义配置,我们需要配置如下几个信息:

  • 创建UserDetailService的Bean,该组件是用来加载用户认证信息
  • 配置编码器,通过该编码器对密码进行加密匹配。
  • 授权规则配置,哪些资源需要什么权限…
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

     //提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager inMemoryUserDetailsManager = 
        new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
        return inMemoryUserDetailsManager;
    }
  

    //密码编码器:不加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }
    
    //授权规则配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()                                //授权配置
                .antMatchers("/login").permitAll()  //登录路径放行
                .anyRequest().authenticated()                   //其他路径都要认证之后才能访问
                .and().formLogin()                              //允许表单登录
                .successForwardUrl("/loginSuccess")             // 设置登陆成功页
                .and().logout().permitAll()                    //登出路径放行
                .and().csrf().disable();                        //关闭跨域伪造检查
    }
}

浏览器访问:http://localhost:8080/login ,进入Security提供的登录页面,输入账号:zs 密码 123 完成登录,登出成功页面显示 “登录成功”

认证流程

  • SpringSecurity根据我们在WebSecurityConfig中的配置会对除了“/login”之外的资源进行拦截做登录检查,
  • 如果没有登录会跳转到默认的登录页面“/login” 做登录
  • 输入用户名和密码后点击登录,SpringSecurity的拦截器会拦截到登录请求,获取到用户名和密码封装成认证对象(Token对象),底层会调用InMemoryUserDetailsService通过用户名获取用户的认证信息(用户名,密码,权限等,这些信息通常是在数据库存储的)
  • 然后执行认证工作:Security把登录请求传入的密码和InMemoryUserDetailsService中加载的用户的密码进行匹配(通过PasswordEncoder), 匹配成功跳转成功地址,认证失败就返回错误

认证流程原理

SpringSecurity是基于Filter实现认证和授权,底层通过FilterChainProxy代理去调用各种Filter(Filter链),Filter通过调用AuthenticationManager完成认证 ,通过调用AccessDecisionManager完成授权,SpringSecurity中核心的过滤器链详细如下:
在这里插入图片描述

Security相关概念

Authentication

认证对象,用来封装用户的认证信息(账户状态,用户名,密码,权限等)所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如 最容易理解的UsernamePasswordAuthenticationToken,其中包含了用户名和密码

Authentication常用的实现类:

UsernamePasswordAuthenticationToken:用户名密码登录的Token
AnonymousAuthenticationToken:针对匿名用户的Token
RememberMeAuthenticationToken:记住我功能的的Token

AuthenticationManager

用户认证的管理类,所有的认证请求(比如login)都会通过提交一个封装了到了登录信息的Token对象给 AuthenticationManager的authenticate()方法来实现认证。AuthenticationManager会 调
用AuthenticationProvider.authenticate进行认证。认证成功后,返回一个包含了认 证
信息的Authentication对象。

AuthenticationProvider

认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我 是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通 过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风, 主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。 前 面讲了AuthenticationManager只是一个代理接口,真正的认证就是由 AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider, 每个provider通过实现一个support方法来表示自己支持那种Token的认证。 AuthenticationManager默认的实现类是ProviderManager。

UserDetailService

用户的认证通过Provider来完成,而Provider会通过UserDetailService拿到数据库(或 内存)中的认证信息然后和客户端提交的认证信息做校验。虽然叫Service,但是我更愿 意把它认为是我们系统里经常有的UserDao。

SecurityContext

当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用 户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识 Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过 SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过 SecurityUtils.getSubject()到达同样的目的

认证流程原理

  • 请求过来会被过滤器链中的UsernamePasswordAuthenticationFilter拦截到,请求中的用户名和密码被封装成UsernamePasswordAuthenticationToken(Authentication的实现类)
  • 过滤器将UsernamePasswordAuthenticationToken提交给认证管理器(AuthenticationManager)进行认证.
  • AuthenticationManager委托AuthenticationProvider(DaoAuthenticationProvider)进行认证,AuthenticationProvider通过调用UserDetailsService获取到数据库中存储的用户信息(UserDetails),然后调用passwordEncoder密码编码器对- - --UsernamePasswordAuthenticationToken中的密码和UserDetails中的密码进行比较
  • AuthenticationProvider认证成功后封装Authentication并设置好用户的信息(用户名,密码,权限等)返回
  • Authentication被返回到UsernamePasswordAuthenticationFilter,通过调用- SecurityContextHolder工具把Authentication封装成SecurityContext中存储起来。然后-UsernamePasswordAuthenticationFilter调用- AuthenticationSuccessHandler.onAuthenticationSuccess做认证成功后续处理操作
  • 最后SecurityContextPersistenceFilter通过SecurityContextHolder.getContext()获取到-- SecurityContext对象然后调用SecurityContextRepository将SecurityContext存储起来,然后调用SecurityContextHolder.clearContext方法清理SecurityContext。

注意:SecurityContext是一个和当前线程绑定的工具,在代码的任何地方都可以通过SecurityContextHolder.getContext()获取到登陆信息。

SecurityContextPersistenceFilter

这个filter是整个filter链的入口和出口,请求开始会从SecurityContextRepository中 获取SecurityContext对象并设置给SecurityContextHolder。在请求完成后将
SecurityContextHolder持有的SecurityContext再保存到配置好的
DecurityContextRepository中,同时清除SecurityContextHolder中的SecurityContext

总结一下:SecurityContextPersistenceFilter它的作用就是请求来的时候将包含了认证授权信息的SecurityContext对象从SecurityContextRepository中取出交给SecurityContextHolder工具类,方便我们通过SecurityContextHolder获取SecurityContext从而获取到认证授权信息,请求走的时候又把SecurityContextHolder清空,源码如下:


public class SecurityContextPersistenceFilter extends GenericFilterBean {
  ...省略...
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
  ...省略部分代码...
  HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
  			response);
  //从SecurityContextRepository获取到SecurityContext 
  	SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

  	try {
  	 //把 securityContext设置到SecurityContextHolder,如果没认证通过,这个SecurtyContext就是空的
  		SecurityContextHolder.setContext(contextBeforeChainExecution);
  		//调用后面的filter,比如掉用usernamepasswordAuthenticationFilter实现认证
  		chain.doFilter(holder.getRequest(), holder.getResponse());

  	}
  	finally {
  		//如果认证通过了,这里可以从SecurityContextHolder.getContext();中获取到SecurityContext
  		SecurityContext contextAfterChainExecution = SecurityContextHolder
  				.getContext();
  		// Crucial removal of SecurityContextHolder contents - do this before anything
  		// else.
  		 //删除SecurityContextHolder中的SecurityContext 
  		SecurityContextHolder.clearContext();
  		//把SecurityContext 存储到SecurityContextRepository
  		repo.saveContext(contextAfterChainExecution, holder.getRequest(),
  				holder.getResponse());
  		request.removeAttribute(FILTER_APPLIED);

  		if (debug) {
  			logger.debug("SecurityContextHolder now cleared, as request processing completed");
  		}
  	}
...省略...

UsernamePasswordAuthenticationFilter

它的重用是,拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括username,password等封装成UsernamePasswordAuthenticationToken,然后调用
AuthenticationManager的认证方法进行认证。

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
	// ~ Static fields/initializers
	// =====================================================================================
	//从登录请求中获取参数:username,password的名字
	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	//默认支持POST登录
	private boolean postOnly = true;
	//默认拦截/login请求,Post方式
	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}

	// ~ Methods
	// ========================================================================================================

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
			//判断请求是否是POST
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		//获取到用户名和密码
		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();
		//用户名和密码封装Token
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		//设置details属性
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		//调用AuthenticationManager().authenticate进行认证,参数就是Token对象
		return this.getAuthenticationManager().authenticate(authRequest);
	}


AuthenticationManager

请求通过UsernamePasswordAuthenticationFilter调用AuthenticationManager,默认走的实现类是ProviderManager,它会找到能支持当前认证的AuthenticationProvider实现类调用器authenticate方法执行认证,认证成功后会清除密码,然后抛出AuthenticationSuccessEvent事件

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {
		...省略...
		//这里authentication 是封装了登录请求的认证参数,
		//即:UsernamePasswordAuthenticationFilter传入的Token对象
	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;
		boolean debug = logger.isDebugEnabled();
		//找到所有的AuthenticationProvider ,选择合适的进行认证
		for (AuthenticationProvider provider : getProviders()) {
			//是否支持当前认证
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				//调用provider执行认证
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
				...省略...
		}
		...省略...
		//result就是Authentication ,使用的实现类依然是UsernamepasswordAuthenticationToken,
		//封装了认证成功后的用户的认证信息和授权信息
		if (result != null) {
			if (eraseCredentialsAfterAuthentication
				&& (result instanceof CredentialsContainer)) {
			// Authentication is complete. Remove credentials and other secret data
			// from authentication
			//这里在擦除登录密码
			((CredentialsContainer) result).eraseCredentials();
		}

		// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
		// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
		if (parentResult == null) {
			//发布事件
			eventPublisher.publishAuthenticationSuccess(result);
		}
		return result;
	}



DaoAuthenticationProvider

请求到达AuthenticationProvider,默认实现是DaoAuthenticationProvider,它的作用是根据传入的Token中的username调用UserDetailService加载数据库中的认证授权信息(UserDetails),然后使用PasswordEncoder对比用户登录密码是否正确

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
		//密码编码器
		private PasswordEncoder passwordEncoder;
		//UserDetailsService ,根据用户名加载UserDetails对象,从数据库加载的认证授权信息
		private UserDetailsService userDetailsService;
		//认证检查方法
		protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
		//获取密码
		String presentedPassword = authentication.getCredentials().toString();
		//通过passwordEncoder比较密码,presentedPassword是用户传入的密码,userDetails.getPassword()是从数据库加载到的密码
		//passwordEncoder编码器不一样比较密码的方式也不一样
		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}

	//检索用户,参数为用户名和Token对象
	protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			//调用UserDetailsService的loadUserByUsername方法,
			//根据用户名检索数据库中的用户,封装成UserDetails 
			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);
		}
	}
	//创建认证成功的认证对象Authentication,使用的实现是UsernamepasswordAuthenticationToken,
	//封装了认证成功后的认证信息和授权信息,以及账户的状态等
	@Override
	protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		boolean upgradeEncoding = this.userDetailsPasswordService != null
				&& this.passwordEncoder.upgradeEncoding(user.getPassword());
		if (upgradeEncoding) {
			String presentedPassword = authentication.getCredentials().toString();
			String newPassword = this.passwordEncoder.encode(presentedPassword);
			user = this.userDetailsPasswordService.updatePassword(user, newPassword);
		}
		return super.createSuccessAuthentication(principal, authentication, user);
	}
	...省略...

这里提供了三个方法

  • additionalAuthenticationChecks:通过passwordEncoder比对密码
  • retrieveUser:根据用户名调用UserDetailsService加载用户认证授权信息
  • createSuccessAuthentication:登录成功,创建认证对象Authentication
    然而你发现 DaoAuthenticationProvider 中并没有authenticate认证方法,真正的认证逻辑是通过父类AbstractUserDetailsAuthenticationProvider.authenticate方法完成的
public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {
		//认证逻辑
		public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
			//得到传入的用户名
			String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
				//从缓存中得到UserDetails
			boolean cacheWasUsed = true;
			UserDetails user = this.userCache.getUserFromCache(username);
			if (user == null) {
			cacheWasUsed = false;

			try {
				//检索用户,底层会调用UserDetailsService加载数据库中的UserDetails对象,保护认证信息和授权信息
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				...省略...
			}

			try {
				//前置检查,主要检查账户是否锁定,账户是否过期等
				preAuthenticationChecks.check(user);
				//比对密码在这个方法里面比对的
				additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (AuthenticationException exception) {
			...省略...
			}
			//后置检查
			postAuthenticationChecks.check(user);
	
			if (!cacheWasUsed) {
				//设置UserDetails缓存
				this.userCache.putUserInCache(user);
			}
	
			Object principalToReturn = user;
	
			if (forcePrincipalAsString) {
				principalToReturn = user.getUsername();
			}
			//认证成功,创建Auhentication认证对象
			return createSuccessAuthentication(principalToReturn, authentication, user);
}

UsernamePasswordAuthenticationFilter

认证成功,请求会重新回到UsernamePasswordAuthenticationFilter,然后会通过其父类AbstractAuthenticationProcessingFilter.successfulAuthentication方法将认证对象封装成SecurityContext设置到SecurityContextHolder中

protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}

		//认证成功,吧Authentication 设置到SecurityContextHolder
		SecurityContextHolder.getContext().setAuthentication(authResult);
		//处理记住我业务逻辑
		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		//重定向登录成功地址
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

授权流程分析

授权一定是在认证通过之后,授权流程是通过FilterSecurityInterceptor拦截器来完成,FilterSecurityInterceptor通过调用SecurityMetadataSource来获取当前访问的资源所需要的权限,然后通过调用AccessDecisionManager投票决定当前用户是否有权限访问当前资源。授权流程如下

  1. 当客户端向某个资源发起请求,请求到达FilterSecurityInterceptor,然后会调用其父类AbstractSecurityInterceptor
    的beforeInvocation方法做授权之前的准备工作

  2. 在beforeInvocation法中通过SecurityMetadataSource…getAttributes(object);获得资源所需要的访问权限 ,通过SecurityContextHolder.getContext().getAuthentication()获取当前认证用户的认证信息,即包含了认证信息和权限信息的Authentication对象

  3. 然后FilterSecurityInterceptor通过调用AccessDecisionManager.decide(authenticated, object, attributes);进行授权(authenticated中有用户的权限列表,attributes是资源需要的权限),该方法使用投票器投票来决定用户是否有资源访问权限

  4. AccessDecisionManager接口有三个实现类,他们通过通过AccessDecisionVoter投票器完成投票,三种投票策略如下:

  • AffirmativeBased : 只需有一个投票赞成即可通过
  • ConsensusBased:需要大多数投票赞成即可通过,平票可以配置
  • UnanimousBased:需要所有的投票赞成才能通过

而投票器也有很多,如RoleVoter通过角色投票,如果ConfigAttribute是以“ROLE_”开头的,则将使用RoleVoter进行投票,AuthenticatedVoter 是用来区分匿名用户、通过Remember-Me认证的用户和完全认证的用户(登录后的)

  1. 投票通过,请求放行,响应对应的资源给客户端

Web授权

web授权API说明

在Security配置类中,可以通过HttpSecurity.authorizeRequests()给资源指定访问的权限,其API如下:

  • anyRequest():任何请求
  • antMatchers(“/path”) :匹配某个资源路径
  • authenticationed() : 保护URL需要登录访问
  • permitAll():指定url无需保护(放行)一般用户静态资源
  • hasRole(String role):某个资源需要用户拥有什么样的role才能访问
  • hasAuthority(String authority):某个资源需要用户拥有什么样的权限才能访问
  • hasAnyRole(String …roles):某个资源拥有指定角色中的一个就能访问
  • hasAnyAuthority(String … authorities):某个资源拥有指定权限中的一个就能访问
  • access(String attribute):该方法使用SPEL表达式,可以创建复杂的限制
  • hasIpAddress(String ip):拥有什么样的ip或子网可以访问该资源

授权规则注意

我们通常把细节的规则设置在前面,范围比较大的规则设置放在后面,返例:如有以下配置

.antMatchers("/admin/**").hasAuthority(“admin”)
.antMatchers("/admin/login").permitAll();

那么第二个权限规则将不起作用,因为第一个权限规则覆盖了第二个权限规则
因为权限的设置是按照从上到下的优先级。及满足了最开始的权限设置,那么后面的设置就不起作用了。

Web授权实战

我们这一次在入门案例的基础上进行修改,所有的认证数据,授权数据都从数据库进行获取

1.编写controller

@RestController
public class DeptController {

    @RequestMapping("/dept/list")
    public String list(){
        return "dept.list";
    }

    @RequestMapping("/dept/add")
    public String add(){
        return "dept.add";
    }

    @RequestMapping("/dept/update")
    public String update(){
        return "dept.update";
    }

    @RequestMapping("/dept/delete")
    public String delete(){
        return "dept.delete";
    }

}
---------------------------------------------------------
@RestController
public class EmployeeController {

    @RequestMapping("/employee/list")
    public String list(){
        return "employee.list";
    }
    @RequestMapping("/employee/add")
    public String add(){
        return "employee.add";
    }
    @RequestMapping("/employee/update")
    public String update(){
        return "employee.update";
    }
    @RequestMapping("/employee/delete")
    public String delete(){
        return "employee.delete";
    }
}

方法上的requestmapping就对应了权限表t_permission的资源

2.配置HttpSecurity

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        List<Permission> permissions = permissionMapper.listPermissions();
     ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
                expressionInterceptUrlRegistry = http.csrf().disable()   //关闭CSRF跨站点请求伪造防护
                .authorizeRequests()          //对请求做授权处理
                .antMatchers("/login").permitAll()  //登录路径放行
                .antMatchers("/login.html").permitAll();//对登录页面跳转路径放行

        //动态添加授权:从数据库动态查询出,哪些资源需要什么样的权限
        for(Permission permission : permissions){
            System.out.println(permission.getResource()+" - "+permission.getSn());
            //如: /employee/list    需要     employee:list 权限才能访问
            expressionInterceptUrlRegistry.antMatchers(permission.getResource()).hasAuthority(permission.getSn());
        }

        expressionInterceptUrlRegistry
                .anyRequest().authenticated() //其他路径都要拦截
                .and().formLogin()  //允许表单登录, 设置登陆页
                .successForwardUrl("/loginSuccess") // 设置登陆成功页
                .loginPage("/login.html")   //登录页面跳转地址
                .loginProcessingUrl("/login")   //登录处理地址
                .and().logout().permitAll();    //登出

    }

解释:上面代码从权限表查询出了所有的资源(对应controller中的Requestmapping路径),然后通过循环调用expressionInterceptUrlRegistry.antMatchers(permission.getResource())
.hasAuthority(permission.getSn()); 进行一一授权,指定哪个资源需要哪个权限才能访问。

3.修改UserDetailService加载用户权限


public UserDetails loadUserByUsername(String username)  {
        Login loginFromMysql = loginMapper.selectByUsername(username);
        if(loginFromMysql == null){
            throw new UsernameNotFoundException("无效的用户名");
        }
        //前台用户
        List<GrantedAuthority> permissions = new ArrayList<>();
        List<Permission> permissionSnList =
 systemManageClient.listByUserId(loginFromMysql.getId());
        permissionSnList.forEach(permission->{
            System.out.println("用户:"+username+" :加载权限 :"+permission.getSn());
            permissions.add(new SimpleGrantedAuthority(permission.getSn()));
        });

        return new User(username,loginFromMysql.getPassword(),permissions);
    }

这里在通过UserDetailServer加载用户认证信息的时候就把用户的权限信息一并加载

方法授权

SpringSecurity提供了一些授权的注解让我们可以在service,controller等的方法上贴注解进行授权,即在方法上指定方法方法需要什么样的权限才能访问

@Secured

标记方法需要有什么样的权限才能访问,这个注解需要在配置类上开启授权注解支持;

@EnableGlobalMethodSecurity(securedEnabled=true) ,然后在Controller方法上贴该注解如:

@Secured(“IS_AUTHENTICATED_ANONYMOUSLY”) :方法可以匿名访问
@Secured(“ROLE_DEPT”) ,需要拥有部门的角色才能访问,ROLE_前缀是固定的

1.开启Secured授权支持

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

2.使用@Secured进行方法授权

@RequestMapping("/employee/list")
@Secured(“ROLE_employee:list”)
public String list(){
return “employee.list”;
}

解释:这里使用了 @Secured(“ROLE_employee:list”) 意思是 “/employee/list” 这个资源需要“ROLE_employee:list”权限才能访问,如果认证的用户有该权限(UserDetailService中加载)包含了“ROLE_employee:list”即可访问该资源,否则不能访问。
注意:对于方法授权,没有贴注解的方法默认是匿名访问。@Secured注解授权是需要加上前缀“ROLE_”

.@PreAuthorize

PreAuthorize适合进入方法前的权限验证,拥有和Secured同样的功能,甚至更强大,该注解需要在配置类开启:@EanbleGlobalMethodSecurity(prePostEnabled=true) 方法授权支持,然后在Controller贴注解如下:

@PreAuthorize(“isAnonymous()”) : 方法匿名访问
@PreAuthorize(“hasAnyAuthority(‘p_user_list’,‘p_dept_list’)”) :拥有p_user_listr或者p_dept_list的权限能访问
@PreAuthorize(“hasAuthority(‘p_transfer’) and hasAuthority(‘p_read_accout’)”) : 拥有p_transfer权限和p_read_accout权限才能访问.
该标签不需要有固定的前缀。

1.开启@PreAuthorize授权支持

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled= true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

2.使用@PreAuthorize进行方法授权

@PreAuthorize(“hasAnyAuthority(‘employee:add’,‘employee:update’)”)
@RequestMapping("/employee/add")
public String add(){
return “employee.add”;
}

指明了方法必须要有 employee:add 或者 employee:update的权限才能访问 , 该注解不需要有固定的前缀。注意格式“@PreAuthorize(“hasAuthority(‘employee:add’)”)” ,hasAuthority不能省略,括号中是单引号。

3.3.@PostAuthorize

该注解使用并不多,适合在方法执行后再进行权限验证,使用该注解需要在配置类开启:@EanbleGlobalMethodSecurity(prePostEnabled=true) 方法授权支持,用法同 @PreAuthorize一样

到这里授权流程就完成了,这里实现了两种方式的授权,WEB授权和方法授权,WEB授权可以实现统一配置,而方法授权则需要很多的在方法上帖注解,各有各的好处,你个可以根据项目情况自行选择。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值