专栏首页玩转JavaEESpring Boot2 系列教程(三十八)Spring Security 非法请求直接返回 JSON

Spring Boot2 系列教程(三十八)Spring Security 非法请求直接返回 JSON

关于 Spring Security,松哥之前发过多篇文章和大家聊聊这个安全框架的使用:

  1. 手把手带你入门 Spring Security!
  2. Spring Security 登录添加验证码
  3. SpringSecurity 登录使用 JSON 格式数据
  4. Spring Security 中的角色继承问题
  5. Spring Security 中使用 JWT!
  6. Spring Security 结合 OAuth2

不过,今天要和小伙伴们聊一聊 Spring Security 中的另外一个问题,那就是在 Spring Security 中未获认证的请求默认会重定向到登录页,但是在前后端分离的登录中,这个默认行为则显得非常不合适,今天我们主要来看看如何实现未获认证的请求直接返回 JSON ,而不是重定向到登录页面。

前置知识

这里关于 Spring Security 的基本用法我就不再赘述了,如果小伙伴们不了解,可以参考上面的 6 篇文章。

大家知道,在自定义 Spring Security 配置的时候,有这样几个属性:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .formLogin()
            .loginProcessingUrl("/doLogin")
            .loginPage("/login")
            //其他配置
            .permitAll()
            .and()
            .csrf().disable();
}

这里有两个比较重要的属性:

  • loginProcessingUrl:这个表示配置处理登录请求的接口地址,例如你是表单登录,那么 form 表单中 action 的值就是这里填的值。
  • loginPage:这个表示登录页的地址,例如当你访问一个需要登录后才能访问的资源时,系统就会自动给你通过重定向跳转到这个页面上来。

这种配置在前后端不分的登录中是没有问题的,在前后端分离的登录中,这种配置就有问题了。我举个简单的例子,例如我想访问 /hello 接口,但是这个接口需要登录之后才能访问,我现在没有登录就直接去访问这个接口了,那么系统会给我返回 302,让我去登录页面,在前后端分离中,我的后端一般是没有登录页面的,就是一个提示 JSON,例如下面这样:

@GetMapping("/login")
public RespBean login() {
    return RespBean.error("尚未登录,请登录!");
}

完整代码大家可以参考我的微人事项目。

也就是说,当我没有登录直接去访问 /hello 这个接口的时候,我会看到上面这段 JSON 字符串。在前后端分离开发中,这个看起来没问题(后端不再做页面跳转,无论发生什么都是返回 JSON)。但是问题就出在这里,系统默认的跳转是一个重定向,就是说当你访问 /hello 的时候,服务端会给浏览器返回 302,同时响应头中有一个 Location 字段,它的值为 http://localhost:8081/login ,也就是告诉浏览器你去访问 http://localhost:8081/login 地址吧。浏览器收到指令之后,就会直接去访问 http://localhost:8081/login 地址,如果此时是开发环境并且请求还是 Ajax 请求,就会发生跨域。因为前后端分离开发中,前端我们一般在 NodeJS 上启动,然后前端的所有请求通过 NodeJS 做请求转发,现在服务端直接把请求地址告诉浏览器了,浏览器就会直接去访问 http://localhost:8081/login 了,而不会做请求转发了,因此就发生了跨域问题。

解决方案

很明显,上面的问题我们不能用跨域的思路来解决,虽然这种方式看起来也能解决问题,但不是最佳方案。

如果我们的 Spring Security 在用户未获认证的时候去请求一个需要认证后才能请求的数据,此时不给用户重定向,而是直接就返回一个 JSON,告诉用户这个请求需要认证之后才能发起,就不会有上面的事情了。

这里就涉及到 Spring Security 中的一个接口 AuthenticationEntryPoint ,该接口有一个实现类:LoginUrlAuthenticationEntryPoint ,该类中有一个方法 commence,如下:

/**
 * Performs the redirect (or forward) to the login form URL.
 */
public void commence(HttpServletRequest request, HttpServletResponse response,
		AuthenticationException authException) {
	String redirectUrl = null;
	if (useForward) {
		if (forceHttps && "http".equals(request.getScheme())) {
			redirectUrl = buildHttpsRedirectUrlForRequest(request);
		}
		if (redirectUrl == null) {
			String loginForm = determineUrlToUseForThisRequest(request, response,
					authException);
			if (logger.isDebugEnabled()) {
				logger.debug("Server side forward to: " + loginForm);
			}
			RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
			dispatcher.forward(request, response);
			return;
		}
	}
	else {
		redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
	}
	redirectStrategy.sendRedirect(request, response, redirectUrl);
}

