SpringSecurity自定义资源拦截规则及登录界面跳转问题

 更新时间:2023年12月05日 10:25:08   作者:北海冥鱼未眠  
这篇文章主要介绍了SpringSecurity自定义资源拦截规则及登录界面跳转问题,我们想要自定义认证逻辑,就需要创建一些原来不存在的bean,这个时候就可以使@ConditionalOnMissingBean注解,本文给大家介绍的非常详细,需要的朋友参考下吧

由前面的学习可以知道,SS的默认的拦截规则很简单,我们在项目中实际使用的时候往往需要更加复杂的拦截规则,这个时候就需要自定义一些拦截规则。

自定义拦截规则

在我们的项目中,资源往往是需要不同的权限才能操作的,可以分为下面几种:

  • 公共资源:可以随意访问
  • 认证访问:只有登录了之后的用户才能访问。
  • 授权访问:登录的用户必须具有响应的权限才能够访问。

我们想要自定义认证逻辑,就需要创建一些原来不存在的bean,这个时候就可以使@ConditionalOnMissingBean注解发现创建默认的实现类失效。

测试环境搭建

@RequestMapping("/public/test")
    public String justatest(){
        return "just a test,这个是公共资源!";
    }
    @RequestMapping("/private/t1")
    public String t1(){
        return "访问受限资源!";
    }

下面我们重写一个配置类去替换内部默认的配置类

@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       //ss里面要求放行的资源要写在任何请求的前面
        http.authorizeRequests()//开启请求的权限管理
                .mvcMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();//表单验证的方式
    }
}

下面测试,访问公共资源

访问/private/t1跳转到

输入账号密码之后访问到

自定义登录界面

在前面的学习中我们知道了默认的登录界面是在过滤器DefaultLoginPageGeneratingFilter里面实现的,现在我们想要自定义一个登录界面。

首先引入thymeleaf依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

在templates目录下面创建一个login的html页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>冬木自定义用户登录</title>
</head>
<body>
    <form th:action="@{/login}" method="post">
        用户名:<input type="text" name="username"><br>
        密码:<input type="text" name="password"><br>
        <input type="submit" name="登录">
    </form>
</body>
</html>

编写一个controller接口用于跳转到我们自己写的登录页面,

这里的前缀默认就是在templates下面因此我下面直接return login

package com.dongmu.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
    @RequestMapping("/login.html")
    public String login(){
        return "login";
    }
}

添加配置路径,

spring:
  thymeleaf:
    cache: false #可以让我们的修改立即生效

另外把认证相关的接口放行

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        //ss里面要求放行的资源要写在任何请求的前面
        http.authorizeRequests()//开启请求的权限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login");//表单验证的方式,同时指定默认的登录界面
    }

这个时候再去访问页面就会跳转到下面这个页面

这个时候登录会发现还是条状到登录页面,这里要注意,一旦指自定义了登录页面就需要指定登录的url,所以我们在接口里面添加下面的代码

//ss里面要求放行的资源要写在任何请求的前面
        http.authorizeRequests()//开启请求的权限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表单验证的方式,同时指定默认的登录界面
                //一旦自定义登录界面必须指定登录url
                .loginProcessingUrl("/login")
                .and()
                .csrf().disable();

这个时候就可以登录成功了。

但是这时候要注意源码中指定了登录的参数名,只能是username和password。

这个时候可以进行修改如下

http.authorizeRequests()//开启请求的权限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表单验证的方式,同时指定默认的登录界面
                //一旦自定义登录界面必须指定登录url
                .loginProcessingUrl("/login")
                .usernameParameter("uname")//指定登录的参数
                .passwordParameter("pwd")
//                .successForwardUrl("")//默认验证成功之后的跳转,这个是请求转发, 登录成功之后
				//直接跳转到这个指定的地址,原来的地址不跳转了。
                .defaultSuccessUrl("")//这个也是成功之后的跳转路径,默认是请求重定向。 登录成功之
                //后会记住原来访问的路径,也可以再传递一个boolean参数指定地址默认false
                .and()
                .csrf().disable();
前后端分离项目路径跳转

前面介绍了前后端不分离项目的登录认证成功之后的路径跳转,但是针对于前后端分离项目,比如有的时候可能会发送AJAX请求,这个时候怎么处理呢?

我们可以自定义一个类实现AuthenticationSuccessHandler接口即可。

