SpringSecurity学习笔记(三)自定义资源拦截规则以及登录界面跳转
参考视频,编程不良人
由前面的学习可以知道,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 {@Overrideprotected 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 #可以让我们的修改立即生效
另外把认证相关的接口放行
@Overrideprotected 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 {@Overridepublic 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" propertysetDetails(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 statusthrow 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 itif (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 itif (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 usernameString 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 {@Overridepublic 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);}@Overridepublic 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学习笔记(三)自定义资源拦截规则以及登录界面跳转相关推荐
- element ui登录界面_vue vue-router vuex element-ui axios 写一个代理平台的学习笔记(四)美化头部和登录界面...
美化头部header.vue 找张头像当用户头像 在写点css来美化一下头部,完善一下现在能完成的功能 header.vue :default-active="$route.path&quo ...
- Spring框架学习笔记(三)(AOP,事务管理)
Spring框架学习笔记(三) 九.AOP 9.1 AOP的注解配置 (1) 新建计算器核心功能(模拟:不能在改动核心代码) (2) 建立一个普通的Java类写增强代码(面向切面编程),使用Sprin ...
- J2EE学习笔记三:EJB基础概念和知识 收藏
J2EE学习笔记三:EJB基础概念和知识 收藏 EJB正是J2EE的旗舰技术,因此俺直接跳到这一章来了,前面的几章都是讲Servlet和JSP以及JDBC的,俺都懂一些.那么EJB和通常我们所说的Ja ...
- K8S 学习笔记三 核心技术 Helm nfs prometheus grafana 高可用集群部署 容器部署流程
K8S 学习笔记三 核心技术 2.13 Helm 2.13.1 Helm 引入 2.13.2 使用 Helm 可以解决哪些问题 2.13.3 Helm 概述 2.13.4 Helm 的 3 个重要概念 ...
- 【AngularJs学习笔记三】Grunt任务管理器
为什么80%的码农都做不了架构师?>>> #0 系列目录# AngularJs学习笔记 [AngularJs学习笔记一]Bower解决js的依赖管理 [AngularJs学习笔 ...
- ROS学习笔记三:创建ROS软件包
,# ROS学习笔记三:创建ROS软件包 catkin软件包的组成 一个软件包必须满足如下条件才能被称之为catkin软件包: 这个软件包必须包含一个catkin编译文件package.xml(man ...
- SurfaceFlinger学习笔记(三)之SurfaceFlinger进程
概述 本系列是基于android Q 即android10 SurfaceFlinger学习笔记(一)应用启动流程 SurfaceFlinger学习笔记(二)之Surface SurfaceFling ...
- MySQL学习笔记(三)查询
写在前面:本篇为作者自学总结,学习内容为课堂所学和网络学习笔记汇总,对于内容引用部分在文中和文末注明. 文章仅供参考,如需深入了解,请查阅MySQL参考手册.附上下载链接: 链接:https://pa ...
- 投资信托学习笔记(三)
投资信托学习笔记(三) 投资信托学习笔记(三) - 补充笔记 百度百科信托投资 https://baike.baidu.com/item/%E4%BF%A1%E6%89%98%E6%8A%95%E8% ...
最新文章
- 技术人如何做职场沟通?
- 使用Harry过程中FAQ(问题解答)
- 人工智能思维导图一张,收藏!
- 面试:a==1 a==2 a==3 是 true 还是 false?
- 用c语言编程实现strcpy,用c语言.模拟实现strcpy,strcat,strcat,memcpy,memmove
- MATLAB怎么查找矩阵中所有0的数据并赋值
- Mahout推荐算法API详解
- 反思项目最新周数的反复反复修改
- 李嘉诚今日正式退休,来看看他一生都做过哪些牛逼的事
- 1.逐帧动画shader
- KAFKA 最新版 Shell API单机生产与消费
- list遍历_Qt 容器类之遍历器和隐式数据共享
- 使用源码安装 PostgreSQL 12.5 主从集群
- php做页面编辑器,最牛在线编辑器ueditor在thinkphp框架中的使用方法
- JForum3 学习笔记1
- 大型网站的系统架构(摘)
- LAMP环境的搭建与部署
- UIStepper (加减符号)(步进控件)
- 笔记本无线网卡驱动异常如何解决?
- 如何安装最新版本的office(preview预览版)、更新
热门文章
- 什么是CSM(Certified Scrum Master) 敏捷认证
- PDF 转成 一张图片
- 初中教师资格证计算机试题,2017上半年初中语文教师资格证面试试题(精选)第二批...
- 2018年首篇,带大家一起学习视图拖拽功能的源码
- oracle 逐层合计,Oracle数据仓库的分层管理器解决方案
- Lettuce(基于Python的BDD工具,中文编写自动化测试用例)
- python骂人代码大全_边看chromium的代码,边想骂人...
- Vue.js实战之系统学习第一节
- 关于绝地求生游戏白名单画中画逆向分析
- HDMI原理详解以及时序流程(视频是三对差分信号,音频Audio是PCM级(无压缩)传输,包含在数据包内,依靠协议规定采样)HDMI可以传输RGB与YUV两种格式