首先我们从这个方法的注释中就可以看出,这个方法是用来决定到底是要重定向还是要 forward,通过 Debug 追踪,我们发现默认情况下 useForward 的值为 false,所以请求走进了重定向。

那么我们解决问题的思路很简单,直接重写这个方法,在方法中返回 JSON 即可,不再做重定向操作,具体配置如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .formLogin()
            .loginProcessingUrl("/doLogin")
            .loginPage("/login")
            //其他配置
            .permitAll()
            .and()
            .csrf().disable().exceptionHandling()
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException authException) throws IOException, ServletException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                RespBean respBean = RespBean.error("访问失败!");
                if (authException instanceof InsufficientAuthenticationException) {
                    respBean.setMsg("请求失败,请联系管理员!");
                }
                out.write(new ObjectMapper().writeValueAsString(respBean));
                out.flush();
                out.close();
            }
        });
}

在 Spring Security 的配置中加上自定义的 AuthenticationEntryPoint 处理方法,该方法中直接返回相应的 JSON 提示即可。这样,如果用户再去直接访问一个需要认证之后才可以访问的请求,就不会发生重定向操作了,服务端会直接给浏览器一个 JSON 提示,浏览器收到 JSON 之后,该干嘛干嘛。

结语

好了,一个小小的重定向问题和小伙伴们分享下,不知道大家有没有看懂呢?这也是我最近在重构微人事的时候遇到的问题。预计 12 月份,微人事的 Spring Boot 版本会升级到目前最新版,请小伙伴们留意哦。

本文分享自微信公众号 - 牧码小子(a_javaboy),作者:江南一点雨

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 奇怪,Spring Security 登录成功后总是获取不到登录用户信息?

    一开始我觉得这可能是一个小概率 BUG,但是当问的人多了,我觉得这个问题对于新手来说还有一定的普遍性,有必要来写篇文章跟大家仔细聊一聊这个问题,防止小伙伴们掉坑...

    江南一点雨
  • 手把手教你定制 Spring Security 中的表单登录

    https://mpvideo.qpic.cn/0bf2x4aamaaatialrcmapnpfbp6da27qabqa.f10002.mp4?dis_k=34...

    江南一点雨
  • Spring Boot中通过CORS解决跨域问题

    很多人对跨域有一种误解,以为这是前端的事,和后端没关系,其实不是这样的,说到跨域,就不得不说说浏览器的同源策略。 同源策略是由Netscape提出的一个著名的安...

    江南一点雨
  • AI一分钟 | 小米智能音箱mini版曝光,或售199元;特朗普被指利用AI竞选成功

    整理 | 阿司匹林 一分钟AI 3月19日,小米社区有网友曝光了小爱同学mini版,它可能是3月27日小米MIX 2S发布会的“小惊喜”,售价可能为199元。...

    AI科技大本营
  • python接口自动化(二十五)--unittest断言——下(详解)

      本篇还是回归到我们最初始的话题,想必大家都忘记了,没关系看这里:传送门 没错最初的话题就是登录,由于博客园的登录机制改变了,本篇以我找到的开源免费的登录...

    北京-宏哥
  • Nature重磅:华裔科学家成功解码脑电波,AI直接从大脑中合成语音

    【新智元导读】Nature发表华裔作者论文:通过解码大脑活动提升语音的清晰度,使用深度学习方法直接从大脑信号中产生口语句子,达到150个单词,接近正常人水平。

    新智元
  • 巨头的未来AI之战:腾讯向左,百度向右

    镁客网
  • 一周AI新闻回顾(2017-11-26)

    用户1594945
  • “一部手机游云南”亮相智博会 舒展首谈AI加持智慧旅游

    ? 2019年4月19日,首届“国际人工智能与智慧生活应用博览会”在昆明国际会展中心开幕,“一部手机游云南”黑科技展区亮相大会。 智博会将持续到21号,感兴趣...

    腾讯文旅
  • 干货 | 国际化探索之路-Trip.com如何走进阿拉伯市场

    随着国际化之路的进一步推进,Trip.com已经在全球多个国家开设了站点,今天的主角是阿拉伯世界。

    携程技术

扫码关注云+社区

领取腾讯云代金券