package com.dongmu.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        HashMap<String,Object> hashMap = new HashMap<>();
        hashMap.put("msg","登录成功");
        hashMap.put("code",200);
        hashMap.put("auth",authentication);
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(hashMap);
        response.getWriter().write(s);
    }
}

在successHandler里面配置即可

//ss里面要求放行的资源要写在任何请求的前面
        http.authorizeRequests()//开启请求的权限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表单验证的方式,同时指定默认的登录界面
                //一旦自定义登录界面必须指定登录url
                .loginProcessingUrl("/login")
//                .usernameParameter("uname")
//                .passwordParameter("pwd")
//                .successForwardUrl("")//默认验证成功之后的跳转,这个是请求转发, 登录成功之后直接跳转到这个指定的地址,原来的地址不跳转了。
//                .defaultSuccessUrl("")//这个也是成功之后的跳转路径,默认是请求重定向。 登录成功之后会记住原来访问的路径
                .successHandler(new MyAuthenticationSuccessHandler())//前后端分离的处理方案
                .and()
                .csrf().disable();

这个时候登录成功返回的是一个json字符串。

身份验证失败跳转

首先点进

UsernamePasswordAuthenticationFilter这个类里面由一个方法attemptAuthentication进行身份的验证

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		String password = obtainPassword(request);
		if (username == null) {
			username = "";
		}
		if (password == null) {
			password = "";
		}
		username = username.trim();
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}

然后最后一句代码authenticate(authRequest)会进入

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}
		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}
		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
			// If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}
		// Parent was null, or didn't authenticate (or throw an exception).
		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}
		// If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}

上面代码中

try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}

这一块会进入

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}

这里面user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);的实现

protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

可以发现这里就是去一开始我们学习的map里面找到对应用户名和密码,这里面应该会报出异常。这个异常后面会被这个方法接收

private void doAuthenticate(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		Authentication authResult;
		Object principal = getPreAuthenticatedPrincipal(request);
		Object credentials = getPreAuthenticatedCredentials(request);
		if (principal == null) {
			if (logger.isDebugEnabled()) {
				logger.debug("No pre-authenticated principal found in request");
			}
			return;
		}
		if (logger.isDebugEnabled()) {
			logger.debug("preAuthenticatedPrincipal = " + principal
					+ ", trying to authenticate");
		}
		try {
			PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(
					principal, credentials);
			authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
			authResult = authenticationManager.authenticate(authRequest);
			successfulAuthentication(request, response, authResult);
		}
		catch (AuthenticationException failed) {
			unsuccessfulAuthentication(request, response, failed);
			if (!continueFilterChainOnUnsuccessfulAuthentication) {
				throw failed;
			}
		}
	}

执行unsuccessfulAuthentication

protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
		SecurityContextHolder.clearContext();
		if (logger.isDebugEnabled()) {
			logger.debug("Cleared security context due to exception", failed);
		}
		//这里会把异常信息放到request作用域当中
		request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, failed);
		if (authenticationFailureHandler != null) {
			authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
		}
	}

这里配置请求转发

protected void configure(HttpSecurity http) throws Exception {
        //ss里面要求放行的资源要写在任何请求的前面
        http.authorizeRequests()//开启请求的权限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表单验证的方式,同时指定默认的登录界面
                //一旦自定义登录界面必须指定登录url
                .loginProcessingUrl("/login")
//                .usernameParameter("uname")
//                .passwordParameter("pwd")
//                .successForwardUrl("")//默认验证成功之后的跳转,这个是请求转发, 登录成功之后直接跳转到这个指定的地址,原来的地址不跳转了。
//                .defaultSuccessUrl("")//这个也是成功之后的跳转路径,默认是请求重定向。 登录成功之后会记住原来访问的路径
                .successHandler(new MyAuthenticationSuccessHandler())//前后端分离的处理方案
                .failureForwardUrl("/login.html")//登录失败之后的请求转发页面
//                .failureUrl("/login.html")//登录失败之后的重定向页面
                .and()
                .csrf().disable();
    }

可以直接从request作用域中获取异常

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>冬木自定义用户登录</title>
</head>
<h2>
    <div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
</h2>
<body>
    <form th:action="@{/login}" method="post">
        用户名:<input type="text" name="username"><br>
        密码:<input type="text" name="password"><br>
        <input type="submit" name="登录">
    </form>
</body>
</html>

如果是在重定向就会放在session作用域中。如果是请求转发就会放到reques作用域中。

前后端分离项目认证失败处理

实现接口AuthenticationFailureHandler

