37.5.2 Resolving the CsrfToken
Spring Security provides CsrfTokenArgumentResolver
which can automatically resolve the current CsrfToken
for Spring MVC arguments. By using @EnableWebSecurity you will automatically have this added to your Spring MVC configuration. If you use XML based configuraiton, you must add this yourself.
Once CsrfTokenArgumentResolver
is properly configured, you can expose the CsrfToken
to your static HTML based application.
@RestController public class CsrfController { @RequestMapping("/csrf") public CsrfToken csrf(CsrfToken token) { return token; } }
It is important to keep the CsrfToken
a secret from other domains. This means if you are using Cross Origin Sharing (CORS), you should NOT expose the CsrfToken
to any external domains.
https://docs.spring.io/spring-security/site/docs/4.2.3.RELEASE/reference/htmlsingle/#mvc-csrf
Include CSRF token in action
If allowing unauthorized users to upload temporariy files is not acceptable, an alternative is to place the MultipartFilter
after the Spring Security filter and include the CSRF as a query parameter in the action attribute of the form. An example with a jsp is shown below
<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
The disadvantage to this approach is that query parameters can be leaked. More genearlly, it is considered best practice to place sensitive data within the body or headers to ensure it is not leaked.
Additional information can be found in RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s.
https://docs.spring.io/spring-security/site/docs/4.2.3.RELEASE/reference/htmlsingle/#csrf-include-csrf-token-in-action
如果启动csrf,Spring Security /login和/logout的form中会自动集成csrf
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
运行时的html:
源代码:
<form name="form" th:action="@{/login}" action="/login" method="POST"> <input type="text" id="username" class="span4" name="username" placeholder="Username"/> <br/> <input type="password" id="password" class="span4" name="password" placeholder="Password"/> <input type="checkbox" id="remember-me" th:class="checkbox" name="remember-me" checked="checked"/> <label for="remember-me" th:class="inline">Remember me</label> <button type="submit" name="submit" class="btn btn-info btn-block">Sign in</button> </form>
<form th:action="@{/logout}" method="post"> <input type="submit" value="Sign Out"/> </form>
解决下面报错的方案:
https://github.com/spring-projects/spring-security/issues/3906
As you suggested, the only thing you can really do to prevent this is to eagerly create the CsrfToken
. In Spring Security 4.1.0.RELEASE+ and Java Configuration you can do this with:
http .csrf() .csrfTokenRepository(new HttpSessionCsrfTokenRepository())
This tells Spring Security that you do not want the token to be lazily persisted. If you are concerned with creating a session, you can also use the cookie implementation. For example:
http .csrf() .csrfTokenRepository(new CookieCsrfTokenRepository())
With Spring Boot, you can override the Spring Security version using the information from Customize dependency versions:
build.gradle
ext['spring-security.version'] = '4.1.0.RELEASE'
pom.xml
<spring-security.version>4.1.0.RELEASE</spring-security.version>
with pages that do not require a login attempt to add a CSRF token, which is "too late" because some of the response has already been committed. Suggested resolution: when using extras-springsecurity, (conditionally?) force generation of the session before starting to write to the buffer. (This is easily achievable using a Filter outside Thymeleaf but is somewhat tricky to identify) Caused by: java.lang.IllegalStateException: Cannot create a session after the response has been committed at org.apache.catalina.connector.Request.doGetSession(Request.java:2886) at org.apache.catalina.connector.Request.getSession(Request.java:2256) at org.apache.catalina.connector.RequestFacade.getSession(RequestFacade.java:895) at org.apache.catalina.connector.RequestFacade.getSession(RequestFacade.java:907) at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:240) at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:240) at org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.saveToken(HttpSessionCsrfTokenRepository.java:63) at org.springframework.security.web.csrf.LazyCsrfTokenRepository$SaveOnAccessCsrfToken.saveTokenIfNecessary(LazyCsrfTokenRepository.java:176) at org.springframework.security.web.csrf.LazyCsrfTokenRepository$SaveOnAccessCsrfToken.getToken(LazyCsrfTokenRepository.java:128) at org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor.getExtraHiddenFields(CsrfRequestDataValueProcessor.java:71) at org.thymeleaf.spring4.requestdata.RequestDataValueProcessor4Delegate.getExtraHiddenFields(RequestDataValueProcessor4Delegate.java:86) at org.thymeleaf.spring4.requestdata.RequestDataValueProcessorUtils.getExtraHiddenFields(RequestDataValueProcessorUtils.java:155) at org.thymeleaf.spring4.processor.SpringActionTagProcessor.doProcess(SpringActionTagProcessor.java:116) at org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor.doProcess(AbstractStandardExpressionAttributeTagProcessor.java:98) at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:74)
To configure the custom login form, create a @Configuration class by extending the WebSecurityConfigurerAdapter class as follows.
import org.springframework.security.authentication.BadCredentialsException; 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.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.GrantedAuthority; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password("admin123").roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().hasAnyRole("USER","ADMIN") .and() .authorizeRequests().antMatchers("/login**").permitAll() .and() .formLogin() .loginPage("/login") // Specifies the login page URL .loginProcessingUrl("/signin") // Specifies the URL,which is used //in action attribute on the <from> tag .usernameParameter("userid") // Username parameter, used in name attribute // of the <input> tag. Default is 'username'. .passwordParameter("passwd") // Password parameter, used in name attribute // of the <input> tag. Default is 'password'. .successHandler((req,res,auth)->{ //Success handler invoked after successful authentication for (GrantedAuthority authority : auth.getAuthorities()) { System.out.println(authority.getAuthority()); } System.out.println(auth.getName()); res.sendRedirect("/"); // Redirect user to index/home page }) // .defaultSuccessUrl("/") // URL, where user will go after authenticating successfully. // Skipped if successHandler() is used. .failureHandler((req,res,exp)->{ // Failure handler invoked after authentication failure String errMsg=""; if(exp.getClass().isAssignableFrom(BadCredentialsException.class)){ errMsg="Invalid username or password."; }else{ errMsg="Unknown error - "+exp.getMessage(); } req.getSession().setAttribute("message", errMsg); res.sendRedirect("/login"); // Redirect user to login page with error message. }) // .failureUrl("/login?error") // URL, where user will go after authentication failure. // Skipped if failureHandler() is used. .permitAll() // Allow access to any URL associate to formLogin() .and() .logout() .logoutUrl("/signout") // Specifies the logout URL, default URL is '/logout' .logoutSuccessHandler((req,res,auth)->{ // Logout handler called after successful logout req.getSession().setAttribute("message", "You are logged out successfully."); res.sendRedirect("/login"); // Redirect user to login page with message. }) // .logoutSuccessUrl("/login") // URL, where user will be redirect after successful // logout. Ignored if logoutSuccessHandler() is used. .permitAll() // Allow access to any URL associate to logout() .and() .csrf().disable(); // Disable CSRF support } }
https://www.boraji.com/spring-security-4-custom-login-from-example
org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer
public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<RememberMeConfigurer<H>, H> { /** * The default name for remember me parameter name and remember me cookie name */ private static final String DEFAULT_REMEMBER_ME_NAME = "remember-me";
使用Spring-security默认的Form登陆页:
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .successForwardUrl("/home") .and() .authorizeRequests().anyRequest().permitAll(); } }
import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HomeController { @RequestMapping("/home") public ResponseEntity<String> home() { return ResponseEntity.ok("success"); } }
访问http://localhost:8080/home会跳转到 http://localhost:8080/login输入用户名和密码,则会跳转到 http://localhost:8080/home
如果在HttpSecurity中配置需要authenticate(),则如果没有登陆,或没有相关权限,则会无法访问
2017-01-02 23:39:32.027 DEBUG 10396 --- [nio-8080-exec-8] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/user'; against '/auth/**' 2017-01-02 23:39:32.028 DEBUG 10396 --- [nio-8080-exec-8] o.s.s.w.a.i.FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /user; Attributes: [authenticated] 2017-01-02 23:39:32.029 DEBUG 10396 --- [nio-8080-exec-8] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6:
Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS 2017-01-02 23:39:32.030 DEBUG 10396 --- [nio-8080-exec-8] o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@292b8b5a, returned: -1 2017-01-02 23:39:32.034 DEBUG 10396 --- [nio-8080-exec-8] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point org.springframework.security.access.AccessDeniedException: Access is denied at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:124) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEAS
配置session管理
下面的配置展示了只允许认证用户在同一时间只有一个实例是如何配置的。若一个用户使用用户名为"user"认证并且没有退出,同一个名为“user”的试图再次认证时,第一个用户的session将会强制销毁,并设置到"/login?expired"的url。
@Configuration @EnableWebSecurity public class SessionManagementSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().hasRole("USER") .and() .formLogin() .permitAll() .and() .sessionManagement() .maximumSessions(1) .expiredUrl("/login?expired"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth. inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
当使用SessionManagementConfigurer的maximumSessio(int)时不用忘记为应用配置HttpSessionEventPublisher,这样能保证过期的session能够被清除。
在web.xml中可以这样配置:
<listener> <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class></listener>
httpSecurity
类似于spring security的xml配置文件命名空间配置中的<http>元素。它允许对特定的http请求基于安全考虑进行配置。默认情况下,适用于所有的请求,但可以使用requestMatcher(RequestMatcher)或者其它相似的方法进行限制。
使用示例:
最基本的基于表单的配置如下。该配置将所有的url访问权限设定为角色名称为"ROLE_USER".同时也定义了内存认证模式:使用用户名"user"和密码“password”,角色"ROLE_USER"来认证。
@Configuration @EnableWebSecurity public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").hasRole("USER") .and() .formLogin(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
配置基于openId的认证方式
basic示例,不使用attribute exchange
@Configuration @EnableWebSecurity public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) { http .authorizeRequests() .antMatchers("/").hasRole("USER") .and() .openidLogin() .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() // the username must match the OpenID of the user you are // logging in with .withUser("https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU") .password("password") .roles("USER"); } }
下面展示一个更高级的示例,使用attribute exchange
@Configuration @EnableWebSecurity public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) { http .authorizeRequests() .antMatchers("/").hasRole("USER") .and() .openidLogin() .loginPage("/login") .permitAll() .authenticationUserDetailsService(new AutoProvisioningUserDetailsService()) .attributeExchange("https://www.google.com/.") .attribute("email") .type("http://axschema.org/contact/email") .required(true) .and() .attribute("firstname") .type("http://axschema.org/namePerson/first") .required(true) .and() .attribute("lastname") .type("http://axschema.org/namePerson/last") .required(true) .and() .and() .attributeExchange(".yahoo.com.") .attribute("email") .type("http://schema.openid.net/contact/email") .required(true) .and() .attribute("fullname") .type("http://axschema.org/namePerson") .required(true) .and() .and() .attributeExchange(".myopenid.com.") .attribute("email") .type("http://schema.openid.net/contact/email") .required(true) .and() .attribute("fullname") .type("http://schema.openid.net/namePerson") .required(true); } } public class AutoProvisioningUserDetailsService implements AuthenticationUserDetailsService<OpenIDAuthenticationToken> { public UserDetails loadUserDetails(OpenIDAuthenticationToken token) throws UsernameNotFoundException { return new User(token.getName(), "NOTUSED", AuthorityUtils.createAuthorityList("ROLE_USER")); } }
增加响应安全报文头
默认情况下当使用WebSecuirtyConfigAdapter的默认构造函数时激活。
仅触发Headers()方法而不触发其它方法或者接受WebSecurityConfigureerAdater默认的,等同于:
@Configuration @EnableWebSecurity public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .headers() .contentTypeOptions(); .xssProtection() .cacheControl() .httpStrictTransportSecurity() .frameOptions() .and() ...; } }
项目中用到iframe嵌入网页,然后用到springsecurity就被拦截了 浏览器报错"Refused to display 'http://......' in a frame because it set 'X-Frame-Options' to 'DENY'. "错误
原因是因为springSecurty使用X-Frame-Options防止网页被Frame
解决办法:
@Override protected void configure(HttpSecurity http) throws Exception { http.headers().frameOptions().disable(); http.csrf().disable(); http.authorizeRequests().anyRequest().authenticated(); http.formLogin() .defaultSuccessUrl("/platform/index", true) .loginPage("/login") .permitAll() .and() .logout(); http.addFilterBefore(ssoFilterSecurityInterceptor(), FilterSecurityInterceptor.class); }
取消安全报文头,如下:
@Configuration @EnableWebSecurity public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .headers().disable() ...; } }
使用部分安全报文头
触发headers()方法的返回结果,例如,只使用HeaderConfigurer的cacheControll()方法和HeadersConfigurer的frameOptions()方法.
@Configuration @EnableWebSecurity public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .headers() .cacheControl() .frameOptions() .and() ...; } }
配置PortMapper
允许配置一个从HttpSecurity的getSharedObject(Class)方法中获取的PortMapper。当http请求跳转到https或者https请求跳转到http请求时(例如我们和requiresChanenl一起使用时),别的提供的SecurityConfigurer对象使用P诶账户的PortMapper作为默认的PortMapper。默认情况下,spring security使用PortMapperImpl来映射http端口8080到https端口8443,并且将http端口的80映射到https的端口443.
配置示例如下,下面的配置将确保在spring security中的http请求端口9090跳转到https端口9443 并且将http端口80跳转到https443端口。
@Configuration @EnableWebSecurity public class PortMapperSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").hasRole("USER") .and() .formLogin() .permitAll() .and() // Example portMapper() configuration .portMapper() .http(9090).mapsTo(9443) .http(80).mapsTo(443); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
配置基于容器的预认证
在这个场景中,servlet容器管理认证。
配置示例:
下面的配置使用HttpServletRequest中的principal,若用户的角色是“ROLE_USER”或者"ROLE_ADMIN",将会返回Authentication结果。
@Configuration @EnableWebSecurity public class JeeSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").hasRole("USER") .and() // Example jee() configuration .jee() .mappableRoles("ROLE_USER", "ROLE_ADMIN"); } }
开发者希望使用基于容器预认证时,需要在web.xml中配置安全限制。例如:
<login-config> <auth-method>FORM</auth-method> <form-login-config> <form-login-page>/login</form-login-page> <form-error-page>/login?error</form-error-page> </form-login-config> </login-config> <security-role> <role-name>ROLE_USER</role-name> </security-role> <security-constraint> <web-resource-collection> <web-resource-name>Public</web-resource-name> <description>Matches unconstrained pages</description> <url-pattern>/login</url-pattern> <url-pattern>/logout</url-pattern> <url-pattern>/resources/</url-pattern> </web-resource-collection> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>Secured Areas</web-resource-name> <url-pattern>/</url-pattern> </web-resource-collection> <auth-constraint> <role-name>ROLE_USER</role-name> </auth-constraint> </security-constraint>
配置基于X509的预认证
配置示例,下面的配置试图从X509证书中提取用户名,注意,为完成这个工作,客户端请求证书需要配置到servlet容器中。
@Configuration @EnableWebSecurity public class X509SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").hasRole("USER") .and() // Example x509() configuration .x509(); } }
配置Remember-me服务
配置示例,下面的配置展示了如何允许基于token的remember-me的认证。若http参数中包含一个名为“remember-me”的参数,不管session是否过期,用户记录将会被记保存下来。
@Configuration @EnableWebSecurity public class RememberMeSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").hasRole("USER") .and() .formLogin() .permitAll() .and() // Example Remember Me Configuration .rememberMe(); } }
限制HttpServletRequest的请求访问
配置示例,最基本的示例是配置所有的url访问都需要角色"ROLE_USER".下面的配置要求每一个url的访问都需要认证,并且授权访问权限给用户"admin"和"user".
@Configuration @EnableWebSecurity public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").hasRole("USER") .and() .formLogin(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER") .and() .withUser("adminr") .password("password") .roles("ADMIN","USER"); } }
同样,也可以配置多个url。下面的配置要求以/admin/开始的url访问权限为“admin”用户。
@Configuration @EnableWebSecurity public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/**").hasRole("USER") .and() .formLogin(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER") .and() .withUser("adminr") .password("password") .roles("ADMIN","USER"); } }
注意:匹配起效是按照顺序来的。因此如果下面的配置是无效的,因为满足第一个规则后将不会检查第二条规则:
http .authorizeRequests() .antMatchers("/**").hasRole("USER") .antMatchers("/admin/**").hasRole("ADMIN")
增加CSRF支持
默认情况下,当使用WebSecurityConfigurerAdapter时的默认构造方法时CSRF是激活的。你可以使用如下方法关闭它:
@Configuration @EnableWebSecurity public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() ...; } }
增加logout支持
默认支持,当使用WebSecurityConfigurerAdapter时Logout是支持的。当用户发出“/logout”请求时,系统将会销毁session并且清空配置的rememberMe()认证,然后清除SecurityContextHolder,最后跳向logout成功页面或者登陆页面。
@Configuration
@EnableWebSecurity
public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").hasRole("USER")
.and()
.formLogin()
.and()
// sample logout customization
.logout()
.deleteCookies("remove")
.invalidateHttpSession(false)
.logoutUrl("/custom-logout")
.logoutSuccessUrl("/logout-success");
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("password")
.roles("USER");
}
}
匿名用户控制
使用WebSecurityConfigurerAdapter时自动绑定。默认情况下,匿名用户有一个AnonymousAuthenticationToken标示,包含角色"ROLE_ANONYMOUS"。
下面的配置展示了如何指定匿名用户应该包含"ROLE_ANON".
@Configuration @EnableWebSecurity public class AnononymousSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").hasRole("USER") .and() .formLogin() .and() // sample anonymous customization .anonymous() .authorities("ROLE_ANON"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
基于表单的认证
若FormLoginConfigurer的loginpage(String)没有指定,将会产生一个默认的login页面。
示例配置:
@Configuration @EnableWebSecurity public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/**").hasRole("USER") .and() .formLogin(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
下面的示例展示了自定义的表单认证:
@Configuration
@EnableWebSecurity
public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").hasRole("USER")
.and()
.formLogin()
.usernameParameter("j_username") // default is username
.passwordParameter("j_password") // default is password
.loginPage("/authentication/login") // default is /login with an HTTP get
.failureUrl("/authentication/login?failed") // default is /login?error
.loginProcessingUrl("/authentication/login/process"); // default is /login with an HTTP post
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("password")
.roles("USER");
}
}
配置安全通道
为使配置生效,需至少配置一个通道的映射。
配置示例:
下面例子展示了如何将每个请求都使用https通道。
@Configuration @EnableWebSecurity public class ChannelSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/**").hasRole("USER") .and() .formLogin() .and() .channelSecurity() .anyRequest().requiresSecure(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
配置http 基本认证
配置示例:
@Configuration @EnableWebSecurity public class HttpBasicSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/**").hasRole("USER").and() .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
配置要触发的HttpRequest
重写RequestMatcher方法、antMatcher()z、regexMatcher()等。
配置示例
下面的配置使HttpSecurity接收以"/api/","/oauth/"开头请求。
@Configuration @EnableWebSecurity public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers("/api/**","/oauth/**") .and() .authorizeRequests() .antMatchers("/**").hasRole("USER").and() .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
下面的配置和上面的相同:
@Configuration @EnableWebSecurity public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers("/api/**") .antMatchers("/oauth/**") .and() .authorizeRequests() .antMatchers("/**").hasRole("USER").and() .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
同样也可以这样使用:
@Configuration @EnableWebSecurity public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers("/api/**") .and() .requestMatchers() .antMatchers("/oauth/**") .and() .authorizeRequests() .antMatchers("/**").hasRole("USER").and() .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER"); } }
小结:
本文是从httpSecurity代码中整理得来的,有助于对spring security的全面理解
http://www.cnblogs.com/davidwang456/p/4549344.html?utm_source=tuicool
前一节学习了如何限制登录尝试次数,今天在这个基础上再增加一点新功能:Remember Me. 很多网站,比如博客园,在登录页面就有这个选项,勾选“下次自动登录”后,在一定时间段内,只要不清空浏览器Cookie,就可以自动登录。
一、spring-security.xml 最简单的配置
1 <http auto-config="true" use-expressions="true"> 2 ... 3 <remember-me /> 4 </http>
即:在<http></http>节点之间,加一行<rember-me/>,然后
1 <authentication-manager erase-credentials="false"> 2 ... 3 </authentication-manager>
在<authentication-manager>节点增加一个属性erase-credentials="false" ,配置的修改就算完了
二、登录页login.jsp
1 <input id="_spring_security_remember_me" name="_spring_security_remember_me" type="checkbox" value="true"/>
加上这个checkbox勾选框即可
原理简析:按上面的步骤修改后,如果在登录时勾选了Remember Me,登录成功后,会在浏览器中生成一个名为SPRING_SECURITY_REMEMBER_ME_COOKIE的Cookie项,默认有效值为2周,其值是一个加密字符串,其值据说与用户名、密码等敏感数据有关!
下次再进入该页面时,Spring Security的springSecurityFilterChain这个Filter会检测有没有这个Cookie,如果有,就自动登录。
三、安全性分析
安全性分析:这样虽然很方便,但是大家都知道Cookie毕竟是保存在客户端的,很容易盗取,而且cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,毕竟不太安全。
建议:对于一些重要操作,比如:电子商务中的在线支付、修改用户密码等需要本人亲自操作的业务环节,还是要将用户引导至登录页,重新登录,以保证安全。为了达到这个目的,代码就必须在jsp前端以java后端,有办法检测出当前登录的用户,是否通过“Remember Me Cookie”自动登录,还是通过“输入用户名、密码”安全登录。
在jsp前端检查是否Remember Me自动登录很简单,直接使用security提供的tag标签即可,类似下面这样:
1 <%@taglib prefix="sec" uri="http://www.springframework.org/security/tags"%> 2 ... 3 <sec:authorize access="isRememberMe()"> 4 ... 5 </sec:authorize>
在java 服务端的Controller中,可这样检测:
1 /** 2 * 判断用户是否从Remember Me Cookie自动登录 3 * @return 4 */ 5 private boolean isRememberMeAuthenticated() { 6 7 Authentication authentication = SecurityContextHolder.getContext() 8 .getAuthentication(); 9 if (authentication == null) { 10 return false; 11 } 12 13 return RememberMeAuthenticationToken.class 14 .isAssignableFrom(authentication.getClass()); 15 }
此外,spring security还提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在db中保存该加密串-用户信息的对应关系,自动登录时,用cookie中的加密串,到db中验证,如果通过,自动登录才算通过。
先在db中创建一张表:
1 --Remember Me持久化保存记录 2 create table PERSISTENT_LOGINS 3 ( 4 username VARCHAR2(64) not null, 5 series VARCHAR2(64) not null, 6 token VARCHAR2(64) not null, 7 last_used DATE not null 8 ); 9 10 alter table PERSISTENT_LOGINS 11 add constraint PK_PERSISTENT_LOGIN primary key (series);
然后将spring-security.xml中<remember-me/> 改为:
1 <remember-me data-source-ref="dataSource" 2 token-validity-seconds="1209600" 3 remember-me-parameter="remember-me" />
data-source-ref指定数据源,token-validity-seconds表示cookie的有效期(秒为单位),remember-me-parameter对应登录页上checkbox的名字。
这样处理后,勾选Remember me登录会在PERSISTENT_LOGINS表中,生成一条记录:
logout时,该记录以及客户端的cookie都会同时清空。
最后,如果不想用默认的表名persistent_logins,可研究下:
org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl
org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices
这二个类的源码 以及 相关文章:
http://forum.spring.io/forum/spring-projects/security/126343-spring-3-1-persistenttokenbasedremembermeservices-and-usernamepasswordauthentication
http://www.fengfly.com/document/springsecurity3/remember-me.html
http://docs.huihoo.com/spring/spring-security/3.0.x/remember-me.html#remember-me-persistent-token
示例源码:SpringSecurity-Remember-Me-XML(0722).zip
参考文章:Spring Security Remember Me Example
http://www.cnblogs.com/yjmyzz/p/remember-me-sample-in-spring-security3.html
今天在前面一节的基础之上,再增加一点新内容,默认情况下Spring Security不会对登录错误的尝试次数做限制,也就是说允许暴力尝试,这显然不够安全,下面的内容将带着大家一起学习如何限制登录尝试次数。
首先对之前创建的数据库表做点小调整
一、表结构调整
T_USERS增加了如下3个字段:
D_ACCOUNTNONEXPIRED,NUMBER(1) -- 表示帐号是否未过期
D_ACCOUNTNONLOCKED,NUMBER(1), -- 表示帐号是否未锁定
D_CREDENTIALSNONEXPIRED,NUMBER(1) --表示登录凭据是否未过期
要实现登录次数的限制,其实起作用的字段是D_ACCOUNTNONLOCKED,值为1时,表示正常,为0时表示被锁定,另外二个字段的作用以后的学习内容会详细解释。
新增一张表T_USER_ATTEMPTS,用来辅助记录每个用户登录错误时的尝试次数
D_ID 是流水号
D_USERNAME 用户名,外建引用T_USERS中的D_USERNAME
D_ATTEMPTS 登录次数
D_LASTMODIFIED 最后登录错误的日期
二、创建Model/DAO/DAOImpl
要对新加的T_USER_ATTEMPTS读写数据,得有一些操作DB的类,这里我们采用Spring的JDBCTemplate来处理,包结构参考下图:
T_USER_ATTEMPTS表对应的Model如下
1 package com.cnblogs.yjmyzz.model; 2 3 import java.util.Date; 4 5 public class UserAttempts { 6 7 private int id; 8 9 private String username; 10 private int attempts; 11 private Date lastModified; 12 13 public int getId() { 14 return id; 15 } 16 17 public void setId(int id) { 18 this.id = id; 19 } 20 21 public String getUsername() { 22 return username; 23 } 24 25 public void setUsername(String username) { 26 this.username = username; 27 } 28 29 public int getAttempts() { 30 return attempts; 31 } 32 33 public void setAttempts(int attempts) { 34 this.attempts = attempts; 35 } 36 37 public Date getLastModified() { 38 return lastModified; 39 } 40 41 public void setLastModified(Date lastModified) { 42 this.lastModified = lastModified; 43 } 44 45 }
对应的DAO接口
以及DAO接口的实现
观察代码可以发现,对登录尝试次数的限制处理主要就在上面这个类中,登录尝试次数达到阈值3时,通过抛出异常LockedException来通知上层代码。
三、创建CustomUserDetailsService、LimitLoginAuthenticationProvider
为什么需要这个类?因为下面这个类需要它:
这个类继承自org.springframework.security.authentication.dao.DaoAuthenticationProvider,而DaoAuthenticationProvider里需要一个UserDetailsService的实例,即我们刚才创建的CustomUserDetailService
LimitLoginAuthenticationProvider这个类如何使用呢?该配置文件出场了
四、spring-security.xml
1 <beans:beans xmlns="http://www.springframework.org/schema/security" 2 xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://www.springframework.org/schema/beans 4 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 5 http://www.springframework.org/schema/security 6 http://www.springframework.org/schema/security/spring-security-3.2.xsd"> 7 8 <http auto-config="true" use-expressions="true"> 9 <intercept-url pattern="/admin**" access="hasRole('ADMIN')" /> 10 <!-- access denied page --> 11 <access-denied-handler error-page="/403" /> 12 <form-login login-page="/login" default-target-url="/welcome" 13 authentication-failure-url="/login?error" username-parameter="username" 14 password-parameter="password" /> 15 <logout logout-success-url="/login?logout" /> 16 <csrf /> 17 </http> 18 19 <beans:bean id="userDetailsDao" 20 class="com.cnblogs.yjmyzz.dao.impl.UserDetailsDaoImpl"> 21 <beans:property name="dataSource" ref="dataSource" /> 22 </beans:bean> 23 24 <beans:bean id="customUserDetailsService" 25 class="com.cnblogs.yjmyzz.service.CustomUserDetailsService"> 26 <beans:property name="usersByUsernameQuery" 27 value="SELECT d_username username,d_password password, d_enabled enabled,d_accountnonexpired accountnonexpired,d_accountnonlocked accountnonlocked,d_credentialsnonexpired credentialsnonexpired FROM t_users WHERE d_username=?" /> 28 <beans:property name="authoritiesByUsernameQuery" 29 value="SELECT d_username username, d_role role FROM t_user_roles WHERE d_username=?" /> 30 <beans:property name="dataSource" ref="dataSource" /> 31 </beans:bean> 32 33 <beans:bean id="authenticationProvider" 34 class="com.cnblogs.yjmyzz.provider.LimitLoginAuthenticationProvider"> 35 <beans:property name="passwordEncoder" ref="encoder" /> 36 <beans:property name="userDetailsService" ref="customUserDetailsService" /> 37 <beans:property name="userDetailsDao" ref="userDetailsDao" /> 38 </beans:bean> 39 40 <beans:bean id="encoder" 41 class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"> 42 <beans:constructor-arg name="strength" value="9" /> 43 </beans:bean> 44 45 46 <authentication-manager> 47 <authentication-provider ref="authenticationProvider" /> 48 </authentication-manager> 49 50 </beans:beans>
跟之前的变化有点大,47行是核心,为了实现47行的注入,需要33-38行,而为了完成authenticationProvider所需的一些property的注入,又需要其它bean的注入,所以看上去增加的内容就有点多了,但并不难理解。
五、运行效果
连续3次输错密码后,将看到下面的提示
这时如果查下数据库,会看到
错误尝试次数,在db中已经达到阀值3
而且该用户的“是否未锁定”字段值为0,如果要手动解锁,把该值恢复为1,并将T_USER_ATTEMPTS中的尝试次数,改到3以下即可。
源代码下载:SpringSecurity-Limit-Login-Attempts-XML.zip
参考文章: Spring Security : limit login attempts example
http://www.cnblogs.com/yjmyzz/p/limit-login-attempts-in-spring-security3.html
在前一节使用数据库进行用户认证(form login using database)里,我们学习了如何把“登录帐号、密码”存储在db中,但是密码都是明文存储的,显然不太讲究。这一节将学习如何使用spring security3新加入的bcrypt算法,将登录加密存储到db中,并正常通过验证。
一、Bcrypt算法
1 int t = 0; 2 String password = "123456"; 3 System.out.println(password + " -> "); 4 for (t = 1; t <= 10; t++) { 5 BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); 6 String hashedPassword = passwordEncoder.encode(password); 7 System.out.println(hashedPassword); 8 } 9 10 password = "MIKE123"; 11 System.out.println(password + " -> "); 12 for (t = 1; t <= 10; t++) { 13 BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); 14 String hashedPassword = passwordEncoder.encode(password); 15 System.out.println(hashedPassword); 16 }
输出如下:
123456 ->
2a2a10$.Cjkvbgr2JzGkag9IdbT.Oc/sbY7wVqLgAHws7HCxqcI7eczKtCLq
2a2a10$OCOuRV0Wy7ncCND4LcKfMunVEWOzMOyyU95u5TkTRmJqYbsJNecEK
2a2a10$TXttsDZUaeEb2zX6wiwN0eqREKFoCDyh81Kfa6BgAcZ2hyqPNC0Ra
2a2a10$FfLx/gxq.FyeOBb0nbaVeusLhQjASSdY7w45i1ACl/rcYQMmhaXV2
2a2a10$JdPXAxmuz.WTP5gxYiYseeKRSM/HTFzJJdACcDQ4MdhaaLmC0SjI.
2a2a10$yVEWf2MrwjCyi51rUKqQle/MZb7vwcOf6Gwp.hDT2ZUchlyAtJ4pO
2a2a10$FfJg2ATit7btKfJovL6zmug//8rzToQn7FO.fxOzo1KtNNfhWKuca
2a2a10$pOLMkd13n7i3DtVijLEqze1zeURpjtVz5rAx1qOAPqCQvjGG/d6D.
2a2a10$fQ32i8JsjjmqVRpiEsgT3ekTKtrfXn.JNl69beWEx0.YgdX.SEx5e
2a2a10$78brJFSdftip0XXYx4rS6ewdu4SiSsMIBY9oNcLhAZwg3GysRGk2m
MIKE123 ->
2a2a10$U6KVh1NGxAIGYiM4YVgn6OAQt6ayAoLkh2lODv16rSpkS1iqfbR2C
2a2a10$t0FlEOBLEB8VwWJVoZRrweIRV0XyoBgm29c0SMqfqRK3ZBuvhgYbS
2a2a10$QpW6nHnWNhbTTjLq/NbzBu2Unp8ijwyPeUx2N2eMFWReFezosZ5fi
2a2a10$LtPzoQU0IluAgvP3/WhWquUv2AcDRh2ENhAeWDquiN/spitZYe/7q
2a2a10$Qcx7vUudzF7qzTjz.QpLKOby0tXQ4j.uqkInS1n4/6oD2r2eL0rZW
2a2a10$yZw7cdq1y9sjX8nZhYynseWjQ4jeVv76fPmBl.sg2xPvb8cyXD8Sq
2a2a10$kTmT6BQQE5LyRZ00Qas77.F5kxK0GxsW402ExosQswxmG.eBdgIZW
2a2a10$SRfHDNM.m3qX5y1O7V/cp.hQqgaXnKzfxBGRhLkAF39bufejuOieu
2a2a10$Sw5w2kTImJ5Y8UNlE/5/9OLaUgYxhCXU3P3gFBdEbs9PL8pCl60Q2
2a2a10$0mN8kNAl9GNr0c4K1Nr0b.MIcBW0QcPHB/f20hgeBuRfwvgZXT6hG
从以上输出结果发现bcrypt算法与md5/sha算法有一个很大的区别,每次生成的hash值都是不同的,这样暴力猜解起来或许要更困难一些。同时大家可能也发现了,加密后的字符长度比较长,有60位,所以用户表中密码字段的长度,如果打算采用bcrypt加密存储,字段长度不得低于60.