springsecurity不拦截某个接口_SpringSecurity 默认表单登录页展示流程源码
SpringSecurity 默认表单登录页展示流程源码
本篇主要讲解 SpringSecurity提供的默认表单登录页 它是如何展示的的流程,涉及1.FilterSecurityInterceptor,2.ExceptionTranslationFilter ,3.DefaultLoginPageGeneratingFilter 过滤器,并且简单介绍了 AccessDecisionManager 投票机制
1.准备工作(体验SpringSecurity默认表单认证)
1.1 创建SpringSecurity项目
先通过IDEA 创建一个SpringBoot项目 并且依赖SpringSecurity,Web依赖
此时pom.xml会自动添加
org.springframework.boot spring-boot-starter-security
1.2 提供一个接口
@RestController public class HelloController {
@RequestMapping("/hello") public String hello() { return "Hello SpringSecurity"; } }
1.3 启动项目
直接访问 提供的接口
http://localhost:8080/hello
会发现浏览器被直接重定向到了 /login 并且显示如下默认的表单登录页
http://localhost:8080/login
显示如下:
1.4 登录
在启动项目的时候 控制台会打印一个 seuciryt password : xxx
Using generated security password: f520875f-ea2b-4b5d-9b0c-f30c0c17b90b
直接登录
用户名:user 密码 :f520875f-ea2b-4b5d-9b0c-f30c0c17b90b
登录成功并且 浏览器又会重定向到 刚刚访问的接口
2.springSecurityFilterchain 过滤器链
如果你看过我另一篇关于SpringSecurity初始化源码的博客,那么你一定知道当SpringSecurity项目启动完成后会初始化一个 springSecurityFilterchain 它内部 additionalFilters属性初始化了很多Filter 如下所有的请求都会经过这一系列的过滤器 Spring Security就是通过这些过滤器 来进行认证授权等
3.FilterSecurityInterceptor (它会判断这次请求能否通过)
FilterSecurityInterceptor是过滤器链中最后一个过滤器,主要用于判断请求能否通过,内部通过AccessDecisionManager 进行投票判断
当我们未登录访问
http://localhost:8080/hello
请求会被 FilterSecurityInterceptor 拦截
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); }
重点看invoke方法
public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null && observeOncePerRequest) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); }
InterceptorStatusToken token = super.beforeInvocation(fi);
try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); }
super.afterInvocation(token, null); } }
源码中有这样一句,其实就是判断当前用户是否能够访问指定的接口,可以则执行 fi.getChain().doFilter 调用访问的接口否则 内部会抛出异常
InterceptorStatusToken token = super.beforeInvocation(fi);
** beforeInvocation 方法内部是通过 accessDecisionManager 去做决定的 Spring Security已经内置了几个基于投票的AccessDecisionManager包括(AffirmativeBased ,ConsensusBased ,UnanimousBased)当然如果需要你也可以实现自己的AccessDecisionManager**
使用这种方式,一系列的AccessDecisionVoter将会被AccessDecisionManager用来对Authentication是否有权访问受保护对象进行投票,然后再根据投票结果来决定是否要抛出AccessDeniedException
this.accessDecisionManager.decide(authenticated, object, attributes);
AffirmativeBased的 decide的实现如下
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException { int deny = 0; Iterator var5 = this.getDecisionVoters().iterator();
while(var5.hasNext()) { AccessDecisionVoter voter = (AccessDecisionVoter)var5.next(); int result = voter.vote(authentication, object, configAttributes); if (this.logger.isDebugEnabled()) { this.logger.debug("Voter: " + voter + ", returned: " + result); }
switch(result) { case -1: ++deny; break; case 1: return; } }
if (deny > 0) { throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied")); } else { this.checkAllowIfAllAbstainDecisions(); } }
AffirmativeBased的逻辑是这样的:
(1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。
当我们第一次访问的时候
http://localhost:8080/hello的时候
返回 result = -1 会抛出 AccessDeniedException 拒绝访问异常
4.ExceptionTranslationFilter (捕获AccessDeniedException异常)
该过滤器它会接收到FilterSecurityInterceptor抛出的 AccessDeniedException异常)并且进行捕获,然后发送重定向到/login请求
源码如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res;
try { chain.doFilter(request, response);
logger.debug("Chain processed normally"); } catch (IOException ex) { throw ex; } catch (Exception ex) { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) { ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); }
if (ase != null) { if (response.isCommitted()) { throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex); } handleSpringSecurityException(request, response, chain, ase); } else { // Rethrow ServletExceptions and RuntimeExceptions as-is if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; }
// Wrap other Exceptions. This shouldn't actually happen // as we've already covered all the possibilities for doFilter throw new RuntimeException(ex); } } }
当获取异常后 调用
handleSpringSecurityException(request, response, chain, ase);
handleSpringSecurityException 源码如下:
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { logger.debug( "Authentication exception occurred; redirecting to authentication entry point", exception);
sendStartAuthentication(request, response, chain, (AuthenticationException) exception); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) { logger.debug( "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception);
sendStartAuthentication( request, response, chain, new InsufficientAuthenticationException( messages.getMessage( "ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource"))); } else { logger.debug( "Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception);
accessDeniedHandler.handle(request, response, (AccessDeniedException) exception); } } }
先判断获取的异常是否是AccessDeniedException 再判断是否是匿名用户,如果是则调用 sendStartAuthentication 重定向到登录页面
重定向登录页面之前会保存当前访问的路径,这就是为什么我们访问 /hello接口后 再登录成功后又会跳转到 /hello接口,因为在重定向到/login接口前 这里进行了保存 requestCache.saveRequest(request, response);
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { // SEC-112: Clear the SecurityContextHolder's Authentication, as the // existing Authentication is no longer considered valid SecurityContextHolder.getContext().setAuthentication(null); requestCache.saveRequest(request, response); logger.debug("Calling Authentication entry point."); authenticationEntryPoint.commence(request, response, reason); }
authenticationEntryPoint.commence(request, response, reason);方法内部
调用LoginUrlAuthenticationEntryPoint 的 commence方法
LoginUrlAuthenticationEntryPoint 的commence方法内部有 构造重定向URL的方法
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
String loginForm = determineUrlToUseForThisRequest(request, response, authException);
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
return getLoginFormUrl(); } 最终会获取到需要重定向的URL /login
然后sendRedirect 既会重定向到 /login 请求
5.DefaultLoginPageGeneratingFilter (会捕获重定向的/login 请求)
DefaultLoginPageGeneratingFilter是过滤器链中的一个用于捕获/login请求,并且渲染出一个默认表单页面
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res;
boolean loginError = isErrorPage(request); boolean logoutSuccess = isLogoutSuccess(request); if (isLoginUrlRequest(request) || loginError || logoutSuccess) { String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess); response.setContentType("text/html;charset=UTF-8"); response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length); response.getWriter().write(loginPageHtml);
return; }
chain.doFilter(request, response); }
isLoginUrlRequest 判断请求是否是 loginPageUrl
private boolean isLoginUrlRequest(HttpServletRequest request) { return matches(request, loginPageUrl); }
因为我们没有配置所以 默认的 loginPageUrl = /login
验证通过请求路径 能匹配 loginPageUrl
String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
generateLoginPageHtml 绘制默认的HTML 页面,到此我们默认的登录页面怎么来的就解释清楚了
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { String errorMsg = "Invalid credentials";
if (loginError) { HttpSession session = request.getSession(false);
if (session != null) { AuthenticationException ex = (AuthenticationException) session .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); errorMsg = ex != null ? ex.getMessage() : "Invalid credentials"; } }
StringBuilder sb = new StringBuilder();
sb.append("" + "" + " " + " " + " " + " " + " " + " Please sign in" + " " + " " + " " + " " + "
");
String contextPath = request.getContextPath(); if (this.formLoginEnabled) { sb.append(" " + "
Please sign in
Please sign in
" + createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "
" + " Username" + " " + "
" + "
" + " Password" + " " + "
" + createRememberMe(this.rememberMeParameter) + renderHiddenInputs(request) + " Sign in" + " "); }
if (openIdEnabled) { sb.append(" " + "
Login with OpenID Identity
Login with OpenID Identity
" + createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "
" + " Identity" + " " + "
" + createRememberMe(this.openIDrememberMeParameter) + renderHiddenInputs(request) + " Sign in" + " "); }
if (oauth2LoginEnabled) { sb.append("
Login with OAuth 2.0
Login with OAuth 2.0
"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append("
"); for (Map.Entry clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) { sb.append(" "); String url = clientAuthenticationUrlToClientName.getKey(); sb.append(""); String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue()); sb.append(clientName); sb.append(""); sb.append(""); } sb.append(""); }
if (this.saml2LoginEnabled) { sb.append("
Login with SAML 2.0
Login with SAML 2.0
"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append("
"); for (Map.Entry relyingPartyUrlToName : saml2AuthenticationUrlToProviderName.entrySet()) { sb.append(" "); String url = relyingPartyUrlToName.getKey(); sb.append(""); String partyName = HtmlUtils.htmlEscape(relyingPartyUrlToName.getValue()); sb.append(partyName); sb.append(""); sb.append(""); } sb.append(""); } sb.append("
"); sb.append("");
return sb.toString(); }
至此 SpringSecurity 默认表单登录页展示流程源码部分已经全部讲解完毕,会渲染出下面的页面,但是一定要有网的情况,否则样式可能会变化
6.总结
本篇主要讲解 SpringSecurity提供的默认表单登录页 它是如何展示的的流程,包括涉及这一流程中相关的 3个过滤器
1.FilterSecurityInterceptor
2.ExceptionTranslationFilter
3.DefaultLoginPageGeneratingFilter
并且简单介绍了一下 AccessDecisionManager 它主要进行投票来判断该用户是否能够访问相应的 资源AccessDecisionManager 投票机制我也没有深究 后续我会详细深入一下再展开
个人博客网站 https://www.askajohnny.com 欢迎访问!
本文由博客一文多发平台 https://openwrite.cn?from=article_bottom 发布!
springsecurity不拦截某个接口_SpringSecurity 默认表单登录页展示流程源码相关推荐
- vue登录如何存储cookie_vue项目实现表单登录页保存账号和密码到cookie功能
实现功能: 1.一周内自动登录勾选时,将账号和密码保存到cookie,下次登陆自动显示到表单内 2.点击忘记密码则清空之前保存到cookie的值,下次登陆需要手动输入 次要的就不说了直接上主要的代码 ...
- SpringSecurity系列(三) Spring Security 表单登录
SpringSecurity系列(一) 初识 Spring Security SpringSecurity系列(二) Spring Security入门 1. 服务端 1.1 依赖 <?xml ...
- remmima 不能保存_Vue项目实现表单登录页保存账号和密码到cookie功能_婳祎_前端开发者...
实现功能: 1.一周内自动登录勾选时,将账号和密码保存到cookie,下次登陆自动显示到表单内 2.点击忘记密码则清空之前保存到cookie的值,下次登陆需要手动输入 次要的就不说了直接上主要的代码 ...
- 【从零开始学BPM,Day2】默认表单开发
[课程主题] 主题:5天,一起从零开始学习BPM [课程形式] 1.为期5天的短任务学习 2.每天观看一个视频,视频学习时间自由安排. [第二天课程] Step 1 软件下载:H3 BPM10.0全开 ...
- 清除浏览器默认表单边框/背景特效和下拉菜单背景
大家应该都留意到类似谷歌和火狐浏览器中都会有自己默认的样式设置,比如谷歌浏览器所有的表单都会添加黄色边框特效.当网站本身就已经对于表单样式有一定的设计时,这种默认表单样式就会让人觉得多余了!这里我们介 ...
- SpringBoot 整合Security——自定义表单登录
文章目录 一.添加验证码 1.1 验证servlet 1.2 修改 login.html 1.3 添加匿名访问 Url 二.AJAX 验证 三.过滤器验证 3.1 编写验证码过滤器 3.2 注入过滤器 ...
- 表单提交_Linux curl 表单登录或提交与cookie使用
本文主要讲解通过curl 实现表单提交登录.单独的表单提交与表单登录都差不多,因此就不单独说了. 说明:针对curl表单提交实现登录,不是所有网站都适用,原因是有些网站后台做了限制或有其他校验.我们不 ...
- html透明表单登录注册页面源码
大家好,今天给大家介绍一款,html透明表单登录注册页面源码(图1).送给大家哦,获取方式在本文末尾. 图1 自适应页面,适合于各种分辨率(图2) 图2 部分源码: <!doctype html ...
- VC POST表单——登录验证新浪邮箱
1.本机环境: Windows XP SP3.ADSL 2.开发工具: WildPackets OmniPeek V5.1.4 Visual C++ 6.0 IE6.0 FlexEdit V2.3.1 ...
最新文章
- Android 四大组件之——Acitivity(二) 启动模式
- Microstation研发
- Linux集群架构(下)——DR模式、keepalived+LVS
- CentOS 6.0 X86_64 下 vsftp 配置过程
- ASP.NET Core MVC 2.1 顶级参数验证
- Java 7 Update 21安全改进的详细信息
- Django之templates模板
- VS加快程序编译速度——以VS2013为例
- hdu 3037 Lucas定理
- Linus 发文宣布Linux Kernel 5.0 正式发布
- 剑指Offer面试题:1.实现单例模式
- 北理计算机语言智能与社会计算,北京理工大学校长张军描绘智慧社会:人在思、云在算、端在造...
- HDU1263 水果【map】
- 一张图30分钟带你入门python-大数据时代来了!神级程序员一张图帮你梳理Python脉络,快速入门...
- mysql什么是表的并的关系_MySQL表与表的关系
- 题号的随机抽取滚动背景以及题号展示背景如何定制?
- 浅谈聚合支付系统的安全性
- 构造-牛客寒假集训营3-牛牛的DRB迷宫II
- 记微博上一次“落荒而逃”
- 2023华中农业大学计算机考研信息汇总