Spring Security 为基于 Spring 的应用程序提供安全保护,是一个功能强大且可高度自定义的自份验证和访问控制框架。
应用程序安全性的两个主要方面是身份验证
(认证:Authentication)和授权
(访问控制:Authorization), 这也是 Spring Security 目标的两个主要领域。
**身份验证:**认证,即确认用户可以访问系统,可理解为用户账号密码正确且有效。授权 :访问控制,即用户在当前系统下所拥有的功能权限。
Spring Boot 关于 Spring Security 官方说明Security , Spring Security 官方文档 -> learn , Spring Boot 集成 Spring Security 官方说明 , Spring Security -> Samples and Guides (Start Here)
Spring Security Spring Security 是通过过滤器来实现所有安全的功能。 Spring Security 提供了 AbstractSecurityWebApplicationInitializer 抽象类,实现了 WebApplicationInitializer 接口, 重写了 onStartup(ServletContext servletContext) 方法, 在方法里调用了 insertSpringSecurityFilterChain(servletContext)方法, 将 springSecurityFilterChain 过滤器注册到 Servlet 容器。
源码分析
springSecurityFilterChain 过滤器, 在所有其它过滤器之前执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 @Configuration public class WebSecurityConfiguration implements ImportAware , BeanClassLoaderAware { private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers; @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public Filter springSecurityFilterChain () throws Exception { boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor .postProcess(new WebSecurityConfigurerAdapter () { }); webSecurity.apply(adapter); } return webSecurity.build(); } @Autowired(required = false) public void setFilterChainProxySecurityConfigurer ( ObjectPostProcessor<Object> objectPostProcessor, //获取所有 WebSecurityConfigurer 接口类类型的 Bean(包括自定义的WebSecurityConfig配置) @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers) throws Exception { webSecurity = objectPostProcessor .postProcess(new WebSecurity (objectPostProcessor)); if (debugEnabled != null ) { webSecurity.debug(debugEnabled); } Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null ; Object previousConfig = null ; for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) { Integer order = AnnotationAwareOrderComparator.lookupOrder(config); if (previousOrder != null && previousOrder.equals(order)) { throw new IllegalStateException ( "@Order on WebSecurityConfigurers must be unique. Order of " + order + " was already used on " + previousConfig + ", so it cannot be used on " + config + " too." ); } previousOrder = order; previousConfig = config; } for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) { webSecurity.apply(webSecurityConfigurer); } this .webSecurityConfigurers = webSecurityConfigurers; } }
AbstractSecurityWebApplicationInitializer 抽像类 继承了 WebApplicationInitializer 接口,重写了 onStartup()方法; 该类由 SpringServletContainerInitializer 类通过 Servlet 的 @HandlesTypes注解自动扫描, SpringServletContainerInitializer 实现了 ServletContainerInitializer 接口, Servlet(Tomcat)容器启动时会调用其onStartup()操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public abstract class AbstractSecurityWebApplicationInitializer implements WebApplicationInitializer { public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain" ; public final void onStartup (ServletContext servletContext) throws ServletException { beforeSpringSecurityFilterChain(servletContext); if (this .configurationClasses != null ) { AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext (); rootAppContext.register(this .configurationClasses); servletContext.addListener(new ContextLoaderListener (rootAppContext)); } if (enableHttpSessionEventPublisher()) { servletContext.addListener( "org.springframework.security.web.session.HttpSessionEventPublisher" ); } servletContext.setSessionTrackingModes(getSessionTrackingModes()); insertSpringSecurityFilterChain(servletContext); afterSpringSecurityFilterChain(servletContext); } private void insertSpringSecurityFilterChain (ServletContext servletContext) { String filterName = DEFAULT_FILTER_NAME; DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy ( filterName); String contextAttribute = getWebApplicationContextAttribute(); if (contextAttribute != null ) { springSecurityFilterChain.setContextAttribute(contextAttribute); } registerFilter(servletContext, true , filterName, springSecurityFilterChain); } private final void registerFilter (ServletContext servletContext, boolean insertBeforeOtherFilters, String filterName, Filter filter) { Dynamic registration = servletContext.addFilter(filterName, filter); if (registration == null ) { throw new IllegalStateException ( "Duplicate Filter registration for '" + filterName + "'. Check to ensure the Filter is only configured once." ); } registration.setAsyncSupported(isAsyncSecuritySupported()); EnumSet<DispatcherType> dispatcherTypes = getSecurityDispatcherTypes(); registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters, "/*" ); } }
集成使用
自定义初始化类,继承 AbstractSecurityWebApplicationInitializer 抽像类, 开启。
1 2 3 4 5 6 7 8 9 import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;public class WebAppInitializer extends AbstractSecurityWebApplicationInitializer {}
自定义 WebSecurityConfig 类, 添加 @EnableWebSecurity
注解, 继承 WebSecurityConfigurerAdapter 抽像类,重写里面的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity httpSecurity) throws Exception { super .configure(httpSecurity); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { super .configure(auth); } @Override public void configure (WebSecurity webSecurity) throws Exception { super .configure(webSecurity); } }
Spring Boot 集成 自动配置 Spring Boot 为 Spring Security 提供了自动配置, 在 org.springframework.boot.autoconfigure.security 包下。 Spring Security 自动配置的核心类有 SecurityProperties(属性类), SecurityAutoConfiguration(Security自动配置类), SecurityFilterAutoConfiguration(过滤器自动配置类)。
SecurityProperties 获取在 application.properties 自定义的安全属性, 自定义属性前缀是 spring.security 。
在该文件中有个静态内部类(User
), 里面定义了默认的用户名是user
, 默认的密码是UUID.randomUUID().toString()
, 如果集成了 Spring Security 但又没有自定义登录页面 和用户认证 , 则会输出默认的登录页面, 并使用此默认用户名和密码进行登录认证, 密码会随项目启动输出打印。
默认的登录页面是由 DefaultLoginPageGeneratingFilter 过滤器处理,判断是否有自定义登录页面的URL路径, 不存在时则在该过滤器里拼接了 HTML 代码来输出页面。
SecurityAutoConfiguration 导入了 SpringBootWebSecurityConfiguration 类, 对于 SpringBootWebSecurityConfiguration 的使用,在 Spring Boot v1.5.x 版本和 2.0.x版本上存在较大差异, 详情请对比两个版本的源码。
SpringBootWebSecurityConfiguration 注入了 WebSecurityConfigurerAdapter 抽象类;自定义的安全配置继承 WebSecurityConfigurerAdapter 类。
SecurityFilterAutoConfiguration 创建 springSecurityFilterChain 实例,注册到 Spring 容器中。
Spring Security 默认提交用户认证的路径是/login
, 请求方式默认也只能是POST
; 退出登录路径是/logout
; 提交认证的用户名属性是username
, 密码属性是password
。用户认证是由 UsernamePasswordAuthenticationFilter 过滤器处理。
Spring Security 的用户认证是通过14个条件过滤器 组成的过滤器链 来实现的, 14个过滤器是(WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter(退出登录过滤器), UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter(登录用户认证过滤器), BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter, ExceptionTranslationFilter, FilterSecurityInterceptor)
封装用户认证的对象 authentication 的数据格式如下,可通过 SecurityContextHolder.getContext().getAuthentication();
来获取该对像实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 { "authenticated" : true , "authorities" : [ { "authority" : "ADMIN" } ] , "details" : { "remoteAddress" : "0:0:0:0:0:0:0:1" , "sessionId" : "19CACF37586038F3DE14F808A891D483" } , "name" : "admin" , "principal" : { "accountNonExpired" : true , "accountNonLocked" : true , "address" : "中国" , "age" : 21 , "authorities" : [ { "authority" : "ADMIN" } ] , "credentialsNonExpired" : true , "enabled" : true , "id" : 3 , "password" : "{bcrypt}$2a$10$/.4eK1JTNF9h6jBzPh94ROgdgsj6KBVNAmg3I7pNBx1wWbckq97jG" , "role" : "ADMIN" , "state" : true , "username" : "admin" } }
默认提供一个基于 HTTP Basic 认证的安全防护策略,提供了默认的用户名和密码,也可通过以下属性设置:
1 2 3 spring.security.user.name =admin spring.security.user.password =123456
默认启用了一些必要的 Web 安全策略,比如针对 XSS、CSRF 等常见针对 Web 的攻击,同时,也会将一些常见的静态资源路径排除在安全防护之外。
JSP标签库 Spring Security 还提供了支持 JSP 的标签库,Spring Security -> JSP Tag Libraries
导入标签库包 1 2 3 4 5 <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-taglibs</artifactId > </dependency >
标签库使用 在 JSP 文件顶部添加 taglib 支持:<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <div > <h1 > 欢迎来到首页: <%--从authentication中取值--%> <sec:authentication property ="principal.username" /> <%--从model中取值--%> ${username} </h1 > <a href ="/admin" > 管理页面</a > <br > <a href ="/user" > USER页面</a > <br > <a href ="/logout" > 退出登录</a > <sec:authorize url ="/admin" > <p > 有权向 /admin 路径发送请求才可显示</p > </sec:authorize > </div > <div > <h1 > USER, ADMIN 角色页面</h1 > <sec:authorize access ="hasAuthority('ADMIN')" > <p > 只有 ADMIN 角色可看</p > </sec:authorize > <sec:authorize access ="hasAuthority('USER')" > <p > 只有 USER 角色可看</p > </sec:authorize > </div >
集成示例
导入 Spring Security 依赖, 此示例使用 Spring Boot 2.0.2 Release版本
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency >
WebConfig:Web配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class WebConfig implements WebMvcConfigurer { public void addViewControllers (ViewControllerRegistry registry) { registry.addViewController("/login" ).setViewName("login" ); registry.addViewController("/admin" ).setViewName("admin" ); registry.addViewController("/user" ).setViewName("user" ); registry.addViewController("/error" ).setViewName("error" ); registry.addViewController("/404" ).setViewName("404" ); registry.addRedirectViewController("/" ,"/index" ); } }
WebSecurityConfig:Web安全配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Override protected void configure (HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf().disable() .authorizeRequests() .antMatchers("/admin/**" ).hasAuthority("ADMIN" ) .antMatchers("/user/**" ).hasAnyAuthority("ADMIN" , "USER" ) .antMatchers("/" , "/login" ).permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login" ) .defaultSuccessUrl("/index" ) .failureUrl("/error" ) .permitAll() .and() .rememberMe() .tokenValiditySeconds(604800 ) .key("myKey" ) .and() .logout() .logoutUrl("/logout" ) .logoutSuccessUrl("/login" ) .permitAll() .and() .httpBasic(); } @Bean public CustomUserDetailsService customUserDetailsService () { return new CustomUserServiceImpl (); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(customUserDetailsService()); } @Override public void configure (WebSecurity webSecurity) throws Exception { webSecurity.ignoring().antMatchers("/resources/static/**" ); } }
SysUser:用户实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Entity @Table(name = "user") public class SysUser implements UserDetails { private static final long serialVersionUID = -1L ; @Id @GeneratedValue private Long id; private String username; private String password; private String role; private String address; private Integer age; private Boolean state; @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return true ; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return true ; } @Override public Collection<? extends GrantedAuthority > getAuthorities() { List<GrantedAuthority> authorityList = new ArrayList <>(); authorityList.add(new SimpleGrantedAuthority (getRole())); return authorityList; } }
控制器:IndexController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Controller public class IndexController { @RequestMapping("/index") public String indexPage (Model model) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); logger.info("authentication:{}" , JSON.toJSONString(authentication)); SysUser sysUser = (SysUser) authentication.getPrincipal(); model.addAttribute("username" , sysUser.getUsername()); return "index" ; } }
用户业务层接口:CustomUserDetailsService
1 2 3 4 5 6 7 8 9 import org.springframework.security.core.userdetails.UserDetailsService;public interface CustomUserDetailsService extends UserDetailsService {}
用户业务实现:CustomUserServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class CustomUserServiceImpl implements CustomUserDetailsService { @Autowired private SysUserRepository sysUserRepository; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { Example<SysUser> example = Example.of(new SysUser ().setUsername(username)); SysUser sysUser = sysUserRepository.findOne(example).get(); return sysUser; } }
数据访问层:SysUserRepository
1 2 3 @Repository public interface SysUserRepository extends JpaRepository <SysUser, Long> {}
登录页面:login
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html > <html > <title > 登录页面</title > <head > </head > <body onload ='document.f.username.focus();' > <h3 > 欢迎登录</h3 > <form name ='f' action ='/login' method ='POST' > <table > <tr > <td > User:</td > <td > <input type ='text' name ='username' value ='' > </td > </tr > <tr > <td > Password:</td > <td > <input type ='password' name ='password' /> </td > </tr > <tr > <td colspan ='2' > <input name ="submit" type ="submit" value ="Login" /> </td > </tr > <%--<input name ="_csrf" type ="hidden" value ="a029a020-6e8f-4bce-9155-23545f36275d" /> --%> </table > </form > </body > </html >
首页
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page isELIgnored="false"%> <!DOCTYPE html > <html > <title > 首页</title > <head > </head > <body > <div > <h1 > 欢迎来到首页: <sec:authentication property ="principal.username" /> ---- ${username} </h1 > <a href ="/admin" > 管理页面</a > <br > <a href ="/user" > USER页面</a > <br > <a href ="/logout" > 退出登录</a > <sec:authorize url ="/admin" > <p > 有权向 /admin 路径发送请求才可显示</p > </sec:authorize > </div > </body > </html >
管理员角色页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page isELIgnored="false"%> <!DOCTYPE html > <html > <title > 首页</title > <head > </head > <body > <div > <h1 > ADMIN 角色页面</h1 > </div > </body > </html >
普通用户角色页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> <%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page isELIgnored="false"%> <!DOCTYPE html > <html > <title > 首页</title > <head > </head > <body > <div > <h1 > USER, ADMIN 角色页面</h1 > <sec:authorize access ="hasAuthority('ADMIN')" > <p > 只有 ADMIN 角色可看</p > </sec:authorize > <sec:authorize access ="hasAuthority('USER')" > <p > 只有 USER 角色可看</p > </sec:authorize > </div > </body > </html >
错误页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page isELIgnored="false"%> <!DOCTYPE html > <html > <title > ERROR</title > <head > </head > <body > <div > <h1 > 不好意思,出错了!!!</h1 > </div > </body > </html >
404页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page isELIgnored="false"%> <!DOCTYPE html > <html > <title > ERROR</title > <head > </head > <body > <div > <h1 > 不好意思,出错了!!!</h1 > </div > </body > </html >
拒绝访问错误页
Java 配置
1 2 3 4 5 6 7 8 9 10 @Override protected void configure (HttpSecurity httpSecurity) throws Exception { http.authorizeRequests() .antMatchers("/admin/*" ) .hasAnyRole("ROLE_ADMIN" ) ... .and() .exceptionHandling() .accessDeniedPage("/my-error-page" ); }
自定义AccessDeniedHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException, ServletException { response.sendRedirect("/my-error-page" ); } } @Autowired private CustomAccessDeniedHandler accessDeniedHandler;@Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*" ) .hasAnyRole("ROLE_ADMIN" ) ... .and() .exceptionHandling() .accessDeniedHandler(accessDeniedHandler); }
自定义异常响应消息
1 2 3 4 5 6 7 8 9 10 11 @ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({AccessDeniedException.class}) public ResponseEntity<Object> handleAccessDeniedException (Exception ex, WebRequest request) { return new ResponseEntity <Object>("Access denied message here" , new HttpHeaders (), HttpStatus.FORBIDDEN); } ... }
源码 -> GitHub