Spring Security使用

简介
Spring Security是一个安全框架,前身是 Acegi Security ,能够为 Spring企业应用系统提供声明式的安全访问控制。Spring Security基于Servlet 过滤器、IoC和AOP,为 Web 请求和方法调用提供身份确认和授权处理,避免了代码耦合,减少了大量重复代码工作。
Spring Security提供了若干个可扩展的、可声明式使用的过滤器处理拦截的web请求
在web请求处理时, Spring Security框架根据请求url和声明式配置,筛选出合适的一组过滤器集合拦截处理当前的web请求。这些请求会被转给Spring Security的安全访问控制框架处理通过之后,请求再转发应用程序处理,从而增强了应用的安全性。
Spring Security 提供了可扩展的认证、鉴权机制对Web请求进行相应对处理。
认证:识别并构建用户对象,如:根据请求中的username,获取登录用户的详细信息,判断用户状态,缓存用户对象到请求上下文等。
决策:判断用户能否访问当前请求,如:识别请求url,根据用户、权限和资源(url)的对应关系,判断用户能否访问当前请求url。
想要对对Web资源进行保护,最好的办法莫过于过滤请求,要想对方法调用进行保护,最好的办法莫过于AOP。Spring Security在我们进行用户认证以及授予权限的时候,是通过各种过滤器来控制访问的,
常见过滤器:
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
LogoutFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
UsernamePasswordAuthenticationFilter
BasicAuthenticationFilter
Spring Security框架的核心组件
SecurityContextHolder:提供对SecurityContext的访问
SecurityContext,:持有Authentication对象和其他可能需要的信息
AuthenticationManager 其中可以包含多个AuthenticationProvider
ProviderManager对象为AuthenticationManager接口的实现类
AuthenticationProvider 主要用来进行认证操作的类 调用其中的authenticate()方法去进行认证操作
Authentication:Spring Security方式的认证主体
GrantedAuthority:对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
UserDetails:构建Authentication对象必须的信息,可以自定义,可能需要访问DB得到
UserDetailsService:通过username构建UserDetails对象,通过loadUserByUsername根据userName获取UserDetail对象 (可以在这里基于自身业务进行自定义的实现  如通过数据库,xml,缓存获取等)  

 

1、创建一个Spring Boot项目

只需要额外加上这个依赖就可以出发Spring Security使用,自动配置的原理可以参考这里

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
package com.funtl.hello.spring.boot.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping(value = "/")
    public String hello(){
        return "hello";
    }
}

浏览器地址栏输入:http://localhost:8080/

当Spring Boot项目引入Spring Security依赖时会默认开启配置:

security:
  basic:
    enabled: true

这个配置开启了一个HTTP Basic类型的认证,所有服务的访问都必须先过这个认证,默认的用户名为user,密码由Sping Security自动生成,回到IDE的控制台,可以找到密码信息:

输入用户名user,密码220999be-c7eb-4e1f-b064-8ca496efbdc7后,我们便可以成功访问/接口。

2、表单认证

我们可以通过自定义一些配置将HTTP Basic认证修改为基于表单的认证方式:

package com.funtl.hello.spring.boot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 指定了认证方式为表单登录,httpBasic()是默认的方式
        http.formLogin()
                .and()
                // 授权配置
                .authorizeRequests()
                // 所有请求
                .anyRequest()
                // 都需要认证
                .authenticated();
    }

}

浏览器地址栏输入:http://localhost:8080/

可以看到表单登陆的页面。

BASIC是利用HTTP头部进行认证,访问页面时会由浏览器弹框要求密码,这个是走HTTP协议层面的认证;FORM是基于页面,你需要自己实现一个登录页面,里面要有一个登录表单,表单的action和用户名 密码字段名都是框架定死的,然后你需要再实现一个servlet来处理这个表单的action,实现登录,实际上走的是session/cookie认证。

3、基本原理

上面的示例展示了一个Spring Security最简单的配置,其经过的流程可以大致如下:

如上图所示,Spring Security包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。其中UsernamePasswordAuthenticationFilter过滤器用于处理基于表单方式的登录认证,而BasicAuthenticationFilter用于处理基于HTTP Basic方式的登录验证,后面还可能包含一系列别的过滤器(可以通过相应配置开启)。在过滤器链的末尾是一个名为FilterSecurityInterceptor的拦截器,用于判断当前请求身份认证是否成功,是否有相应的权限,当身份认证失败或者权限不足的时候便会抛出相应的异常。ExceptionTranslateFilter捕获并处理,所以我们在ExceptionTranslateFilter过滤器用于处理了FilterSecurityInterceptor抛出的异常并进行处理,比如需要身份认证时将请求重定向到相应的认证页面,当认证失败或者权限不足时返回相应的提示信息。

4、自定义认证+权限

生产环境下通常是需要我们自定义认证逻辑的,此时我们需要这样做:

package com.funtl.hello.spring.boot.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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.crypto.password.PasswordEncoder;

