SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理

Security参考文档:https://spring.io/guides/gs/securing-web/

thymeleaf参考文档:https://www.thymeleaf.org/doc/articles/springsecurity.html

thymeleaf集成security源码:https://github.com/thymeleaf/thymeleaf-extras-springsecurity

官网文档:https://docs.spring.io/spring-security/site/docs/5.2.2.RELEASE/reference/htmlsingle/#community

源码地址:https://github.com/877148107/springboot_integrate/tree/master/springboot-integrat-security

目录

简介

SpringBoot整合Security

1)、效果图

2)、引入thymeleaf、security的pom文件

3)、编写security的配置类

1.定制请求授权的规则

2.开启自动配置的登录模式

4.开启自动配置的注销模式

5.定制认证规则

4)、security页面控制

1.获取登录名

2.角色权限

3.更多标签的使用

5)、页面跳转

Spring Security运行原理

1)、初始化认证配置规则

2)、初始化请求授权规则

3)、登录验证原理

整合过程中出现的错误信息

1)、密码认证


  • 简介

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。

Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求

Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型。他可以实现强大的web安全控制。对于安全控制,我们仅需引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理。

特征

  • 对身份验证和授权的全面且可扩展的支持

  • 防止攻击,例如会话固定,点击劫持,跨站点请求伪造等

  • Servlet API集成

  • 与Spring Web MVC的可选集成

  • 与thymeleaf的可选集成

核心类

  • WebSecurityConfigurerAdapter:自定义Security策略

  • AuthenticationManagerBuilder:自定义认证策略

  • @EnableWebSecurity:开启WebSecurity模式

  • SpringBoot整合Security

1)、效果图

SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第1张图片

SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第2张图片

SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第3张图片

SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第4张图片

2)、引入thymeleaf、security的pom文件

使用bootstrap的demo作为基础模板页面,详细整合thymeleaf说明:https://blog.csdn.net/WMY1230/article/details/103724042



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.4.RELEASE
         
    
    com.wmy.integrate
    springboot-integrat-security
    0.0.1-SNAPSHOT
    springboot-integrat-security
    Demo project for Spring Boot

    
        1.8
    

    
        
        
            org.thymeleaf.extras
            thymeleaf-extras-springsecurity5
        

        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        
        
        
            org.webjars
            jquery
            3.3.1
        
        
            org.webjars
            bootstrap
            4.0.0
        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
        
            org.springframework.security
            spring-security-test
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


3)、编写security的配置类

1.定制请求授权的规则

控制没有请求路径的权限功能,达到只有这个角色的权限才能访问

http.authorizeRequests()......

2.开启自动配置的登录模式

定制登录表单参数、登录请求、登录成功后跳转的请求、登录失败后跳转的请求

http.formLogin()......

4.开启自动配置的注销模式

可以自己定制注销的请求路径默认是/logout,并且默认请求方式是post。当你使用超链接作为注销按钮发送请求时默认使用的get,因此需要自己指定请求的方式

http.logout()......

5.定制认证规则

认证规则可以从内存里面获取也可以从数据库进行获取验证,这里先使用内存进行认证。后面更新使用数据库的security认证方式。。。。。