package com.dongmu.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
public class MyAuthenticationHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        HashMap<String,Object> hashMap = new HashMap<>();
        hashMap.put("msg","登录成功");
        hashMap.put("code",200);
        hashMap.put("auth",authentication);
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(hashMap);
        response.getWriter().write(s);
    }
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        HashMap<String,Object> hashMap = new HashMap<>();
        hashMap.put("code",403);
        hashMap.put("msg",exception.getMessage());
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(hashMap);
        response.getWriter().write(s);
    }
}

配置认证失败接口实现类

protected void configure(HttpSecurity http) throws Exception {
        //ss里面要求放行的资源要写在任何请求的前面
        http.authorizeRequests()//开启请求的权限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表单验证的方式,同时指定默认的登录界面
                //一旦自定义登录界面必须指定登录url
                .loginProcessingUrl("/login")
//                .usernameParameter("uname")
//                .passwordParameter("pwd")
//                .successForwardUrl("")//默认验证成功之后的跳转,这个是请求转发, 登录成功之后直接跳转到这个指定的地址,原来的地址不跳转了。
//                .defaultSuccessUrl("")//这个也是成功之后的跳转路径,默认是请求重定向。 登录成功之后会记住原来访问的路径
//                .successHandler(new MyAuthenticationSuccessHandler())//前后端分离的处理方案
//                .failureForwardUrl("/login.html")//登录失败之后的请求转发页面
                .failureUrl("/login.html")//登录失败之后的重定向页面
                .failureHandler(new MyAuthenticationHandler())
                .and()
                .csrf().disable();
    }

到此这篇关于SpringSecurity自定义资源拦截规则以及登录界面跳转的文章就介绍到这了,更多相关SpringSecurity登录界面跳转内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring打包jar包时jsp页面无法访问问题解决

    Spring打包jar包时jsp页面无法访问问题解决

    这篇文章主要介绍了Spring打包jar包时jsp页面无法访问问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-05-05
  • JavaWeb开发之【Tomcat 环境配置】MyEclipse+IDEA配置教程

    JavaWeb开发之【Tomcat 环境配置】MyEclipse+IDEA配置教程

    这篇文章主要介绍了JavaWeb开发之【Tomcat 环境配置】MyEclipse+IDEA配置教程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-10-10
  • Java编程实现二项分布的采样或抽样实例代码

    Java编程实现二项分布的采样或抽样实例代码

    这篇文章主要介绍了Java编程实现二项分布的采样或抽样实例代码,小编觉得还是挺不错的,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • Spring MVC 启动过程源码分析详解

    Spring MVC 启动过程源码分析详解

    这篇文章主要介绍了Spring MVC 启动过程源码分析详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-07-07
  • SpringBoot一个非常蛋疼的无法启动的问题解决

    SpringBoot一个非常蛋疼的无法启动的问题解决

    这篇文章主要介绍了SpringBoot一个非常蛋疼的无法启动的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • MyBatis Mapper代理使用方法详解

    MyBatis Mapper代理使用方法详解

    本文是小编日常收集整理的关于mybatis mapper代理使用方法知识,通过本文还给大家提供有关开发规范方面的知识点,本文介绍的非常详细,具有参考借鉴价值,感兴趣的朋友一起看下吧
    2016-08-08
  • Java编程中的vector类用法学习笔记

    Java编程中的vector类用法学习笔记

    Vector通常被用来实现动态数组,即可实现自动增长的对象数组,和C++一样vector类同样被Java内置,下面就来看一下vector类的基本用法.
    2016-05-05
  • Java的MD5工具类和客户端测试类

    Java的MD5工具类和客户端测试类

    这篇文章主要介绍了Java的MD5工具类和客户端测试类,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2021-12-12
  • Java 详解如何获取网络接口信息

    Java 详解如何获取网络接口信息

    读万卷书不如行万里路,只学书上的理论是远远不够的,只有在实践中才能获得能力的提升,本篇文章手把手带你用Java获取网络接口的信息,大家可以在过程中查缺补漏,提升水平
    2021-11-11
  • Java程序部署到服务器上,接口请求下载文件失败/文件为空/文件名不对的问题

    Java程序部署到服务器上,接口请求下载文件失败/文件为空/文件名不对的问题

    这篇文章主要介绍了Java程序部署到服务器上,接口请求下载文件失败/文件为空/文件名不对,本文给大家分享错误原因及解决方法,需要的朋友可以参考下
    2020-07-07

最新评论