SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录

一、SpringSecurity 响应 json 数据

1、返回 Json 数据

首先先把这两句注释掉:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第1张图片

a、登录成功返回 Json 数据

在上篇的博客中,登录成功后是跳转操作,现在不想要跳转了,想要传回 json 格式的数据。

这里先创建一个返回结果的实体类:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第2张图片
接着开始写登录成功返回 Json 的逻辑:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第3张图片

b、登录失败返回 Json 数据

接着就是登录失败返回 Json 数据:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第4张图片
到这里就基本写完了。接下来就可以测试:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第5张图片
可以发现测试成功。

这里要特别说明两点,一是这里博主密码并没有设置加密。二是即便设置了加密, 返回的数据依然有密码,虽然是加密后的密码,但这样仍然是不安全的,可以在获取密码之后将密码设置为 null。

2、SpringSecurity 使用 Json 格式登录

可以发现,前面用的是 key—value 的格式来登录。

如果仅仅是使用上面的代码来尝试用 Json 格式登录,那必然是不行的:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第6张图片
因为登录接口是 SpringSecurity 提供的,所以要自己手动去按照里面的方式去配置。

a、查看源码

首先要查看源码,才能根据里面的处理方式配置出自己想要的处理方式。
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第7张图片

b、过滤 Json 格式和 key—value 格式

通过上面的源码,来写一个过滤器,过滤 Json 格式和 key—value 格式:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第8张图片
这里就把代码贴一下:

@Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

//        判断请求参数是 key—value 形式还是 json 形式
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)){
//            说明参数是 JSON 格式
            if (!request.getMethod().equals("POST")){
                throw new AuthenticationServiceException("Authentication method not supported:"+request.getMethod());
            }
            String username = "";
            String password = "";
            try {
//                读取请求参数
                User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
                username = user.getUsername();
                password = user.getPassword();
            } catch (IOException e) {
                e.printStackTrace();
            }
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            setDetails(request,authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }else {
//            说明参数是 key—value 格式
            return super.attemptAuthentication(request,response);
        }
    }

c、在 config 里面配置过滤器

这里并没有在过滤器上面添加注解以注册到 Spring 容器中,因为这里加了注解不太好处理;因为待会还要配置很多属性。通过自动装填的方式注册到 Spring 容器中。

如果自定义 Filter ,那么之前配置关于登录表单的东西统统都会失效。包括后面的登录成功和失败的回调都会失效。这些东西都需要重新配置!(意思是只要自定义 Filter ,那么前面关于验证跟登录的代码都失效了!)看下图:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第9张图片
然后还有这一块:调用下图的方法可以获取到 authenticationManager 的实例,这里重写了这个方法但是不需要任何改动,直接注册到 Spirng 容器中即可。

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第10张图片

接着还要将自定义的过滤器加入已有的过滤器链中:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第11张图片
那么到这就结束了。

现在既支持 Json 形式也支持 key—value 形式的数据。

二、SpringSecurity 资源放行问题

1、静态资源放行

上面的登录逻辑中,拦截了很多访问路径,但是在一个网站中,很多静态资源例如 html文件、css 文件
js文件这些是不拦截的,不涉及安全验证。

在给一个资源放行这一块,有两种方法,现在创建两个 html 文件:
在这里插入图片描述

a、给一个资源放行——方法一

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第12张图片

b、给一个资源放行——方法二

在 SecurityConfig 文件里面:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第13张图片

c、两种方式的区别和如何选择

方式一:请求将来会经过 Spring Security 过滤器链,但是不会被拦截。类似于 shiro 的/01.html=anon

方式二:这个地方配置的放行的请求,将来用户请求这个资源的时候,将不会经过 Spring Security 过滤器链。

这里的选择并没有正确答案,不过大致的原则应该是这样:
1、静态资源都可以配置不经过 Spring Security 过滤器链(因为静态资源不需要经过计算,都是原封不动的取回来)
2、非静态资源,需要匿名访问,则可以配置经过 Spring Security 过滤器链,因为只有经过了过滤器链,才有用户信息,如果不经过过滤器链,则是获取不到用户信息的。

根据上面的原则,这里讲一点注意信息:如果现在有某个接口,需要经过涉及到一些用户信息,或者计算等这类的这种接口(也就是动态资源接口),那么就要使用方式一,因为只有经过过滤器链才能获取到用户信息。

2、测试两种方式中是否包含用户信息

这个章节主要验证测试上一个小结中的结论:
接口:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第14张图片
然后输出信息:
在这里插入图片描述

