最近自己在练习spring boot ,发现真的很好用,登陆是必不可少的,自然而然的就用到了security,然后发现了jwt,就试着做了一下,确实是搞出来了,但是遇到了一些问题。
整合Spring boot security jwt我参考的是Spring Boot中使用使用Spring Security和JWT
大家自己去看看,我只是稍作修改,原理流程和配置都是用的这个。
但是文章都有了我为什么还要写这个呢?
因为我发现了bug啊!!!
所以不要点了我的发的文章链接就把我给关了!!!
至少先看完我发现了什么bug!!!
首先说一下我做的是前后台分离的项目,网上我找了很多的文章,确实是可以达到整合的目的,但是你确定整合了就能用?
网上的文章大部分都是使用postman工具进行测试是否整合完成,他们确定返回值正常就代表整合完成了,也就是说在理想环境下是没问题的,但是我们在开发的时候不确定的因素太多了,我是实实在在的用项目进行测试。
废话说太多了,我们开始聊bug。
首先spring boot 我用的2.x版本,在整合完成之后我确实是看到了返回值正常,token什么的都有,但是仅限于登录,之后请求数据就直接报错了,因为我项目都搞好了,所以我就不改回去把报错信息给大家看(其实怕出问题又改出一堆问题):
1.跨域的问题
这个时候可能大家会问一个问题,登录都成功了怎么会存在跨域的问题?你是没有配置跨域访问吧?mdzz,这都不会?。。。
首先,跨域访问我确实配置了,跨域配置如下:
但是为什么会出现跨域不能访问的问题呢?带着问题找答案,网上确实没有什么文章说这个的,那就只能自己找了
我就观察前台发送的网络请求,发现有一个OPTIONS的请求,请求地址和我需要请求数据的地址是一样的,但是没有我设置的请求头,好像是浏览器自动发送的,返回值是200,但是就是报错,说跨域不能访问,找了一些文章,说是测试服务器是否支持该类型请求。
那么我明明配置了跨域为什么还是会出测试出我不能跨域呢?来看看我推荐的文章里面写的东西
我的测试请求中是没有携带token的,也就是说它会被拦截,最后的结果就是用户没有登录,然后就跳到这货写的拦截器里面去了,他的拦截器也是厉害,就写一个跨域头外加一个错误码,我试过这样的配置,直接是不能跨域,看看我上面贴出的跨域配置写了那么多,他就写一个,有什么办法,改喽。
http.exceptionHandling().authenticationEntryPoint(getEntryPointUnauthorizedHandler()).accessDeniedHandler(getRestAccessDeniedHandler());
//上面这一段是设置两个权限不足的地方
//401错误
class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
jwtAuthenticationTokenFilter.setCorsResponse(request, response);//这个是设置返回头的地方,我单独写到了jwtFilter里面去了,因为还有其他用处
response.setStatus(401);
}
}
//403错误
class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) {
jwtAuthenticationTokenFilter.setCorsResponse(request, response);//这个是设置返回头的地方,我单独写到了JwtAuthenticationTokenFilter里面去了,因为还有其他用处
response.setStatus(403);
}
}
//实例化成bean就可以直接用了,不用像他那样还要新建两个类,我写了两个内部类就行了
@Bean
public EntryPointUnauthorizedHandler getEntryPointUnauthorizedHandler() {
return new EntryPointUnauthorizedHandler();
}
@Bean
public RestAccessDeniedHandler getRestAccessDeniedHandler() {
return new RestAccessDeniedHandler();
}
JwtAuthenticationTokenFilter中的请求头配置 ,和上面自己之前写的配置同步就行
public void setCorsResponse(HttpServletRequest request, HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));//跨域资源请求,*代表任何请求都可以访问
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");//支持的请求方式
response.setHeader("Access-Control-Max-Age", "3600");//跨域缓存实践,这里设置的是一个小时
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept,authorization");//支持的请求头,* 代表所有
response.setHeader("Access-Control-Allow-Credentials", "true");
}
这一下跨域的问题算是解决了,但是新问题又来了
2.验证不通过
刚才说到了浏览器高级跨域会发送一个测试请求OPTIONS来测试服务器是否支持跨域,但是这个是不会返回数据的,它只是测试,但是还是被security拦截了,然后用推文里面的方法返回的不是200码,当然我觉得就算用其他的也会有这个问题,毕竟这个请求确实没有通过验证啊,没通过验证我肯定不能给你正确的返回的,不然我还需要验证干嘛。
这个时候我又上网查了一堆资料,发现没有能解决这个问题的文章,很无奈的又只能自己解决了。
怎么解决呢,其实很简单,只要让这个请求不参与验证就可以了呗,有了思路又开始查资料,发现可以写一个过滤器,写一个CorsFilter就可以了,话不多说贴代码
package com.zh.lore.list.file.config.app;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created by zenghang on 2019/2/12.
*/
//@Configuration
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
HttpServletRequest req = (HttpServletRequest) request;
res.setHeader("Access-Control-Allow-Origin", req.getHeader("origin"));
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
res.setHeader("Access-Control-Max-Age", "3600");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept,authorization");
res.setHeader("Access-Control-Allow-Credentials", "true");
//由于使用前后分离的方式,发现每次请求之前都会发送一个OPTIONS来测试服务器是否支持跨域,但是又被security jwt拦截,所以这里发现这个请求之后直接返回
if (req.getMethod().equals("OPTIONS")) {
res.setStatus(200);
return;
}
chain.doFilter(req, res);
}
@Override
public void destroy() {
}
}
这样写确实可以修复这个bug,但是我这个人太懒,单独为这一个功能写个过滤器太麻烦了吧,而且每个请求都会进入这个过滤器,也有点浪费,所以你们可以看到我上面的@configuration被屏蔽掉了,这个类我也会弃用的。
我在调试的时候发现请求进来的时候优先进入的不是这个过滤器,而是JwtAuthenticationTokenFilter这个过滤器,同时里面都有一段chain.doFilter(req, res);这个代码,所以我果断的修改JwtAuthenticationTokenFilter,贴出我的JwtAuthenticationTokenFilter:
package com.zh.lore.list.file.config.filter;
import com.zh.lore.list.file.entity.User;
import com.zh.lore.list.file.service.UserService;
import com.zh.lore.list.file.utils.JwtUtil.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created by zenghang on 2019/1/17.
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private UserService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
@Autowired
public JwtAuthenticationTokenFilter(UserService userDetailsService, JwtTokenUtil jwtTokenUtil) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String tokenHead = "Bearer ";
if (authHeader != null && authHeader.startsWith(tokenHead)) {
String authToken = authHeader.substring(tokenHead.length());
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
User userDetails = (User) this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} else {
setCorsResponse(request, response);
//由于使用前后分离的方式,发现每次请求之前都会发送一个OPTIONS来测试服务器是否支持跨域,但是又被security jwt拦截,所以这里发现这个请求之后直接返回
if (request.getMethod().equals("OPTIONS")) {
response.setStatus(200);
return;
}
}
chain.doFilter(request, response);
}
public void setCorsResponse(HttpServletRequest request, HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept,authorization");
response.setHeader("Access-Control-Allow-Credentials", "true");
}
}
现在一切正常了,但是其实还有一个问题,也是属于跨域的问题,不过按照我上面写的进行修改,这个问题也就不存在了,但是我还是得提一下
3.跨域
跨域的时候要设置资源请求头,有哪些请求头可以访问,如果有不在设置里面的请求头出现那么这个请求也是不能跨域的,我们这个就有一个authorization这样的请求头,最开始我是没有设置的,也是一直不能跨域访问,在jwt的过滤器中可以看到jwt 的是会获取这个头信息的,也就是说这个头是我们自定义的,需要在跨域中进行配置,当然这只是一个小细节,稍微注意下就行了。
好了,自此我的问题描述完毕,都看到这里了我想可能会对你有帮助吧,麻烦点个赞再走呗~~~