问题:
Spring Security是干嘛的?
Spring Security如何配置?
Spring Security如何设置需要被保护的URL?
Spring Security如何设置获取用户角色信息的策略?
Spring Security如何自定义登录页面?
Spring Security如何自定义退出页面?
Spring Security如何设置权限验证失败后的处理?
Spring Security是干嘛的? Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
我的理解是,我们在需要验证用户权限时,从零开始的做法是我们从cookie/session中提取用户信息,如果没有,那么提示错误或者让用户去登录,如果用户已经登录,我们就通过用户id去数据库查询用户的角色/权限信息,然后判断是否可以执行。而Spring Security就是提取了这些操作共性加高度可配置的安全中间件。使用Spring Security我们不需要完全手写这些功能,只需要配置策略即可。对于需要定制的地方,通过自定义扩展配置搞定。
Spring Security如何配置? 参考Spring笔记-Spring配置 | 木杉的博客 ,引入Spring Security,我们需要添加新的WebApplicationInitializer
实现,Spring Security提供了AbstractSecurityWebApplicationInitializer
抽象类,我们直接继承即可。这个Initializer中初始化了Spring Security一些相关配置。
配置的结构图如下:
配置代码:
1 2 3 4 5 6 7 8 9 public class MyWebApplicationInitializer implements WebApplicationInitializer { public void onStartup (ServletContext servletContext) throws ServletException { ServletRegistration.Dynamic registration = servletContext.addServlet("test" , new DispatcherServlet()); registration.setLoadOnStartup(1 ); registration.addMapping("/*" ); registration.setInitParameter("contextClass" , "org.springframework.web.context.support.AnnotationConfigWebApplicationContext" ); registration.setInitParameter("contextConfigLocation" , "WebSecurityConfig" ); } }
1 2 public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {}
1 2 3 4 5 6 7 8 @Configuration public class AppConfig { @Bean public TestServlet testServlet () { return new TestServlet(); } }
1 2 3 4 5 6 7 8 9 10 11 @EnableWebSecurity @Import (AppConfig.class)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public UserDetailsService userDetailsService () { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("user" ).password("password" ).roles("USER" ).build()); return manager; } }
这个Security配置只配置了用户获取策略。但是他做了很多的事情:
使你的应用的所有URL都需要授权才可访问
为你生成了一个登陆表单
允许使用user/password这个账号来通过based authentication进行授权
允许用户登出
防止CSRF攻击
防止Session Fixation
整合安全相关Header
结合Servlet API方法
Spring Security如何设置需要被保护的URL? 上面的简单配置,默认会让所有的URL都需要授权。那我们如何自定义呢?
WebSecurityConfigurerAdapter
中指定了默认配置:
1 2 3 4 5 6 7 8 9 10 protected void configure (HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)." ); http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); }
其中anyRequest()
指定了所有的URL都需要授权。同时formLogin()
指定生成默认登录页面,httpBasic()
指定用based authentication来进行授权登录。
我们可以覆盖这个方法来指定我们需要的配置。
1 2 3 4 5 6 @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/test/**" ).authenticated() .and().formLogin() .and().httpBasic(); }
这里我们指定/test/**
下的URL才需要授权。这里使用ant风格的url匹配,你也可以使用regexMatchers()
方法来使用正则风格的匹配模式。
Spring Security如何设置获取用户角色信息的策略? 上面的例子中,我们使用最简单的方式从内存中获取用户信息,这个用于演示还可以,但是实际中应该没有应用会这么做。Spring Security支持从多种地方获取用户信息,比如内存,数据库,LDAP等。
大部分场景我们从数据库中读取用户信息,看下例子:
1 2 3 4 5 6 7 8 9 10 11 12 @Autowired private DataSource dataSource;@Autowired public void configureGlobal (AuthenticationManagerBuilder auth) throws Exception { auth .jdbcAuthentication() .dataSource(dataSource) .withDefaultSchema() .withUser("user" ).password("password" ).roles("USER" ).and() .withUser("admin" ).password("password" ).roles("USER" , "ADMIN" ); }
Spring Security如何自定义登录页面? 1 2 3 4 5 6 7 8 9 protected void configure (HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login" ) .permitAll(); }
formLogin()
指定登录配置,使用loginPage("/login")
指定登录页面URL,注意对于登录页面一定要设置permitAll()
否则未登录的用户就进不了登录页面了。
自定义的登录页面可以是这样的:
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 <c:url value ="/login" var ="loginUrl" /> <form action ="${loginUrl}" method ="post" > 1 <c:if test ="${param.error != null}" > 2 <p > Invalid username and password. </p > </c:if > <c:if test ="${param.logout != null}" > 3 <p > You have been logged out. </p > </c:if > <p > <label for ="username" > Username</label > <input type ="text" id ="username" name ="username" /> 4 </p > <p > <label for ="password" > Password</label > <input type ="password" id ="password" name ="password" /> 5 </p > <input type ="hidden" 6 name ="${_csrf.parameterName}" value ="${_csrf.token}" /> <button type ="submit" class ="btn" > Log in</button > </form >
Spring Security如何自定义退出页面? 1 2 3 4 5 6 7 8 9 10 11 12 protected void configure (HttpSecurity http) throws Exception { http .logout() .logoutUrl("/my/logout" ) .logoutSuccessUrl("/my/index" ) .logoutSuccessHandler(logoutSuccessHandler) .invalidateHttpSession(true ) .addLogoutHandler(logoutHandler) .deleteCookies(cookieNamesToClear) .and() ... }
logoutUrl("/my/logout")
指定登出的URL
logoutSuccessUrl("/my/index")
指定登出成功时转到的URL
logoutSuccessHandler(logoutSuccessHandler)
指定登出成功时,触发的处理器
invalidateHttpSession(true)
指定登出时是否清理session
Spring Security如何设置权限验证失败后的处理? 验证权限失败有两种场景:
对于第一种情况,Spring Security默认会跳转到登录页面,如果没有登录页面,则会抛出403页面。我们可以指定AuthenticationEntryPoint
,也就是登录入口。比如对于RESTful应用,用户如果没有登录,是不会跳转到登录页面的,而是直接提示未授权。对于这种场景,我们可以自定义登录入口,把登录入口设置为错误提示即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Component ( "restAuthenticationEntryPoint" )public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence ( HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized" ); } }
在Security Config中设置授权入口为自定义的错误提示:
1 2 3 4 5 6 7 8 @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/test/**" ).authenticated() .antMatchers("/admin/**" ).hasRole("ADMIN" ) .and().formLogin() .and().exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint); }
对于“用户登录了但是没有对应的权限”这种情况,默认会返回access deny界面,这个界面也是可以定制的,可以直接指定错误信息页面:
1 .and().exceptionHandling().accessDeniedPage("/deny" );
或者指定一个处理器:
1 2 3 4 .and().exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() { public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { }
参考资料