我在Spring MVC 4.0.4.RELEASE中使用Spring Security 3.2.1.RELEASE
我正在尝试为具有两个不同登录条目页面的Web应用程序设置Spring Security。我需要将页面区分开来,因为它们的样式和访问方式不同。
首次登录页面适用于管理员用户并保护管理页面/ admin / **
第二个登录页面适用于客户用户并保护客户页面/客户/**.
我试图设置两个WebSecurityConfigurerAdapter子类来配置各个HttpSecurity对象。
如果未经授权,CustomerFormLoginWebSecurity正在保护客户页面并重定向到客户登录页面。 如果未经授权,AdminFormLoginWebSecurity正在保护管理页面重定向到管理员登录页面。
不幸的是,似乎只强制执行第一个配置。我认为我错过了一些额外的东西,以使这两者都有效。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
public void registerGlobalAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("customer").password("password").roles("CUSTOMER").and()
.withUser("admin").password("password").roles("ADMIN");
}
@Configuration
@Order(1)
public static class CustomerFormLoginWebSecurity extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/", "/signin/**", "/error/**", "/templates/**", "/resources/**", "/webjars/**");
}
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/customer/**").hasRole("CUSTOMER")
.and()
.formLogin()
.loginPage("/customer_signin")
.failureUrl("/customer_signin?error=1")
.defaultSuccessUrl("/customer/home")
.loginProcessingUrl("/j_spring_security_check")
.usernameParameter("j_username").passwordParameter("j_password")
.and()
.logout()
.permitAll();
http.exceptionHandling().accessDeniedPage("/customer_signin");
}
}
@Configuration
public static class AdminFormLoginWebSecurity extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/", "/signin/**", "/error/**", "/templates/**", "/resources/**", "/webjars/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/admin_signin")
.failureUrl("/admin_signin?error=1")
.defaultSuccessUrl("/admin/home")
.loginProcessingUrl("/j_spring_security_check")
.usernameParameter("j_username").passwordParameter("j_password")
.and()
.logout()
.permitAll();
http.exceptionHandling().accessDeniedPage("/admin_signin");
}
}
}
答案 0 :(得分:7)
重定向到登录页面的Spring登录链的组件是身份验证过滤器,使用http.formLogin()
时插入的过滤器是DefaultLoginPageGeneratingFilter
。
如果没有提供登录页面网址,此过滤器会重定向到登录网址或构建默认的基本登录页面。
您需要的是一个自定义身份验证过滤器,其逻辑用于定义所需的登录页面,然后将其插入spring安全链中以代替单页身份验证过滤器。
考虑通过继承TwoPageLoginAuthenticationFilter
并覆盖DefaultLoginPageGeneratingFilter
来创建getLoginPageUrl()
,如果这还不够,请复制代码并对其进行修改以满足您的需求。
此过滤器为GenericFilterBean
,因此您可以这样声明:
@Bean
public Filter twoPageLoginAuthenticationFilter() {
return new TwoPageLoginAuthenticationFilter();
}
然后尝试只构建一个http配置,不要设置formLogin()
,而是设置:
http.addFilterBefore(twoPageLoginAuthenticationFilter, ConcurrentSessionFilter.class);
这会将两个表单身份验证过滤器插入链中的正确位置。
答案 1 :(得分:7)
我为多个登录页面提供的解决方案涉及单个http身份验证,但我提供了自己的实现
AuthenticationEntryPoint
AuthenticationFailureHandler
LogoutSuccessHandler
我需要的是这些实现能够依赖于请求路径中的令牌进行切换。
在我的网站中,网址中包含客户令牌的网页受到保护,需要用户在customer_signin页面上以CUSTOMER身份进行身份验证。 因此,如果想要转到页面/客户/主页,那么我需要重定向到customer_signin页面以进行首先进行身份验证。 如果我无法在customer_signin上进行身份验证,那么我应该返回带有错误参数的customer_signin。这样就可以显示一条消息 当我成功通过CUSTOMER认证然后希望注销时,LogoutSuccessHandler会将我带回customer_signin页面。
我对admin需要在admin_signin页面进行身份验证以访问网址中包含管理令牌的网页有类似的要求。
首先,我定义了一个允许我获取令牌列表的类(每种类型的登录页面都有一个)
public class PathTokens {
private final List<String> tokens = new ArrayList<>();
public PathTokens(){};
public PathTokens(final List<String> tokens) {
this.tokens.addAll(tokens);
}
public boolean isTokenInPath(String path) {
if (path != null) {
for (String s : tokens) {
if (path.contains(s)) {
return true;
}
}
}
return false;
}
public String getTokenFromPath(String path) {
if (path != null) {
for (String s : tokens) {
if (path.contains(s)) {
return s;
}
}
}
return null;
}
public List<String> getTokens() {
return tokens;
}
}
然后我在PathLoginAuthenticationEntryPoint
中使用它来根据请求uri中的令牌更改登录URL。
@Component
public class PathLoginAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
private final PathTokens tokens;
@Autowired
public PathLoginAuthenticationEntryPoint(PathTokens tokens) {
// LoginUrlAuthenticationEntryPoint requires a default
super("/");
this.tokens = tokens;
}
/**
* @param request the request
* @param response the response
* @param exception the exception
* @return the URL (cannot be null or empty; defaults to {@link #getLoginFormUrl()})
*/
@Override
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
return getLoginUrlFromPath(request);
}
private String getLoginUrlFromPath(HttpServletRequest request) {
String requestUrl = request.getRequestURI();
if (tokens.isTokenInPath(requestUrl)) {
return "/" + tokens.getTokenFromPath(requestUrl) + "_signin";
}
throw new PathTokenNotFoundException("Token not found in request URL " + requestUrl + " when retrieving LoginUrl for login form");
}
}
PathTokenNotFoundException扩展AuthenticationException,以便您可以通常的方式处理它。
public class PathTokenNotFoundException extends AuthenticationException {
public PathTokenNotFoundException(String msg) {
super(msg);
}
public PathTokenNotFoundException(String msg, Throwable t) {
super(msg, t);
}
}
接下来,我提供AuthenticationFailureHandler
的实现,该实现查看请求标头中的referer url,以确定将用户定向到哪个登录错误页面。
@Component
public class PathUrlAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final PathTokens tokens;
@Autowired
public PathUrlAuthenticationFailureHandler(PathTokens tokens) {
super();
this.tokens = tokens;
}
/**
* Performs the redirect or forward to the {@code defaultFailureUrl associated with this path} if set, otherwise returns a 401 error code.
* <p/>
* If redirecting or forwarding, {@code saveException} will be called to cache the exception for use in
* the target view.
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
setDefaultFailureUrl(getFailureUrlFromPath(request));
super.onAuthenticationFailure(request, response, exception);
}
private String getFailureUrlFromPath(HttpServletRequest request) {
String refererUrl = request.getHeader("Referer");
if (tokens.isTokenInPath(refererUrl)) {
return "/" + tokens.getTokenFromPath(refererUrl) + "_signin?error=1";
}
throw new PathTokenNotFoundException("Token not found in referer URL " + refererUrl + " when retrieving failureUrl for login form");
}
}
接下来,我提供LogoutSuccessHandler
的实现,该实现将注销用户并将其重定向到正确的登录页面,具体取决于请求标头中的参考URL中的令牌。
@Component
public class PathUrlLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final PathTokens tokens;
@Autowired
public PathUrlLogoutSuccessHandler(PathTokens tokens) {
super();
this.tokens = tokens;
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
setDefaultTargetUrl(getTargetUrlFromPath(request));
setAlwaysUseDefaultTargetUrl(true);
handle(request, response, authentication);
}
private String getTargetUrlFromPath(HttpServletRequest request) {
String refererUrl = request.getHeader("Referer");
if (tokens.isTokenInPath(refererUrl)) {
return "/" + tokens.getTokenFromPath(refererUrl) + "_signin";
}
throw new PathTokenNotFoundException("Token not found in referer URL " + refererUrl + " when retrieving logoutUrl.");
}
}
最后一步是在安全配置中将它们连接在一起。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired PathLoginAuthenticationEntryPoint loginEntryPoint;
@Autowired PathUrlAuthenticationFailureHandler loginFailureHandler;
@Autowired
PathUrlLogoutSuccessHandler logoutSuccessHandler;
@Bean
public PathTokens pathTokens(){
return new PathTokens(Arrays.asList("customer", "admin"));
}
@Autowired
public void registerGlobalAuthentication(
AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("customer").password("password").roles("CUSTOMER").and()
.withUser("admin").password("password").roles("ADMIN");
}
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/", "/signin/**", "/error/**", "/templates/**", "/resources/**", "/webjars/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http .csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/customer/**").hasRole("CUSTOMER")
.and()
.formLogin()
.loginProcessingUrl("/j_spring_security_check")
.usernameParameter("j_username").passwordParameter("j_password")
.failureHandler(loginFailureHandler);
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.exceptionHandling().authenticationEntryPoint(loginEntryPoint);
http.exceptionHandling().accessDeniedPage("/accessDenied");
}
}
配置完成后,您需要一个控制器来指向实际的登录页面。下面的SigninControiller检查queryString是否有指示signin错误的值,然后设置用于控制错误消息的属性。
@Controller
@SessionAttributes("userRoles")
public class SigninController {
@RequestMapping(value = "customer_signin", method = RequestMethod.GET)
public String customerSignin(Model model, HttpServletRequest request) {
Set<String> userRoles = AuthorityUtils.authorityListToSet(SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("userRole", userRoles);
if(request.getQueryString() != null){
model.addAttribute("error", "1");
}
return "signin/customer_signin";
}
@RequestMapping(value = "admin_signin", method = RequestMethod.GET)
public String adminSignin(Model model, HttpServletRequest request) {
Set<String> userRoles = AuthorityUtils.authorityListToSet(SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("userRole", userRoles);
if(request.getQueryString() != null){
model.addAttribute("error", "1");
}
return "signin/admin_signin";
}
}
答案 2 :(得分:1)
也许这篇文章可以帮到你: Multiple login forms
这是弹簧安全性的不同版本,但同样的问题:只采取了第一种配置。
似乎已经通过更改两个登录页面之一的login-processing-url来解决,但是人们建议使用相同的URL处理,但使用ViewResolver使用不同的布局。如果您使用相同的机制对用户进行身份验证(验证机制是负责处理浏览器正在发送的凭据的事物),那么这是一种解决方案。
这篇文章似乎也说如果你改变你的loginProcessingUrl你会成功: Configuring Spring Security 3.x to have multiple entry points
答案 3 :(得分:0)
我也遇到了这个问题,发现我错过了第一个过滤部分。
这一个:
http.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
应该是:
http.csrf().disable()
.antMatcher("/admin/**")
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
添加第一个过滤.antMatcher(&#34; / admin / **&#34;)将首先过滤它,以便它将使用AdminFormLoginWebSecurity而不是另一个。