此时可以看到是可以输出用户信息的。

a、通过方式一的方式来测试

那么现在给 hello 接口设置可以匿名访问:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第15张图片

然访问接口,结果:
在这里插入图片描述
这里会报错,这是正常的,因为是通过匿名用户去访问,匿名用户的 getPrincipal()获取的结果是一个字符串,所以是强转不了成 User 的。但是这里还是可以获取到信息的!

下图就是匿名用户的字符串:
在这里插入图片描述

这里稍微补充一点:
这里用 permitAll:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第16张图片

结果:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第17张图片
很明显,第一个是直接访问的结果,第二个是登录后再访问的结果。

b、通过方式二的方式来测试

上面用的是方式一来测试,这里用方式二来测试:
给 hello 接口设置匿名访问:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第18张图片

然后去访问,这里分两次访问,一是直接匿名访问,二是登录后再去访问:
直接匿名访问:
在这里插入图片描述
会报一个空指针错误,因为不登录会导致 getAuthentication() 为 null,那么后面的 getPrincipal()这里就会报空指针错误。

然后登录后再去访问:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第19张图片

还是报错。错误依然是空指针异常,但是明明是登录过了。这就能验证之前说的,没有经过过滤器链,就获取不到用户信息。

c、两种方式中最重要的区别

Spring Security 登录流程:
登录成功之后,系统会把当前登录成功的用户信息保存到两个地方:

  1. HttpSession 中,但是在日常开发中,一般不会从 HttpSession 中去提取当前登录的用户信息,因为比较麻烦。
  2. SecurityContext 中,这里可以通过 SecurityContextHolder.getContest() 方法直接获取在登录请求结束的时候,系统会自动擦除 SecurityContext 中的用户信息。那么这时用户信息只存在于HttpSession中。

Spring Security 过滤器链默认情况下什么都不配置不写,是 15 个过滤器组成的过滤器链。总共是 32 个。

以后每一次发送请求的时候,当请求到达 (15 个过滤器中的第个)SecurityContextPersistenceFilter 过滤器的时候,该过滤器会自动从HttpSession 中读取出来当前用户信息并存入 SecurityContext 中,这样在用户后续的处理中,就可以从SecurityContextHolder.getContest() 中获取当前登录用户信息了。当请求结束的时候,又会自动擦除 SecurityContext 中的用户信息。

在 Spring Security 框架中,凡是需要提取当前登录的用户信息,都是从 SecurityContext 中拿!

补充一个知识点:默认情况下,SecurityContextHolder 保存 SecurityContext 的方式是存在 ThreadLocal 中,即哪个线程存,哪个线程取。

3、批量放行静态资源

上面是放行一个静态资源,下图是放行某些文件夹内的所有静态资源等:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第20张图片

三、SpringSecurity中的登录

1、自动登录——rememberMe

a、实现 rememberMe 功能

项目层级:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第21张图片

代码:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第22张图片

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第23张图片
效果:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第24张图片
接着用刚刚的账号密码就能登录。

如果通过 rememberme 登录,就会发现 cookie 里面多了个 rememberme 的参数:
在这里插入图片描述
以后每次发请求,都会带上这个。

可以通过 base64解码:
在这里插入图片描述
可以得到一个时间戳,意思是什么时候过期,默认是两周后过期。

后面的加密文字,用的是当前的用户名、密码、时间戳和配的 key ,将这四个拼到一起成一个字符串,然后再用 md5 加密,得到这个结果。所以是不可逆的。

b、自定义接口是否要开启 rememberMe 功能

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第25张图片

2、会话管理

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第26张图片
看看被挤下线的效果:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第27张图片

但是上面的配置有点瑕疵:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第28张图片

在已经登录的情况下,其他浏览器再次登录会不允许登录:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第29张图片

这时候原来的注销,再次登录,但结果一样是登不上去。

虽然注销的时候 session 是销毁了,但是原理是内存中会自己去统计了当前有几个会话,用的是 map 去保存。虽然 session 销毁了,但是 map 没有,换句话说是 Spring 容器不知道要销毁。但是其实是有提供工具的:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第30张图片
原理:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第31张图片

3、csrf 请求攻击与防御

a、前后端不分离的方式

csrf 攻击是跨域请求伪造。

举个例子:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第32张图片

手机中的 app 不必担心这个。

只有浏览器需要担心,因为浏览器有个机制是自动携带 cookie 的机制,才有这种问题。其实别人也不知道我们的 cookie,只是利用了这种机制造出这种攻击。