@Configuration
public class BrowserUserDetailService implements UserDetailsService {

    /**
     * 需要自己注入
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String password = passwordEncoder.encode("123456");
        return new User(username, password, true,
                true, true,
                true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

注意到这里使用了@Configuration注解就能被Spring Security找到并调用,如果只写一个@Service注解,就需要手动注册进去,通过继承WebSecurityConfigurerAdapter重写configureGlobal()方法注册进去。

package com.funtl.hello.spring.boot.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 指定了认证方式为表单登录,httpBasic()是默认的方式
        http.formLogin()
                .and()
                // 授权配置
                .authorizeRequests()
                // 所有请求
                .anyRequest()
                // 都需要认证
                .authenticated();
    }

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

    }
}

这里解释下org.springframework.security.core.userdetails.User这个类的成员含义:
authorities:获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象;
password:密码
usename:用户名
accountNonExpired:方法返回boolean类型,用于判断账户是否未过期,未过期返回true反之返回false;
accountNonLocked:方法用于判断账户是否未锁定;
credentialsNonExpired:用于判断用户凭证是否没过期,即密码是否未过期;
enabled:方法用于判断用户是否可用。

另外,PasswordEncoder是一个密码加密接口,而BCryptPasswordEncoder是Spring Security提供的一个实现方法,我们也可以自己实现PasswordEncoder。不过Spring Security实现的BCryptPasswordEncoder已经足够强大,它对相同的密码进行加密后可以生成不同的结果。

5、自定义登陆页

在src/main/resources/resources目录下定义一个login.html

然后修改:

package com.funtl.hello.spring.boot.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 指定了认证方式为表单登录,httpBasic()是默认的方式
        http.formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .and()
                // 授权配置
                .authorizeRequests()
                .antMatchers("/login.html").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要认证
                .authenticated().
                and().csrf().disable();
    }

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

    }
}

这样就可以访问到我们自定义的页面。必须关掉CSRF功能才嫩正确登陆。其中的关键点有2,1是Spring Boot默认静态资源路径的规则,2是Spring Security关于CSRF的规则。上面代码中.loginPage("/login.html")指定了跳转到登录页面的请求URL,.loginProcessingUrl("/login")对应登录页面form表单的action="/login",.antMatchers("/login.html").permitAll()表示跳转到登录页面的请求不被拦截,否则会进入无限循环。

6、认证通过和失败处理

Spring Security有一套默认的处理认证成功和失败的方法:当用户认证成功时,页面会跳转引发请求,比如在未登录的情况下访问http://localhost:8080/hello,页面会跳转到登录页,登录成功后再跳转回来;登录失败时则是跳转到SpringSecurity默认的错误提示页面。下面我们通过一些自定义配置来替换这套默认的处理机制。

package com.funtl.hello.spring.boot.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    
    @Autowired
    private ObjectMapper mapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
    }
}
package com.funtl.hello.spring.boot.config;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(authentication.toString());
    }
}
package com.funtl.hello.spring.boot.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyAuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 指定了认证方式为表单登录,httpBasic()是默认的方式
        http.formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .and()
                // 授权配置
                .authorizeRequests()
                .antMatchers("/login.html").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要认证
                .authenticated().
                and().csrf().disable();
    }

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

    }
}

7、配置思路

由于软件开发中,要解决的安全的问题非常多且零碎,导致了Spring Security在配置项也很多,对于接触不久的人来说,可能本身安全方面的东西平时“工作生活”中就接触比较少,导致在学习Spring Security的过程中,有种剪不断理还乱的感觉。下面我们就通过Spring Security的Config模块的架构,来理清这个关系。Spring Security Config模块一共有3个builder,认证相关的AuthenticationManagerBuilder和web相关的WebSecurityHttpSecurity。

# AuthenticationManagerBuilder
AuthenticationManagerBuilder用来配置全局的认证相关的信息,其实就是AuthenticationProviderUserDetailsService,前者是认证服务提供商,后者是用户详情查询服务。
# WebSecurity
全局请求忽略规则配置(比如说静态文件,比如说注册页面)、全局HttpFirewall配置、是否debug配置、全局SecurityFilterChain配置、privilegeEvaluator、expressionHandler、securityInterceptor、
# HttpSecurity
具体的权限控制规则配置。一个这个配置相当于xml配置中的一个标签。
各种具体的认证机制的相关配置,OpenIDLoginConfigurer、AnonymousConfigurer、FormLoginConfigurer、HttpBasicConfigurer
LogoutConfigurer
RequestMatcherConfigurer:spring mvc style、ant style、regex style
HeadersConfigurer:
CorsConfigurer、CsrfConfigurer
SessionManagementConfigurer:
PortMapperConfigurer:
JeeConfigurer:
X509Configurer:
RememberMeConfigurer:
ExpressionUrlAuthorizationConfigurer:
RequestCacheConfigurer:
ExceptionHandlingConfigurer:
SecurityContextConfigurer:
ServletApiConfigurer:
ChannelSecurityConfigurer:
此模块的authenticationProvider和userDetailsService;
SecurityFilterChain控制

WebSecurityConfigurerAdapter
spring security为web应用提供了一个WebSecurityConfigurerAdapter适配器,应用里spring security相关的配置可以通过继承这个类来编写;具体是提供了上边三个顶级配置项构建器的构建重载回调方法:

protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}
 
public void configure(WebSecurity web) throws Exception {
}
 
protected void configure(HttpSecurity httpSecurity) throws Exception {
}

具体配置思路:

1、httpSecurity.authorizeRequests()返回一个ExpressionInterceptUrlRegistry对象,这个对象就一个作用,注册intercept url规则权限匹配信息,通过设置URL Matcher,antMatchers,mvcMatchers,regexMatchers或者直接设置一个一个或者多个RequestMatcher对象;

2、上边设置matchers的方法会返回一个AuthorizedUrl对象,用于接着设置符合其规则的URL的权限信息,AuthorizedUrl对象提供了access方法用于设置一个权限表达式,比如说字符串“hasRole(‘ADMIN’) and hasRole(‘DBA’)”,同时提供了多个方便的语义方法,比如说:

public ExpressionInterceptUrlRegistry hasRole(String role) 
public ExpressionInterceptUrlRegistry hasAuthority(String authority)

这些方法返回值是ExpressionInterceptUrlRegistry,用于接着设置下一条过滤规则:

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/resources/**", "/signup", "/about").permitAll()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
            .anyRequest().authenticated()
            .and()
        // ...
        .formLogin();
}

上边1和2结合起来的功能相当于标签的功能;
UrlAuthorizationConfigurer能实现上边类似的功能; 

protected void configure(HttpSecurity http) throws Exception {
    http.apply(new UrlAuthorizationConfigurer<HttpSecurity>()).getRegistry()
            .antMatchers("/users**", "/sessions/**").hasRole("USER")
            .antMatchers("/signup").hasRole("ANONYMOUS").anyRequest().hasRole("USER");
}

 formLogin和logout
    FormLoginConfigurer
    OpenIDLoginConfigurer
    HttpBasicConfigurer
    LogoutConfigurer

8、AuthenticationEntryPoint和AccessDeniedException

以上接口和类是SpringSecurity默认自带的异常处理机制,我们可以继承以自定义异常处理机制
1. AccessDeniedException
该异常有很多子类。子类都是涉及到权限校验问题的。
2. AuthenticationEntryPoint
同样该异常类也有很多子类。SpringSecurity把异常划分的很细。概括来说都是身份校验问题。接下来说说Spring Security是怎么分辨异常类型的,主要是ExceptionTranslationFilter这个filter,下面是它的核心代码

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
		throws IOException, ServletException {
	HttpServletRequest request = (HttpServletRequest) req;
	HttpServletResponse response = (HttpServletResponse) res;

	try {
		chain.doFilter(request, response);

		logger.debug("Chain processed normally");
	}
	catch (IOException ex) {
		throw ex;
	}
	catch (Exception ex) {
                // 不管是AccessDeniedException还是AuthenticationEntryPoint都在这里被捕获
		// Try to extract a SpringSecurityException from the stacktrace
		Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
		RuntimeException ase = (AuthenticationException) throwableAnalyzer
				.getFirstThrowableOfType(AuthenticationException.class, causeChain);

		if (ase == null) {
			ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
					AccessDeniedException.class, causeChain);
		}

		if (ase != null) {
			if (response.isCommitted()) {
				throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
			}
			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 {
// 认证异常
	if (exception instanceof AuthenticationException) {
		logger.debug(
				"Authentication exception occurred; redirecting to authentication entry point",
				exception);

		sendStartAuthentication(request, response, chain,
				(AuthenticationException) exception);
	}
	else if (exception instanceof AccessDeniedException) {
// 权限异常
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
			logger.debug(
					"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
					exception);

			sendStartAuthentication(
					request,
					response,
					chain,
					new InsufficientAuthenticationException(
						messages.getMessage(
							"ExceptionTranslationFilter.insufficientAuthentication",
							"Full authentication is required to access this resource")));
		}
		else {
			logger.debug(
					"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
					exception);

			accessDeniedHandler.handle(request, response,
					(AccessDeniedException) exception);
		}
	}
}

实际使用方式:

// 对应上面认证异常
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.getWriter().println(e.getMessage());
    }
}

// 对应上面权限异常
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.getWriter().println(e.getMessage());
    }
}

注册进去:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors()
            .and()
                .csrf().disable()
            .authorizeRequests()
                .antMatchers("/user/sign").permitAll().anyRequest().authenticated()
            .and()
                .addFilter(new JWTLoginFilter(authenticationManager()))
                .addFilter(new JwtAuthenticationFilter(authenticationManager()));
                //添加自定义异常入口,处理accessdeine异常
        http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
        .accessDeniedHandler(myAccessDeniedHandler);       
    }
    ...
}

9、

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值