在上篇的博客中,登录成功后是跳转操作,现在不想要跳转了,想要传回 json 格式的数据。
这里先创建一个返回结果的实体类:
接着开始写登录成功返回 Json 的逻辑:
接着就是登录失败返回 Json 数据:
到这里就基本写完了。接下来就可以测试:
可以发现测试成功。
这里要特别说明两点,一是这里博主密码并没有设置加密。二是即便设置了加密, 返回的数据依然有密码,虽然是加密后的密码,但这样仍然是不安全的,可以在获取密码之后将密码设置为 null。
可以发现,前面用的是 key—value 的格式来登录。
如果仅仅是使用上面的代码来尝试用 Json 格式登录,那必然是不行的:
因为登录接口是 SpringSecurity 提供的,所以要自己手动去按照里面的方式去配置。
首先要查看源码,才能根据里面的处理方式配置出自己想要的处理方式。
通过上面的源码,来写一个过滤器,过滤 Json 格式和 key—value 格式:
这里就把代码贴一下:
@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);
}
}
这里并没有在过滤器上面添加注解以注册到 Spring 容器中,因为这里加了注解不太好处理;因为待会还要配置很多属性。通过自动装填的方式注册到 Spring 容器中。
如果自定义 Filter ,那么之前配置关于登录表单的东西统统都会失效。包括后面的登录成功和失败的回调都会失效。这些东西都需要重新配置!(意思是只要自定义 Filter ,那么前面关于验证跟登录的代码都失效了!)看下图:
然后还有这一块:调用下图的方法可以获取到 authenticationManager 的实例,这里重写了这个方法但是不需要任何改动,直接注册到 Spirng 容器中即可。
接着还要将自定义的过滤器加入已有的过滤器链中:
那么到这就结束了。
现在既支持 Json 形式也支持 key—value 形式的数据。
上面的登录逻辑中,拦截了很多访问路径,但是在一个网站中,很多静态资源例如 html文件、css 文件
js文件这些是不拦截的,不涉及安全验证。
在给一个资源放行这一块,有两种方法,现在创建两个 html 文件:
方式一:请求将来会经过 Spring Security 过滤器链,但是不会被拦截。类似于 shiro 的/01.html=anon
方式二:这个地方配置的放行的请求,将来用户请求这个资源的时候,将不会经过 Spring Security 过滤器链。
这里的选择并没有正确答案,不过大致的原则应该是这样:
1、静态资源都可以配置不经过 Spring Security 过滤器链(因为静态资源不需要经过计算,都是原封不动的取回来)
2、非静态资源,需要匿名访问,则可以配置经过 Spring Security 过滤器链,因为只有经过了过滤器链,才有用户信息,如果不经过过滤器链,则是获取不到用户信息的。
根据上面的原则,这里讲一点注意信息:如果现在有某个接口,需要经过涉及到一些用户信息,或者计算等这类的这种接口(也就是动态资源接口),那么就要使用方式一,因为只有经过过滤器链才能获取到用户信息。
这个章节主要验证测试上一个小结中的结论:
接口:
然后输出信息:
此时可以看到是可以输出用户信息的。
然访问接口,结果:
这里会报错,这是正常的,因为是通过匿名用户去访问,匿名用户的 getPrincipal()获取的结果是一个字符串,所以是强转不了成 User 的。但是这里还是可以获取到信息的!
结果:
很明显,第一个是直接访问的结果,第二个是登录后再访问的结果。
上面用的是方式一来测试,这里用方式二来测试:
给 hello 接口设置匿名访问:
然后去访问,这里分两次访问,一是直接匿名访问,二是登录后再去访问:
直接匿名访问:
会报一个空指针错误,因为不登录会导致 getAuthentication() 为 null,那么后面的 getPrincipal()这里就会报空指针错误。
还是报错。错误依然是空指针异常,但是明明是登录过了。这就能验证之前说的,没有经过过滤器链,就获取不到用户信息。
Spring Security 登录流程:
登录成功之后,系统会把当前登录成功的用户信息保存到两个地方:
Spring Security 过滤器链默认情况下什么都不配置不写,是 15 个过滤器组成的过滤器链。总共是 32 个。
以后每一次发送请求的时候,当请求到达 (15 个过滤器中的第个)SecurityContextPersistenceFilter 过滤器的时候,该过滤器会自动从HttpSession 中读取出来当前用户信息并存入 SecurityContext 中,这样在用户后续的处理中,就可以从SecurityContextHolder.getContest() 中获取当前登录用户信息了。当请求结束的时候,又会自动擦除 SecurityContext 中的用户信息。
在 Spring Security 框架中,凡是需要提取当前登录的用户信息,都是从 SecurityContext 中拿!
补充一个知识点:默认情况下,SecurityContextHolder 保存 SecurityContext 的方式是存在 ThreadLocal 中,即哪个线程存,哪个线程取。
上面是放行一个静态资源,下图是放行某些文件夹内的所有静态资源等:
如果通过 rememberme 登录,就会发现 cookie 里面多了个 rememberme 的参数:
以后每次发请求,都会带上这个。
可以通过 base64解码:
可以得到一个时间戳,意思是什么时候过期,默认是两周后过期。
后面的加密文字,用的是当前的用户名、密码、时间戳和配的 key ,将这四个拼到一起成一个字符串,然后再用 md5 加密,得到这个结果。所以是不可逆的。
这时候原来的注销,再次登录,但结果一样是登不上去。
虽然注销的时候 session 是销毁了,但是原理是内存中会自己去统计了当前有几个会话,用的是 map 去保存。虽然 session 销毁了,但是 map 没有,换句话说是 Spring 容器不知道要销毁。但是其实是有提供工具的:
原理:
csrf 攻击是跨域请求伪造。
手机中的 app 不必担心这个。
只有浏览器需要担心,因为浏览器有个机制是自动携带 cookie 的机制,才有这种问题。其实别人也不知道我们的 cookie,只是利用了这种机制造出这种攻击。
解决思路很简单:在携带 cookie 的同时,额外在携带一个随机的字符串,这个字符串叫令牌。这个令牌只有自己知道,浏览器也不知道。浏览器可以携带 cookie ,但是不会携带这个令牌。
csrf 防御是默认开启的。
除了登录之外,其他访问都需要这个令牌。这个令牌需要自己手动渲染;如果不带这个令牌,哪怕是自己访问自己的接口,也是访问不了,报 403(权限不足) 错误。
那么怎么带这个令牌呢?:
默认是开启的。
后端这么处理,就可以了。
虽然现在令牌是放到 cookie 里面了,但是不能直接用的。需要用 JavaScript 读取 cookie,然后把里面的令牌拎出来,用 js 构造一个请求参数,然后把这个参数传上去才可以。
csrf 攻击是没有把这个令牌拎出来,反而是一股脑的发送上去。对于服务端来说,是没有接收到这个令牌的。
那么前端怎么处理呢?:
这里引入了 jq 的 cookie 库,不用这个也行。
到目前为止,基本所有的登录都是有状态登录。
目前比较流行的技术栈就是 JWT,全称是 Json Web Token, 是一种 JSON 风格的轻量级的授权和身份认证规范,可实现无状态、分布式的 Web 应用授权(JWT 只是一种规范,可以用在很多语言里面):
用 shiro 和不用框架都能使用。
这里使用 SpringSecurity。
依赖只需要三个: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>
JWT 的写法特别多,这里只是展示其中之一的写法:
先写个接口:
然后配置 SecurityConfig(除去 http.addFilterBefore 这行代码(就在方法下的第一行),其他的代码的作用基本就是构建 jwt 字符串,保存用户信息):
到此就配置完毕。
此时可以使用 postman 测试 login 接口获取 jwt 字符串:
首先先获取 jwt 字符串。
这时候就能发现能够成功访问 hello 接口。
说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如
下:
私以为最大的问题是注销的问题。
一旦签发出去,给用户使用了,服务端就无法控制了,除非到期过期,否则这个字符串一直是可用的。即使丢失了服务端也没有任何办法。
在实际应用中,很难做到真正纯粹的无状态,也就是不需要 session,没有 session 做不了注销。所以这个 jwt 还是会结合 redis 来使用,虽然这个方案不是很好的方案,但也没有其他办法了。