解决思路很简单:在携带 cookie 的同时,额外在携带一个随机的字符串,这个字符串叫令牌。这个令牌只有自己知道,浏览器也不知道。浏览器可以携带 cookie ,但是不会携带这个令牌。

csrf 防御是默认开启的。

除了登录之外,其他访问都需要这个令牌。这个令牌需要自己手动渲染;如果不带这个令牌,哪怕是自己访问自己的接口,也是访问不了,报 403(权限不足) 错误。

那么怎么带这个令牌呢?:

在这里插入图片描述
这样就可以了。

b、关闭 csrf 防御

默认是开启的。

这样关闭:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第33张图片

b、前后端分离的方式

后端这么处理,就可以了。
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第34张图片
虽然现在令牌是放到 cookie 里面了,但是不能直接用的。需要用 JavaScript 读取 cookie,然后把里面的令牌拎出来,用 js 构造一个请求参数,然后把这个参数传上去才可以。

csrf 攻击是没有把这个令牌拎出来,反而是一股脑的发送上去。对于服务端来说,是没有接收到这个令牌的。

那么前端怎么处理呢?:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第35张图片
这里引入了 jq 的 cookie 库,不用这个也行。

四、JWT——有状态登录和无状态登录

到目前为止,基本所有的登录都是有状态登录。

1、有状态和无状态

a、什么是有状态

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第36张图片

b、什么是无状态

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第37张图片

c、如何实现无状态

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第38张图片

2、JWT简介

目前比较流行的技术栈就是 JWT,全称是 Json Web Token, 是一种 JSON 风格的轻量级的授权和身份认证规范,可实现无状态、分布式的 Web 应用授权(JWT 只是一种规范,可以用在很多语言里面):
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第39张图片

交互流程:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第40张图片

3、JWT 数据格式

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第41张图片
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第42张图片

4、使用 JWT

用 shiro 和不用框架都能使用。

这里使用 SpringSecurity。

a、前期准备

依赖只需要三个:web 和 Security (这两个创建项目的时候选中)和 JJWT(手动导入):

<dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwt-implartifactId>
            <version>0.11.2version>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwt-apiartifactId>
            <version>0.11.2version>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwt-jacksonartifactId>
            <version>0.11.2version>
            <scope>runtimescope>
        dependency>

这里为了省事,就不使用数据库:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第43张图片

b、接口

JWT 的写法特别多,这里只是展示其中之一的写法:
先写个接口:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第44张图片

c、SecurityConfig(主要作用是构建 jwt 字符串)

然后配置 SecurityConfig(除去 http.addFilterBefore 这行代码(就在方法下的第一行),其他的代码的作用基本就是构建 jwt 字符串,保存用户信息):
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第45张图片
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第46张图片

到此就配置完毕。

此时可以使用 postman 测试 login 接口获取 jwt 字符串:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第47张图片

d、JwtFilter——拦截请求,提取 JWT 字符串进行校验

SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第48张图片
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第49张图片

e、测试

首先先获取 jwt 字符串。

然后复制 jwt 字符串。接着去访问 hello 接口:
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第50张图片
SpringBoot(九)——SpringSecurity 响应 json 数据、资源放行问题、登录(自动登录,会话管理、csrf请求攻击和防御)、JWT——有状态登录和无状态登录_第51张图片

这时候就能发现能够成功访问 hello 接口。

5、JWT 存在的问题

说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如
下:

  1. 续签问题,这是被很多人诟病的问题之一,传统的 cookie+session 的方案天然的支持续签,但是jwt 由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入 redis,虽然可以解决问题,但是 jwt 也变得不伦不类了。
  2. 注销问题,由于服务端不再保存用户信息,所以一般可以通过修改 secret 来实现注销,服务端secret 修改后,已经颁发的未过期的 token 就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。
  3. 密码重置,密码重置后,原本的 token 依然可以访问系统,这时候也需要强制修改 secret。
  4. 基于第 2 点和第 3 点,一般建议不同用户取不同 secret。

私以为最大的问题是注销的问题。

一旦签发出去,给用户使用了,服务端就无法控制了,除非到期过期,否则这个字符串一直是可用的。即使丢失了服务端也没有任何办法。

在实际应用中,很难做到真正纯粹的无状态,也就是不需要 session,没有 session 做不了注销。所以这个 jwt 还是会结合 redis 来使用,虽然这个方案不是很好的方案,但也没有其他办法了。

你可能感兴趣的:(SpringBoot,Java,java,SpringSecurity,JWT)