auth.inMemoryAuthentication()......
auth.jdbcAuthentication()......
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 定制请求授权规则
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //所有角色都能访问
        http.authorizeRequests().antMatchers("/").permitAll()
                //订单管理员的访问权限
                .antMatchers("/page/order/**","/page/report/**","/page/customer/**").hasRole("orderManager")
                //产品管理员的访问权限
                .antMatchers("/page/product/**","/page/report/**").hasRole("productManager")
                //系统管理员的访问权限
                .antMatchers("/page/**").hasRole("systemManager")
                //登录才能访问
                .antMatchers("/main.html").authenticated();
        //开启自动配置的登录模式
        http.formLogin()
                //定制表单的名称
                .usernameParameter("userName").passwordParameter("password")
                //the URL "/login", redirecting to "/login?error" for authentication failure.
                //这里配置默认是SpringSecurity的登录页面,需要配置成自己的登录页面
                .loginPage("/")
                //定制URL处理器登录请求
                .loginProcessingUrl("/user/login")
                //登录成功后跳转的页面
                .successForwardUrl("/page/main")
                //登录失败后跳转的页面
                .failureForwardUrl("/");
        //开启自动配置的注销功能,注销请求路径/logout并注销session
        http.logout()
                //由于页面采用的是超链接get请求方式进行注销,而自动配置默认使用的post请求
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout","GET"))
                //配置注销成功跳转的url
                .logoutSuccessUrl("/");
        //开启自动配置的记住我,这里form表单的name默认是remember-me,也可以自己定义参数名
        http.rememberMe();
    }

    /**
     * 定制认证规则
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在内存里面校验
        auth.inMemoryAuthentication()
                //这里需要对密码进行编码不然会抛异常,详细情况可以参考错误信息及官方文档
                .passwordEncoder(new BCryptPasswordEncoder())
                //分别赋予登录的角色编码
                .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("systemManager","productManager","orderManager")
                .and()
                .withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("productManager")
                .and()
                .withUser("lisi").password(new BCryptPasswordEncoder().encode("123456")).roles("orderManager");
    }
}

4)、security页面控制

引入thymeleaf集成security的名称空间

xmlns:sec="http://www.thymeleaf.org/extras/spring-security"

1.获取登录名

可以使用thymeleaf集成security的标签SPEL表达式获取


    您好,

2.角色权限

 判断当前登录人的角色是否有访问权限

3.更多标签的使用

thymeleaf参考文档:https://www.thymeleaf.org/doc/articles/springsecurity.html

thymeleaf集成security源码:https://github.com/thymeleaf/thymeleaf-extras-springsecurity




    
    Title










5)、页面跳转

@RequestMapping("/page")
@Controller
public class PageController {

    /**
     * 跳转到主页面
     * @return
     */
    @RequestMapping("/main")
    public String mianPage(){
        return "redirect:/main.html";
    }

    /**
     * 跳转到订单页面
     * @return
     */
    @RequestMapping("/order")
    public String orderPage(){
        return "/page/order/order";
    }

    /**
     * 跳转到产品页面
     * @return
     */
    @RequestMapping("/product")
    public String productPage(){
        return "/page/product/product";
    }

    /**
     * 跳转到顾客页面
     * @return
     */
    @RequestMapping("/customer")
    public String customerPage(){
        return "/page/customer/customer";
    }

    /**
     * 跳转到报表页面
     * @return
     */
    @RequestMapping("report")
    public String reportPage(){
        return "/page/report/report";
    }

    /**
     * 跳转到系统页面
     * @return
     */
    @RequestMapping("/system")
    public String systemPage(){
        return "/page/system";
    }
}
  • Spring Security运行原理

1)、初始化认证配置规则

这里对配置的用户名、密码及角色配置初始化加载进入内存中。利用User里面的内部类UserBuilder对象进行保存。这里的角色并且都默认加了ROLE_前缀

	public void init(final WebSecurity web) throws Exception {
		final HttpSecurity http = getHttp();
		web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
			FilterSecurityInterceptor securityInterceptor = http
					.getSharedObject(FilterSecurityInterceptor.class);
			web.securityInterceptor(securityInterceptor);
		});
	}
	protected final HttpSecurity getHttp() throws Exception {
		if (http != null) {
			return http;
		}

		DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
				.postProcess(new DefaultAuthenticationEventPublisher());
		localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
        //初始化认证规则,并加入到内存中
		AuthenticationManager authenticationManager = authenticationManager();
		authenticationBuilder.parentAuthenticationManager(authenticationManager);
		authenticationBuilder.authenticationEventPublisher(eventPublisher);
		Map, Object> sharedObjects = createSharedObjects();

		http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
				sharedObjects);
		if (!disableDefaults) {
			// @formatter:off
			http
				.csrf().and()
				.addFilter(new WebAsyncManagerIntegrationFilter())
				.exceptionHandling().and()
				.headers().and()
				.sessionManagement().and()
				.securityContext().and()
				.requestCache().and()
				.anonymous().and()
				.servletApi().and()
				.apply(new DefaultLoginPageConfigurer<>()).and()
				.logout();
			// @formatter:on
			ClassLoader classLoader = this.context.getClassLoader();
			List defaultHttpConfigurers =
					SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);

			for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
				http.apply(configurer);
			}
		}
		configure(http);
		return http;
	}

 

