简介
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相关的WebSecurity、HttpSecurity。
# AuthenticationManagerBuilder
AuthenticationManagerBuilder用来配置全局的认证相关的信息,其实就是AuthenticationProvider和UserDetailsService,前者是认证服务提供商,后者是用户详情查询服务。
# 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、