protected AuthenticationManager authenticationManager() throws Exception {
		if (!authenticationManagerInitialized) {
			configure(localConfigureAuthenticationBldr);
			if (disableLocalConfigureAuthenticationBldr) {
				authenticationManager = authenticationConfiguration
						.getAuthenticationManager();
			}
			else {
				authenticationManager = localConfigureAuthenticationBldr.build();
			}
			authenticationManagerInitialized = true;
		}
		return authenticationManager;
	}
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在内存里面校验
        auth.inMemoryAuthentication()
                //这里需要对密码进行编码不然会抛异常,详细情况可以参考错误信息及官方文档
                .passwordEncoder(new BCryptPasswordEncoder())
                //分别赋予登录的角色编码
                .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("systemManager","productManager","orderManager")
                .and()
                .withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("productManager")
                .and()
                .withUser("lisi").password(new BCryptPasswordEncoder().encode("123456")).roles("orderManager");
    }

 SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第5张图片

2)、初始化请求授权规则

这里初始化请求权限、登录、注销等规则信息,并且启动相关的配置比如可以配置上启动防止跨域请求的配置等等。。。。

	protected final HttpSecurity getHttp() throws Exception {
		if (http != null) {
			return http;
		}

		DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
				.postProcess(new DefaultAuthenticationEventPublisher());
		localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
        //初始化认证规则,并加入到内存中
		AuthenticationManager authenticationManager = authenticationManager();
		authenticationBuilder.parentAuthenticationManager(authenticationManager);
		authenticationBuilder.authenticationEventPublisher(eventPublisher);
		Map, Object> sharedObjects = createSharedObjects();

		http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
				sharedObjects);
		if (!disableDefaults) {
			// @formatter:off
			http
				.csrf().and()
				.addFilter(new WebAsyncManagerIntegrationFilter())
				.exceptionHandling().and()
				.headers().and()
				.sessionManagement().and()
				.securityContext().and()
				.requestCache().and()
				.anonymous().and()
				.servletApi().and()
				.apply(new DefaultLoginPageConfigurer<>()).and()
				.logout();
			// @formatter:on
			ClassLoader classLoader = this.context.getClassLoader();
			List defaultHttpConfigurers =
					SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);

			for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
				http.apply(configurer);
			}
		}
//初始化请求授权规则、登录、注销、记住我。。。。。
		configure(http);
		return http;
	}

加载到内存中的多个配置类

{Class@5308} "class org.springframework.security.config.annotation.web.configurers.CsrfConfigurer" -> {ArrayList@5483}  size = 1
{Class@5315} "class org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer" -> {ArrayList@5484}  size = 1
{Class@5316} "class org.springframework.security.config.annotation.web.configurers.HeadersConfigurer" -> {ArrayList@5485}  size = 1
{Class@5336} "class org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer" -> {ArrayList@5486}  size = 1
{Class@5341} "class org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer" -> {ArrayList@5487}  size = 1
{Class@5342} "class org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer" -> {ArrayList@5488}  size = 1
{Class@5343} "class org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer" -> {ArrayList@5489}  size = 1
{Class@5345} "class org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer" -> {ArrayList@5490}  size = 1
{Class@5347} "class org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer" -> {ArrayList@5491}  size = 1
{Class@5357} "class org.springframework.security.config.annotation.web.configurers.LogoutConfigurer" -> {ArrayList@5492}  size = 1
{Class@5367} "class org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer" -> {ArrayList@5493}  size = 1
{Class@5381} "class org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer" -> {ArrayList@5494}  size = 1
{Class@5416} "class org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer" -> {ArrayList@5495}  size = 1

SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第6张图片

3)、登录验证原理

使用FilterChainProxy代理执行多个过滤器filter,拦截登录请求、注销等等。登录用到了UsernamePasswordAuthenticationFilter用户名密码验证的过滤器

@Override
		public void doFilter(ServletRequest request, ServletResponse response)
				throws IOException, ServletException {
			if (currentPosition == size) {
				if (logger.isDebugEnabled()) {
					logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
							+ " reached end of additional filter chain; proceeding with original chain");
				}

				// Deactivate path stripping as we exit the security filter chain
				this.firewalledRequest.reset();

				originalChain.doFilter(request, response);
			}
			else {
				currentPosition++;

				Filter nextFilter = additionalFilters.get(currentPosition - 1);

				if (logger.isDebugEnabled()) {
					logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
							+ " at position " + currentPosition + " of " + size
							+ " in additional filter chain; firing Filter: '"
							+ nextFilter.getClass().getSimpleName() + "'");
				}

				nextFilter.doFilter(request, response, this);
			}
		}
	}
0 = {WebAsyncManagerIntegrationFilter@5443} 
1 = {SecurityContextPersistenceFilter@6879} 
2 = {HeaderWriterFilter@6878} 
3 = {CsrfFilter@6875} 
4 = {LogoutFilter@6873} 
5 = {UsernamePasswordAuthenticationFilter@5517} 
6 = {RequestCacheAwareFilter@7008} 
7 = {SecurityContextHolderAwareRequestFilter@7007} 
8 = {RememberMeAuthenticationFilter@7006} 
9 = {AnonymousAuthenticationFilter@7141} 
10 = {SessionManagementFilter@7142} 
11 = {ExceptionTranslationFilter@7143} 
12 = {FilterSecurityInterceptor@7144} 

 UsernamePasswordAuthenticationFilter,对用户名密码的验证,并获取登录人的角色,验证通过后添加记住我的cookie和session

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		try {
			//用户密码密码的验证及角色获取
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			//session的管理
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		//验证成功添加cookie和session
		successfulAuthentication(request, response, chain, authResult);
	}
	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		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();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

 ProviderManager,递归循环验证管理是支持验证。根据用户名获取缓存中的用户信息

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			//遍历provider 是否支持class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
			if (!provider.supports(toTest)) {
				continue;
			}

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

			try {
				//利用验证管理器去验证用户名和密码是否正确
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

		//如果结果为空使用递归继续判定下一个管理器
		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}

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

		// Parent was null, or didn't authenticate (or throw an exception).

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

		// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}

SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第7张图片

 AbstractUserDetailsAuthenticationProvider->authenticate()验证用户密码是否正确并获取对应角色

SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第8张图片

SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第9张图片

 AbstractUserDetailsAuthenticationProvider->createSuccessAuthentication()将查询的角色等详细赋值,

	protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());

		return result;
	}

SpringBoot(十八)——————SpringBoot整合Security、SpringSecurity原理_第10张图片

  • 整合过程中出现的错误信息

1)、密码认证

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

SpringSecurity对密码需要进行编码认证,不支持使用明文认证的方式。

解决方案:https://docs.spring.io/spring-security/site/docs/5.2.2.RELEASE/reference/htmlsingle/#servlet-hello

密码的一般格式为:

{id} encodedPassword

这样id的标识符是用于查找PasswordEncoder应使用的标识符,并且encodedPassword是所选的原始编码密码PasswordEncoder。在id必须在密码的开始,开始{和结束}。如果id找不到,id则将为null。例如,以下可能是使用different编码的密码列表id。所有原始密码均为“密码”。

你可能感兴趣的:(SpringBoot,spring,security/Apache,Shiro)