【JavaWeb】过滤器与拦截器-Spring Security
JavaWeb进阶学习路线 》》点击免费获取
一、Spring的过滤器和拦截器
我们在进行 Web 应用开发时,时常需要对请求进行拦截或处理,故 Spring 为我们提供了过滤器和拦截器来应对这种情况。那么两者之间有什么不同呢?本文将详细讲解两者的区别和对应的使用场景。spring security原理
过滤器和拦截器 底层实现方式大不相同:过滤器是基于函数回调的,拦截器则是基于Java的反射机制(动态代理)实现的。过滤器(Filter):它依赖于servlet容器。
1.1 什么是过滤器
过滤器(Filter),是 Servlet 规范规定的,在 Servlet 前执行的。用于拦截和处理 HTTP 请求和响应,可用于身份认证、授权、日志记录和设置字符集(CharacterEncodingFilter)等场景
过滤器位于整个请求处理流程的最前端,因此在请求到达 Controller 层前,都会先被过滤器处理。
过滤器可以拦截多个请求或响应,一个请求或响应也可以被多个过滤器拦截。
过滤器是基于函数回调实现
Filter 的生命周期对应的三个关键方法:
方法 说明
init() 当请求发起时,会调用 init() 方法初始化 Filter 实例,仅初始化一次。若需要设置初始化参数的时可调用该方法。
doFilter() 拦截要执行的请求,对请求和响应进行处理。
destroy() 请求结束时调用该方法销毁 Filter 的实例。
1.2 @WebFilter实现过滤器
@WebFilter + @ServletComponentScan
在自定义 Filter 类上,添加 @WebFilter 注解,
启动类上增加
@ServletComponentScan("com.athena.common.filter") 注解,
参数就是 Filter 所在的包路径。
创建 Filter 处理类,实现javax.servlet.Filter接口,加上@WebFilter注解配置拦截 Url,但是不能指定过滤器执行顺序,也可通过web.xml配置。
@WebFilter(urlPatterns = "/*")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 用于完成 Filter 的初始化
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
System.out.println("过滤器已经拦截成功!!!");
// 执行该方法之前,即对用户请求进行预处理;执行该方法之后,即对服务器响应进行后处理。
chain.doFilter(request,response);
}
@Override
public void destroy() {
// 用于 Filter 销毁前,完成某些资源的回收;
Filter.super.destroy();
}
}
在启动类添加注解@ServletComponentScan ,让 Spring 可以扫描到。
@SpringBootApplication
@ServletComponentScan
public class MyFilterDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MyFilterDemoApplication.class, args);
}
}
详细的实例:
@Order(1)
@WebFilter(urlPatterns = "/*", filterName = "MyFilter")
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("MyFilter");
// 要继续处理请求,必须添加 filterChain.doFilter()
filterChain.doFilter(servletRequest,servletResponse);
}
}
@SpringBootApplication
@ServletComponentScan("com.athena.common.filter")
public class FilterDemoApplication {
public static void main(String[] args) {
SpringApplication.run(FilterDemoApplication.class, args);
}
}
优点:配置简单、集中,支持 Filter 的顺序,支持对 Filter 匹配指定 URL。
1.3 @Component 实现过滤器
创建 Filter 处理类,实现javax.servlet.Filter接口,加@Component注解。
可以使用@Order注解保证过滤器执行顺序,不加则按照类名排序。
过滤器不能指定拦截的url , 只能默认拦截全部。
@Component
@Order(1)
public class MyComponentFilter1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("我是过滤器1已经拦截成功!!!");
chain.doFilter(request,response);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
@Component
@Order(2)
public class MyComponentFilter2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
System.out.println("我是过滤器2已经拦截成功!!!");
chain.doFilter(request,response);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
注意:
如果 Filter 要使请求被继续处理,就一定要调用 filterChain.doFilter();
这里我们可以通过 @Order 控制过滤器的级别,值越小级别越高越先执行。
优缺点:
优点:注解方式配置简单,支持自定义 Filter 顺序。
缺点:只能拦截所有 URL,不能通过配置去拦截指定的 URL。
1.4 @Configuration实现过滤器
定义一个过滤器
public class LogCostFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
long start = System.currentTimeMillis();
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("Execute cost="+(System.currentTimeMillis()-start));
}
}
通过 JavaConfig 配置方式,注册 Filter
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean registerMyFilter(){
FilterRegistrationBean<MyFilter> bean = new FilterRegistrationBean<>();
bean.setOrder(1);
bean.setFilter(new LogCostFilter());
// 匹配"/hello/"下面的所有url
bean.addUrlPatterns("/hello/*");
return bean;
}
@Bean
public FilterRegistrationBean registerMyAnotherFilter(){
FilterRegistrationBean<MyAnotherFilter> bean = new FilterRegistrationBean<>();
bean.setOrder(2);
bean.setFilter(new MyAnotherFilter());
// 匹配所有url
bean.addUrlPatterns("/*");
return bean;
}
}
优点:功能强大,配置灵活。只需要把每个自定义的 Filter 声明成 Bean 交给 Spring 管理即可,还可以设置匹配的 URL 、指定 Filter 的先后顺序。
1.4 什么是拦截器
拦截器(Interceptor)是一个 Spring 组件,并由 Spring 容器管理,并不依赖 Tomcat 等容器,是可以单独使用的。不仅能应用在 web 程序中,也可以用于 Application、Swing 等程序中。
自定义拦截器只需要实现接口 HandlerInterceptor 即可。
过滤器是servlet接受到请求之后,在调用Servlet之前执行的,拦截器是在程序调用Servlet之后,在controller调用之前运行的,过滤器只能操作response 和request,通常用于字符编码、跨域等问题进行过滤;
拦截器(Interceptor)和Servlet无关,由Spring框架实现。可用于身份认证、授权、日志记录、预先设置数据以及统计方法的执行效率等。一般基于 Java 的反射机制实现,属于AOP的一种运用。
Interceptor 作用
- 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算 PV(Page View)等;
- 权限检查:如登录检测,进入处理器检测是否登录;
- 性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。(反向代理,如 Apache 也可以自动记录)
- 通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的即可使用拦截器实现。
目前了解的 Spring 中的拦截器有:
- HandlerInterceptor
- MethodInterceptor
1.5 HandlerInterceptor 拦截器
HandlerInterceptor 类似 Filter,拦截的是请求地址 ,但提供更精细的的控制能力,这里注意下必须过DispatcherServlet 的请求才会被拦截。
它允许你在请求处理前、处理后以及视图渲染完成前执行自定义逻辑,可以用来对请求地址做一些认证授权、预处理,也可以计算一个请求的响应时间等,还可以处理跨域(CORS)问题。
简单的执行流程描述:
- 请求到达 DispatcherServlet,然后发送至 Interceptor,执行 preHandler;
- 请求到达 Controller,请求结束后,执行 postHandler。
创建 Interceptor 类,实现HandlerInterceptor接口,重写 3 个方法,加@Component注解。
代码实例:
@Component
public class MyHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//请求开始时间
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
// 一个简单的安全校验,要求请求头中必须包含 req-name : yihuihui
String header = request.getHeader("req-name");
if ("yihuihui".equals(header)) {
return true;
}
log.info("请求头错误: {}", header);
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long)request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
// 统计耗时
long executeTime = endTime - startTime;
System.out.println("executeTime : " + executeTime + "ms");
log.info("执行完毕!");
response.setHeader("res", "postHandler");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
@Order(2)
@Slf4j
public class SecurityInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 一个简单的安全校验,要求请求头中必须包含 req-name : yihuihui
String header = request.getHeader("req-name");
if ("yihuihui".equals(header)) {
return true;
}
log.info("请求头错误: {}", header);
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("执行完毕!");
response.setHeader("res", "postHandler");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("回收");
}
}
配置拦截器,实现WebMvcConfigurer接口,加@Configuration注解并重写addInterceptors方法。
@Configuration
public class MyWebConfigurer implements WebMvcConfigurer {
@Resource
private MyHandlerInterceptor myHandlerInterceptor;
@Autowired
private SecurityInterceptor demoInterceptor1;
@Autowired
private DemoInterceptor2 demoInterceptor2;
// 这个方法是用来配置静态资源的,比如html,js,css,等等
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
// 这个方法用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效
@Override
public void addInterceptors(InterceptorRegistry registry) {
//addPathPatterns("/**") 表示拦截所有的请求,
//excludePathPatterns("/login", "/register") 表示除了登陆与注册之外,因为登陆注册不需要登陆也可以访问
List<String> patterns = new ArrayList<>();
patterns.add("/test/handlerInterceptor");
registry.addInterceptor(myHandlerInterceptor)
.addPathPatterns(patterns) // 需要拦截的请求
.excludePathPatterns(); // 不需要拦截的请求
registry.addInterceptor(demoInterceptor1)
.addPathPatterns("/**")
.excludePathPatterns("/login", "/register");
registry.addInterceptor(demoInterceptor2)
.addPathPatterns("/**")
.excludePathPatterns("/login", "/register");
System.out.println("************addInterceptors**********");
}
}
注意:Spring项目可通过使用mvc:interceptors标签来声明需要加入到 SpringMVC 拦截器链中的拦截器。
1,preHandle
在 handler 方法执行之前(简单理解为 Controller 提供的服务调用之前)会被触发,如果返回 ture,表示拦截通过,可以执行;若果返回 false,表示不允许往后走。
因此在这里,通常可以用来做安全校验、用户身份处理等操作
注意,无论是拦截器,还是 Filter,在使用 Request 中的请求流的时候,要警惕,通常请求参数流的读取是一次性的,如果在这里实现了一个请求参数日志输出,把请求流的数据读出来了,但是又没有写回去,就会导致请求参数丢失了。
2,postHandler
这个是在 handler 方法执行之后,视图渲染之前被回调,简单来说,我们在这个时候,是可以操作 ModelAndView,往里面添加一下信息,并能被视图解析渲染的。
当然鉴于现在前后端分离的趋势,这个实际上用得也不多了。
3,afterCompletion
该方法将在整个请求结束之后,也就是在 DispatcherServlet 渲染了对应的视图之后执行。此方法主要用来进行资源清理。
1.6 MethodInterceptor 拦截器
MethodInterceptor 是 AOP 中的拦截器,它拦截的目标是方法,可以不是 Controller 中的方法。
在对一些普通的方法上的拦截可以使用该拦截器,这是 HandlerInterceptor 无法实现的。
可用来进行方法级别的身份认证、授权以及日志记录等,也可基于自定义注解实现一些通用的方法增强功能。
MethodInterceptor 是基于 AOP 实现的,所以根据不同的代理有多种实现方式。
这里我将介绍通过BeanNameAutoProxyCreator自动代理实现拦截。该类是基于 Bean 名称的自动代理,可以针对特定的Bean进行个性化的 AOP 配置。
public interface UserService {
public String getUser();
}
@Component
public class UserServiceImpl implements UserService{
@Override
public String getUser() {
return "我是福星";
}
}
创建 Interceptor 类,实现MethodInterceptor接口,重写invoke方法,加@Component注解。
@Component
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("进入拦截,方法执行前,拦截方法是:" + invocation.getMethod().getName());
Object result = invocation.proceed();
System.out.println("方法执行后");
return result;
}
}
配置自动代理,加@Configuration注解并创建自动代理BeanNameAutoProxyCreator。
@Configuration
public class MyMethodConfigurer {
@Resource
private MyMethodInterceptor myMethodInterceptor;
@Bean
public BeanNameAutoProxyCreator beanNameAutoProxyCreator() {
// 使用BeanNameAutoProxyCreator来创建代理
BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
// 指定一组需要自动代理的Bean名称,Bean名称可以使用*通配符
beanNameAutoProxyCreator.setBeanNames("user*");
//设置拦截器名称,这些拦截器是有先后顺序的
beanNameAutoProxyCreator.setInterceptorNames("myMethodInterceptor");
return beanNameAutoProxyCreator;
}
}
1.7 Filter 和 Interceprtor
在 Spring MVC 中,Interceprtor 与 Filter 两者的应用场景好像差不多,最大的区别可能是前者属于 Spring 的组件,而后者则是 Servlert 三剑客中的一个,本质的区别在于两者发生的时机不一致。
Filter 和 Interceprtor 对比:
Filter:在执行 Servlet#service 方法之前,会执行过滤器;执行完毕之后也会经过过滤器;
Filter 操作 Request、Response。
Interceptor:对会话进行拦截,可以在调用 Handler 方法之前、视图渲染之前、方法返回之前,三个时机触发回调。
Interceptor 操作 Request、Response、handler、modelAndView、exception。
Filter 和 Interceprtor 的执行顺序:
Filter 处理 -> Interceptor 前置 -> controller -> Interceptor 处理中 -> Interceptor 处理后 -> Filter 处理后.
过滤器基于函数回调方式实现,拦截器基于 Java 反射机制实现。
实际开发中,拦截器的应用场景会比过滤器要更多:
拦截器的应用场景:权限控制,日志打印,参数校验
过滤器的应用场景:跨域问题解决,编码转换
1.7 小结
过滤器一般用于对 Servlet 请求和响应进行通用性的处理,通常关注请求和响应内容,而不涉及具体的业务逻辑。而拦截器用于对 SpringMVC 的请求和响应进行特定的业务处理,通常与控制器层的请求处理有关。
不论是过滤器和拦截器,都可以有多个。执行顺序上拦截器是由配置中的顺序决定,而过滤器可通过@Component+@Order决定,也可由web.xml文件中的配置顺序决定。
总的来说,拦截器的使用更加灵活,Filter 能做的事情,拦截器也能做。Filter 一般用于对 URL 请求做编码处理、过滤无用参数、安全校验(比如登陆态校验),如果涉及业务逻辑上的,还是建议用拦截器。
- 多个过滤器的执行顺序跟定义的先后关系有关。通过@Order控制过滤器的级别,值越小级别越高越先执行。
- 多个拦截器执行顺序跟注册先后顺序有关。
registry.addInterceptor(demoInterceptor1).addPathPatterns("/**")
.excludePathPatterns("/login", "/register");
registry.addInterceptor(demoInterceptor2).addPathPatterns("/**")
.excludePathPatterns("/login", "/register");
拦截器默认的执行顺序,就是它的注册顺序,也可以通过Order手动设置控制,值越小越先执行。
二、SpringSecurity
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
- 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
- 授权:经过认证后判断当前用户是否有权限进行某个操作
2.1 基本原理
SpringSecurity的基本原理就是应用了Tomcat容器的Filter,其的实现原理也就是类似于Tomcat本身的ApplicationFilterChain,也就是Filter执行链。
ApplicationFilterChain本身就是先执行所有的filters,执行完成后,其就会执行当前请求的Servlet,对于SpringMVC来说,就是DispatchSevelt:
public final class ApplicationFilterChain implements FilterChain {
........
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
...........
public ApplicationFilterChain() {
}
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
..........
} else {
this.internalDoFilter(request, response);
}
}
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.pos < this.n) {
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
try {
Filter filter = filterConfig.getFilter();
.........
} else {
filter.doFilter(request, response, this);
}
.........
} else {
try {
.........
} else {
this.servlet.service(request, response);
}
} ........
}
}
}
这里也就是说,先执行Tomcat自身已有的Filter,然后再交给SpringSecurity定义的FilterChainProxy,然后其再去执行SpringSecurity用于认证、授权管理的各种Filter。
这个就是SpringSecurity的核心原理。
Spring Security就是一条过滤器链,如果你登录了,那么会有过滤器将你的认证信息解析出来并放到Security的上下文中,这样其他过滤器就通过这个认证信息来鉴权。
认引入spring-boot-starter-security后其会默认加入的Filter
2.2 Spring Security核心功能
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,实现权限安全控制。
Spring Security的核心功能包括:
- Authentication(认证):验证用户是他们声明的身份。
- Authorization(授权):访问控制过程,决定用户是否有权限进行某个操作。
- SessionManagement(会话管理):管理用户会话,确保用户在多次请求间保持登录状态。
- Cryptography(加密):安全的密码编码。
- Web Security(Web安全性):提供web请求的安全性,如CSRF保护。
- Method Security(方法安全性):支持安全代理,可以用注解的方式保护方法。
补充:
1 什么是认证
在互联网中,我们每天都会使用到各种各样的APP和网站,
在使用过程中通常还会遇到需要注册登录的情况,
输入你的用户名和密码才能正常使用,
也就是说成为这个应用的合法身份才可以访问应用的资源,
这个过程就是认证。认证是为了保护系统的隐私数据与资源,
用户的身份合法方可访问该系统的资源。
当然认证的方式有很多,常见的账号密码登录,手机验证码登录,指纹登录,刷脸登录等等。
简单说: 认证就是让系统知道我们是谁。
2 什么是授权
认证是为了保护身份的合法性,授权则是为了更细粒度的对数据进行划分,
授权是在认证通过的前提下发生的。控制不同的用户能够访问不同的资源。
2.3 常见的认证方式
2.3.1 Cookie-Session认证
早期互联网以 web 为主,客户端是浏览器,所以 Cookie-Session 方式最那时候最常用的方式,直到现在,一些 web 网站依然用这种方式做认证:
认证过程大致如下:
A. 用户输入用户名、密码或者用短信验证码方式登录系统;
B. 服务端验证后,创建一个 Session 记录用户登录信息 ,并且将 SessionID 存到 cookie,响应回浏览器;
C. 下次客户端再发起请求,自动带上 cookie 信息,服务端通过 cookie 获取 Session 信息进行校验;
弊端
1,只能在 web 场景下使用,如果是 APP 中,不能使用 cookie 的情况下就不能用了;
即使能在 web 场景下使用,也要考虑跨域问题,因为 cookie 不能跨域;(域名或者ip一致,端口号一致,协议要一致)
2,cookie 存在 CSRF(跨站请求伪造)的风险;
3,如果是分布式服务,需要考虑 Session 同步(同步)问题;
4,session-cookie机制是有状态的方式(后端保存主题的用户信息-浪费后端服务器内存)
2.3.2 jwt令牌无状态认证
JSON Web Token(JWT-字符串)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。JSON Web Token(JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。该token被设计为紧凑且安全的,特别适用于前后端无状态认证的场景。
**认证过程: **
A. 依然是用户登录系统;
B. 服务端验证,并通过指定的算法生成令牌返回给客户端;
C. 客户端(浏览器)拿到返回的 Token,存储到 local storage(关闭浏览器后,token不会消失)/session Storate(关闭浏览器后,token会消失)/Cookie中;
D. 下次客户端再次发起请求,将 Token 附加到 header 中;
E. 服务端获取 header 中的 Token ,通过相同的算法对 Token 进行验证,如果验证结果相同,则说明这个请求是正常的,没有被篡改。这个过程可以完全不涉及到查询 Redis 或其他存储;
优点
A. 使用 json 作为数据传输,有广泛的通用型,并且体积小,便于传输;
B. 不需要在服务器端保存相关信息,节省内存资源的开销;
C. jwt 载荷部分可以存储业务相关的信息(非敏感的),例如用户信息、角色等;
JWT介绍
1,头部(Header)(非敏感)
-----------------------------
头部用于描述关于该JWT的最基本的信息,例如数据类型以及签名所用的算法等,
本质是一个JSON格式对象;
举例说明
{"typ":"JWT","alg":"HS256"}
解释:在头部指明了签名算法是HS256算法,整个JSON对象被BASE64编码形成JWT头部字符串信息;
BASE64编码后的字符串:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2,载荷(playload)(非敏感数据)
-------------------------------
载荷就是存放有效信息的地方,该部分的信息是可以自定义的;
载荷payload格式:{"sub":"1234567890","name":"John Doe","admin":true}
载荷相关的JSON对象经过BASE64编码形成JWT第二部分:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG7CoERvZSIsImFkbWluIjp0cnVlfQ==
3,签证(signature)
--------------------------------
jwt的第三部分是一个签证信息,
这个签证信息由三部分组成:
签名算法( header (base64后的).payload (base64后的).secret)
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,
然后通过header中声明的加密方式进行加盐secret秘钥组合加密,
然后就构成了jwt的第三部分:TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
最后将这三部分用●连接成一个完整的字符串,
构成了最终的jwt:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG7CoERvZSIsImFkbWluIjp0cnVlfQ==.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
使用JWT
导入jwt依赖
-----------------------
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
生成JWT令牌
----------------------
public void testGenerate(){
String compact = Jwts.builder()
.setId(UUID.randomUUID().toString())//设置唯一标识
.setSubject("JRZS") //设置主题
.claim("name", "nineclock") //自定义信息
.claim("age", 88) //自定义信息
.setExpiration(new Date()) //设置过期时间
.setIssuedAt(new Date()) //令牌签发时间
.signWith(SignatureAlgorithm.HS256, "hhH")//签名算法, 秘钥
.compact();
System.out.println(compact);
}
JWT令牌校验
----------------------
public void testVerify(){
String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5MzljNjU4MC0yMTQyLTRlOWEtYjcxOC0yNzlmNzRhODVmNDMiLCJzdWIiOiJOSU5FQ0xPQ0siLCJuYW1lIjoibmluZWNsb2NrIiwiYWdlIjo4OCwiaWF0IjoxNjE3MDMxMjUxfQ.J-4kjEgyn-Gkh0ZuivUCevrzDXt0K9bAyF76rn1BfUs";
Claims claims = Jwts.parser().setSigningKey("hhh").parseClaimsJws(jwt).getBody();
System.out.println(claims);
}
JWT在前端保存方案
后端基于JWT生成的Token信息在前端有如下保存方式:
1,LocalStorage(浏览器关闭后,token不会消失),
2,SessionStorage(浏览器关闭后,token会消失)
3,cookie
4,页面中
5,其它
----------
以LocalStorage为例:
-------------------------
<script>
//保存信息
localStorage.setItem("token", "xxx");
//获取信息
alert(localStorage.getItem("token"))
//删除信息
localStorage.removeItem("token");
</script>
2.4 Spring Security工作原理
本质上,Spring Security的实现原理很简单,就是提供了一个用于安全验证的Filter。假如我们自己实现一个简化版的Filter,它的大概逻辑应该是这样的:
- 从HTTP请求中获取用户名和密码,来源包括标准的Basic Auth HTTP Header,表单字段或者cookie等等。
- 身份认证,也就是校验用户名和密码。
- 认证通过后,需要检查当前登录的用户有没有访问当前HTTP请求的权限,也就是鉴权逻辑。
- 权限校验也通过后,就继续执行其它Filter,所有Filter都通过后,进入Servlet,最终到达具体的Controller。
在安全领域,由于攻防手段的多样性和认证鉴权方式的复杂性,将所有功能都放在一个Filter中会导致该Filter迅速演变为一个庞大而复杂的类。因此,在实际应用场景中,我们常常将这个庞大的Filter拆分成多个小Filter,并将它们链接在一起。每个Filter都只负责特定领域的功能,比如CsrfFilter,AuthenticationFilter,AuthorizationFilter等。
这种概念被称为FilterChain,实际上JarkataEE规范也有相识的概念。通过使用FilterChain,你就可以以插拔的方式添加或移除特定功能的Filter,而无需改动现有的代码。
2.5 FilterChain介绍
Spring Security通过DefaultSecurityFilterChain类来完成安全相关的功能,而该类本身又由其它Filter组成。默认情况下,Spring Security Starter引入了15个Filter,如下图所示:
下面我们简要介绍下其中几个重要的Filter:
- CsrfFilter:这个Filter用于防止跨站点请求伪造攻击,这也是导致所有POST请求都失败的原因。基于Token验证的API服务可以选择关闭CsrfFilter,而一般Web页面需要开启。
- BasicAuthenticationFilter:支持HTTP的标准Basic Auth的身份验证模块。
- UsernamePasswordAuthenticationFilter:支持Form表单形式的身份验证模块。
- DefaultLoginPageGeneratingFilter和DefaultLogoutPageGeneratingFilter:用于自动生成登录页面和注销页面。
- AuthorizationFilter:这个Filter负责授权模块。值得注意的是,在老版本中鉴权模块是FilterSecurityInterceptor.
过滤器 | 作用 |
---|---|
WebAsyncManagerIntegrationFilter | 将WebAsyncManger与SpringSecurity上下文进行集成 |
SecurityContextPersistenceFilter | 在处理请求之前, 将安全信息加载到SecurityContextHolder中 |
HeaderWriterFilter | 处理头信息假如响应中 |
CsrfFilter | 处理CSRF攻击 |
LogoutFilter | 处理注销登录 |
UsernamePasswordAuthenticationFilter | 处理表单登录 |
DefaultLoginPageGeneratingFilter | 配置默认登录页面 |
DefaultLogoutPageGeneratingFilter | 配置默认注销页面 |
BasicAuthenticationFilter | 处理HttpBasic登录 |
RequestCacheAwareFilter | 处理请求缓存 |
SecurityContextHolderAwareRequestFilter | 包装原始请求 |
AnonymousAuthenticationFilter | 配置匿名认证 |
SessionManagementFilter | 处理session并发问题 |
ExceptionTranslationFilter | 处理认证/授权中的异常 |
FilterSecurityInterceptor | 处理授权相关 |
下图是主要的过滤器
这些Filter构成了Spring Security的核心功能,通过它们,我们可以实现身份验证、授权、防护等安全特性。根据应用的需求,我们可以选择启用或禁用特定的Filter,以定制和优化安全策略。
我这一副架构图(图中蓝色和橘红色的部分代表Security Security)。从图中可以看出,Spring Security框架通过DelegatingFilterProxy建立起了Servlet容器和Spring容器的链接,FilterChainProxy基于匹配规则(比如URL匹配),决定使用哪个SecurityFilterChain。而SecurityFilterChain又由零到多个Filter组成,这些Filter完成实际的功能。
DefaultSecurityFilterChain类实现了SecurityFilterChain接口,我们打开这个接口的源码,会发现它只有两个方法,matches用于匹配特定的Http请求(比如特定规则的URL),getFilters用于获取可用的所有Security Filter。
public interface SecurityFilterChain {
boolean matches(HttpServletRequest request);
// 规则匹配
List getFilters () ;
// 该FilterChain下的所有Security Filter
}
2.6 SpringSecurityConfig
创建SpringSecurityConfig配置类,继承WebSecurityConfigurerAdapter,并且该类需要加上@EnableWebSecurity注解,该类里面通常要写3个方法:
忽略某些配置的方法
public void configure(WebSecurity web) throws Exception {}
配置对应地址拦截请求的方法,例如拦截地址、关闭csrf、
protected void configure(HttpSecurity http) throws Exception {}
授权用户,比如创建某些账号,某些账号就可以登录了
protected void configure(AuthenticationManagerBuilder auth) throws Exception {}
例如:
@Component
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* UserServiceImpl是UserDetailsService的实现类,也就是写的认证类。
*/
@Autowired
private IUserService userService;
/***
* 忽略安全过滤
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//忽略相关地址
web.ignoring().antMatchers("/images/**");
web.ignoring().antMatchers("/js/**");
web.ignoring().antMatchers("/login.html");
web.ignoring().antMatchers("/error.html");
}
/***
* 请求拦截配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//拦截规则配置
http.authorizeRequests()
//ADMIN角色可以访问/pages/下的所有文件
.antMatchers("/pages/**").access("hasRole('ADMIN')")
//USER角色可以访问/jsp/下的所有文件
.antMatchers("/jsp/**").access("hasRole('USER')")
//指定登录页和处理登录的地址
//.and().formLogin().loginPage("/login.html").loginProcessingUrl("/login")
//指定登出页和登出后让session无效
//.and().logout().logoutUrl("/logout").invalidateHttpSession(true);
//登录相关配置
http.formLogin().loginPage("/login.html") //指定登录地址
.loginProcessingUrl("/login") //指定处理登录的请求地址
.defaultSuccessUrl("/success.html",true); //登录成功后总是跳转到/admin/index.html页面
//登出配置
http.logout().logoutUrl("/logout").invalidateHttpSession(true); //登出地址为/logout,并且登出后销毁session
//设置用户只允许在一处登录,在其他地方登录则挤掉已登录用户,被挤掉的已登录用户则需要返回/login.html重新登录
http.sessionManagement().maximumSessions(1).expiredUrl("/login.html");
//关闭CSRF安全策略
http.csrf().disable();
/允许跳转显示iframe
http.headers().frameOptions().disable();
//异常处理,例如403
http.exceptionHandling().accessDeniedPage("/error.html");
//只允许一个用户登录,如果同一个账户两次登录,那么第一个账户将被踢下线,跳转到登录页面
http.sessionManagement().maximumSessions(1).expiredUrl("/login.html");
}
/***
* 创建用户并授权
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//方式1:创建一个用户存在内存中,账号是admin,密码是123456,角色是ROLE_ADMIN
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
auth.inMemoryAuthentication().withUser("user").password("user").roles("USER");
//方式2:自定义认证类后注册自定义认证类
// auth.userDetailsService(userService);
}
}
技术实现
技术 概述
------------------------------------------------
1,Apache Shiro Apache旗下的一款安全框架
2,SpringSecurity Spring家族的一部分, Spring体系中提供的安全框架,
包含认证、授权两个大的部分
3,CAS CAS是一个单点登录(SSO)服务,开始是由耶鲁大学的一个组织开发,
后来归到apereo去管
4,自行实现 自行通过业务代码实现, 实现繁琐, 代码量大
三、SpringSecurity实例
3.1 基本用法
3.1.1 引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.7</version>
<relativePath/>
</parent>
<dependencies>
<!-- web起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springBoot整合Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
3.1.2 定义Controller
@RestController
public class UserController {
@GetMapping("/hello")
public String hello(){
return "hello security";
}
@GetMapping("/say")
public String say(){
return "say security";
}
@GetMapping("/register")
public String register(){
return "register security";
}
}
3.1.3 引导类
@SpringBootApplication
public class MySecurityApplication {
public static void main(String[] args) {
SpringApplication.run(MySecurityApplication.class,args);
}
}
访问: http://localhost:8080/hello
会自动拦截,并跳转到登录页面(SpringSecurity提供),登录之后才可以访问; 而登录的用户名和密码都是SpringSecurity中内置的默认的用户名密码, 用户名为user , 密码为控制台输出的一段随机数;
内置登录页面:
可以在默认配置文件中,指定账户+密码
# 我们也可在配置文件中配置用户名和密码,实际开发中密码不应明文配置
spring.security.user.name=user
spring.security.user.password=6666
3.2 自定义认证用法
上述的入门程序中, 用户名密码是框架默认帮我们生成的, 我们并没有指定, 如果我们想指定系统的访问用户名及密码, 可以通过配置的形式声明 , 声明一个 UserDetailsService
类型的 Bean。
我们最终实现的如下流程:
3.2.1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.2.2 定义 controller
@RestController
public class UserController {
@GetMapping("/hello")
public String hello(){
return "hello security";
}
@GetMapping("/say")
public String say(){
return "say security";
}
@GetMapping("/register")
public String register(){
return "register security";
}
}
3.2.3 SpringSecurity自定义认证配置
@Configuration
@EnableWebSecurity//开启web安全设置生效
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//使用该方法创建用户,并为用户赋予权限
/**
* 构建认证服务,并将对象注入spring IOC容器,用户登录时,会调用该服务进行用户合法信息认证
*/
@Bean
protected UserDetailsService userDetailsService() {
//从内存获取用户认证信息的服务类(了解)后期用户的信息要从表中获取
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
//创建用户,这里自定义用户,以后通过数据库进行用户创建
UserDetails u1= User.withUsername("hhh")
.password("{noop}123456")//{noop}-->表示no operation 就是直接明文比对
.authorities("P1","ROLE_SELECT")//用户的权限信息
.build();
UserDetails u2=User.withUsername("aaa")
.password("{noop}123456")
.authorities("P2","ROLE_ADMIN")
.build();
//构建用户
inMemoryUserDetailsManager.createUser(u1);
inMemoryUserDetailsManager.createUser(u2);
return inMemoryUserDetailsManager;
}
}
补充说明:
说明:
1.在userDetailsService()方法中
返回了一个UserDetailsService对象给spring容器管理,当用户发生登录认证行为时,
Spring Security底层会自动调用UserDetailsService类型bean提供的用户信息进行合法比对,
如果比对成功则资源放行,否则就认证失败;
2.当前暂时使用InMemoryUserDetailsManager实现类,
后续我们也可手动实现UserDetailsService接口,做最大程度的自定义;
3.2.4 SpringSecurity自定义授权配置
给每个路径分配权限 ,访问某一个路径时,需要访问某一个路径时,需要进行用户认证,只有这个用户拥有访问这个路径的权限时,才能访问这个路径
permitAll()这个方法不用进行用户认证,可以直接访问
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("itcast").password("{noop}123456").authorities("P1","ROLE_ADMIN").build());
inMemoryUserDetailsManager.createUser(User.withUsername("itheima").password("{noop}123456").authorities("O1","ROLE_SELLER").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//开启默认form表单登录方式
.and()
.logout()//登出用默认的路径登出 /logout
.permitAll()//允许所有的用户访问登录或者登出的路径
.and()
.csrf().disable()//启用CSRF,防止CSRF攻击
.authorizeRequests()//授权方法,该方法后有若干子方法进行不同的授权规则处理
//允许所有账户都可访问(不登录即可访问),同时可指定多个路径
.antMatchers("/register").permitAll()//允许所有的用户访问
.antMatchers("/hello").hasAuthority("P1") //具有P5权限才可以访问
.antMatchers("/say").hasRole("SELECT") //具有ROLE_ADMIN 角色才可以访问,会自动加上ROLE_
.antMatchers("/aa","/bb").hasAnyAuthority("P1","ROLE_SELECT")//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasAnyRole("SELECT","ADMIN)//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasIpAddress("192.168.xxx.xxx")//必须是192.168.地址才能访问
.antMatchers("/aa","/bb").denyAll()//任何用户都不可以访问
.anyRequest().authenticated(); //除了上边配置的请求资源,其它资源都必须授权才能访问
}
}
补充:
CSRF(Cross-site request forgery)跨站请求伪造,也被称为"One Click Attack"或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。
3.2.5 使用注解自定义授权
1,开启SpringSecurity注解支持
@EnableGlobalMethodSecurity(prePostEnabled = true)
------------------------------
@Configuration
@EnableWebSecurity//开启web安全设置生效
//开启SpringSecurity相关注解支持
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
2,添加注解
@RestController
public class UserController {
//拥有ROLE_ADMIN权限的用户才能访问此接口
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/hello")
public String hello(){
return "hello security";
}
//拥有ROLE_SELECT权限的用户才能访问此接口
@PreAuthorize("hasRole('SELECT')")
@GetMapping("/say")
public String say(){
return "say security";
}
@PermitAll//任何用户都可以访问此接口,不需要进行认证
@GetMapping("/register")
public String register(){
return "register security";
}
}
注意:
使用@PreAuthorize,需要开启全局方法授权开关,加上注解@EnableGlobalMethodSecurity(prePostEnabled=true)
3.2.6 密码使用加密
上文中密码采用的是明文的,不安全 ,现在调整为密文方式。
在配置类 SecurityConfig 中配置Bean(加密类型):
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//配置密码加密器 ;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
//配置认证信息 , 密码使用BCryptPasswordEncoder加密 ;
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("muziteng")
.password("$2a$10$qcKkkvsoClF9tO8c9wlR/ebgU8VM39GP5ZUdsts.XSPDmE40l.BP2")
.authorities("P1","ROLE_ADMIN").build());
inMemoryUserDetailsManager.createUser(User.withUsername("lili")
.password("$2a$10$qcKkkvsoClF9tO8c9wlR/ebgU8VM39GP5ZUdsts.XSPDmE40l.BP2")
.authorities("O1","ROLE_SELLER").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//开启默认form表单登录方式
.and()
.logout()//登出用默认的路径登出 /logout
.permitAll()//允许所有的用户访问登录或者登出的路径
.and()
.csrf().disable()//启用CSRF,防止CSRF攻击
.authorizeRequests()//授权方法,该方法后有若干子方法进行不同的授权规则处理
//允许所有账户都可访问(不登录即可访问),同时可指定多个路径
.antMatchers("/register").permitAll()//允许所有的用户访问
.antMatchers("/hello").hasAuthority("P1") //具有P5权限才可以访问
.antMatchers("/say").hasRole("SELECT") //具有ROLE_ADMIN 角色才可以访问,会自动加上ROLE_
.antMatchers("/aa","/bb").hasAnyAuthority("P1","ROLE_SELECT")//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasAnyRole("SELECT","ADMIN)//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasIpAddress("192.168.xxx.xxx")//必须是192.168.地址才能访问
.antMatchers("/aa","/bb").denyAll()//任何用户都不可以访问
.anyRequest().authenticated(); //除了上边配置的请求资源,其它资源都必须授权才能访问
}
}
3.2.7 账户信息连接DB
上文中,用户名/密码直接通过程序硬编码,不够灵活 ,扩展性也非常差。线下一般使用Mysql方式存储账户信息,现在我们就使用DB动态验证账户信息。
创建简单的DB表格
create database security_demo default charset=utf8mb4;
use security_demo;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(100) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
`roles` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `tb_user` VALUES (1, 'itcast', '$2a$10$f43iK9zKD9unmgLao1jqI.VluZ.Rr/XijizVEA73HeOu9xswaUBXC', 'ROLE_ADMIN,P1');
INSERT INTO `tb_user` VALUES (2, 'itheima', '$2a$10$f43iK9zKD9unmgLao1jqI.VluZ.Rr/XijizVEA73HeOu9xswaUBXC', 'ROLE_SELLER,O1');
自定义UserDetailsService
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private TbUserMapper tbUserMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
TbUser user = tbUserMapper.findByUserName(userName);
if (user==null) {
throw new UsernameNotFoundException("用户不存在");
}
//构建认证明细对象
//获取用户权限
List<GrantedAuthority> list = AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles());
User user1 = new User(user.getUsername(),user.getPassword(),list);
return user1;
//TODO 查询数据库进行认证逻辑
//模拟查询得到的用户信息
//String userName="user";
// String password="user";
//构建角色集合,用户所拥有的角色
//List<SimpleGrantedAuthority> roleList = new ArrayList<>();
// roleList.add(new SimpleGrantedAuthority("ROLE_USER"));
//处理用户对象封装成UserDetails
// UserDetails user=new User(userName,"{noop}"+password,roleList);
/**
* username 用户名
* password 密码
* enabled 帐户是否可用
* accountNonExpired 帐户是否过期
* credentialsNonExpired 认证是否过期
* accountNonLocked 帐户是否锁定
* authorities 账户所属角色集合
*/
// User user = new User(userName, "{noop}"+password, true, true, true, true,
}
}
说明:
UserDetails是一个接口,
User是该接口的实现类,封装用户的数据及用户的权限数据, 注意不要导错包 ;
在SecurityConfig中注释掉inMemoryUserDetailsManager bean,并配置加密bean:
因为已经实现了UserDetailsService接口,@Component 在应用启动时候会被加载。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//配置密码加密器 ;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//开启默认form表单登录方式
.and()
.logout()//登出用默认的路径登出 /logout
.permitAll()//允许所有的用户访问登录或者登出的路径
.and()
.csrf().disable()//启用CSRF,防止CSRF攻击
.authorizeRequests()//授权方法,该方法后有若干子方法进行不同的授权规则处理
//允许所有账户都可访问(不登录即可访问),同时可指定多个路径
.antMatchers("/register").permitAll()//允许所有的用户访问
.antMatchers("/hello").hasAuthority("P1") //具有P5权限才可以访问
.antMatchers("/say").hasRole("SELECT") //具有ROLE_ADMIN 角色才可以访问,会自动加上ROLE_
.antMatchers("/aa","/bb").hasAnyAuthority("P1","ROLE_SELECT")//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasAnyRole("SELECT","ADMIN)//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasIpAddress("192.168.xxx.xxx")//必须是192.168.地址才能访问
.antMatchers("/aa","/bb").denyAll()//任何用户都不可以访问
.anyRequest().authenticated(); //除了上边配置的请求资源,其它资源都必须授权才能访问
}
}
3.2 JWT认证
添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
配置JwtConfig
在使用JWT之前,需要配置JwtConfig,例如:
@Configuration
public class JwtConfig {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private int expiration;
public String getSecret() {
return secret;
}
public int getExpiration() {
return expiration;
}
}
其中,jwt.secret是JWT的签名密钥,jwt.expiration是JWT的过期时间(单位为秒)。
实现JwtTokenProvider
@Component
public class JwtTokenProvider {
private final JwtConfig jwtConfig;
@Autowired
public JwtTokenProvider(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
public String generateToken(UserDetails userDetails) {
Date now = new Date();
Date expiration = new Date(now.getTime() + jwtConfig.getExpiration() * 1000);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret())
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = getUserDetails(token);
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public UserDetails getUserDetails(String token) {
String username = getUsername(token);
return new User(username, "", new ArrayList<>());
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtConfig.getSecret()).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
private String getUsername(String token) {
return Jwts.parser().setSigningKey(jwtConfig.getSecret()).parseClaimsJws(token).getBody().getSubject();
}
}
在上面的代码中,generateToken方法用于生成JWT Token,其中包括用户名、颁发时间、过期时间等信息;getAuthentication方法用于根据Token获取用户信息,并将其封装成Authentication对象;getUserDetails方法用于根据Token获取用户详细信息;validateToken方法用于验证Token是否有效;getUsername方法用于根据Token获取用户名。
需要注意的是,以上代码中的UserDetails、User等类需要根据实际情况进行修改。
实现JwtAuthenticationFilter
接下来,需要实现JwtAuthenticationFilter,用于拦截请求并解析JWT Token,例如:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Autowired
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getToken(request);
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
以上代码中的JwtAuthenticationFilter用于拦截请求,并从请求头中获取Token,然后根据Token获取用户信息并设置到SecurityContext中。
配置WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/authenticate").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Controller
// cn.mb.itemdemo.controller.TestController
@GetMapping("/login")
public CommonResult login() {
// 登录校验
UserDetails userDetails = userService.loadUserByUsername("root");
String token = jwtTokenUtil.generateToken(userDetails);
return CommonResult.success(token);
}
以上代码中的SecurityConfig继承自WebSecurityConfigurerAdapter,用于配置Spring Security的行为。在这里,我们将/api/authenticate接口设置为不需要认证,其他接口需要认证。同时,将JwtAuthenticationFilter添加到Spring Security的过滤器链中,用于拦截请求并进行身份认证。
// cn.mb.itemdemo.controller.TestController
@GetMapping("/login")
public CommonResult login() {
// 登录校验
UserDetails userDetails = userService.loadUserByUsername("root");
String token = jwtTokenUtil.generateToken(userDetails);
return CommonResult.success(token);
}
解析token过滤器
// cn.mb.itemdemo.component.CustomTokenFilter
public class CustomTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 获取请求头
String header = request.getHeader(jwtTokenUtil.getTokenHeader());
// 解析token
if (header != null && header.startsWith(jwtTokenUtil.getTokenHead())) {
String token = header.substring(jwtTokenUtil.getTokenHead().length());
// 获取用户名
String username = jwtTokenUtil.getUserNameFromToken(token);
// 每次都重新查询用户及其权限(保证动态权限)
UserDetails userDetails = userService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(token, userDetails)) {
// 将用户信息放入SecurityContextHolder中
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
在Security配置类中将过滤器注入到过滤链中。
protected void configure(HttpSecurity http) throws Exception {
// 自定义token解析器
http.addFilterBefore(customTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
通过上述操作,即可实现JWT认证
其实只要知道token是在过滤链中解析成用户数据即可,后面的事就交给Security来做(一开始我也是懵的不知道怎么结合,其实跟用拦截器判断认证一样)
FilterSecurityInterceptor是过滤器链的最后一个,在执行时就会做鉴权操作
但其依赖AccessDecisionManager.decide方法做实际鉴权操作
且依赖SecurityMetadataSource.getAttributes获取当前资源对应的权限
因此我们需要自定义上述三个对象并注入到Security中
自定义鉴权过滤器:放行、调用鉴权
public class CustomAuthFilter extends AbstractSecurityInterceptor implements Filter {
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final CustomMetadataSource customMetadataSource;
public CustomAuthFilter(IgnoreUrlsConfig ignoreUrlsConfig, CustomMetadataSource customMetadataSource, CustomAccessDecisionManager customAccessDecisionManager) {
this.ignoreUrlsConfig = ignoreUrlsConfig;
this.customMetadataSource = customMetadataSource;
super.setAccessDecisionManager(customAccessDecisionManager);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
// OPTIONS请求直接放行
if (request.getMethod().equals(HttpMethod.OPTIONS.toString())) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
// 白名单请求直接放行
PathMatcher pathMatcher = new AntPathMatcher();
for (String path : ignoreUrlsConfig.getUrls()) {
if (pathMatcher.match(path, request.getRequestURI())) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
}
// 此处会调用AccessDecisionManager中的decide方法进行鉴权操作
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return customMetadataSource;
}
}
自定义鉴权管理器:实际鉴权
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
// 当接口未被配置资源时直接放行
if (CollUtil.isEmpty(configAttributes)) {
return;
}
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
// 将当前访问所需资源或用户拥有资源进行比对
String needAuthority = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
// 如果有该权限直接放行
if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("抱歉,您没有访问权限");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
自定义获取权限源:获取当前资源对应的权限
public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource {
// 所有资源
private List<String> allResource;
@PostConstruct
public void loadDataSource() {
// 把所有权限加载到内存中
allResource = new ArrayList<>();
allResource.add("/add");
allResource.add("/delete");
allResource.add("/update");
}
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
List<ConfigAttribute> configAttributes = new ArrayList<>();
// 获取当前访问的路径
String url = ((FilterInvocation) o).getRequestUrl();
if (allResource.contains(url)) {
configAttributes.add(new org.springframework.security.access.SecurityConfig(url));
}
return configAttributes;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
将过滤器注入过滤器链中
protected void configure(HttpSecurity http) throws Exception {
// 自定义权限过滤器
http.addFilterBefore(customAuthFilter, FilterSecurityInterceptor.class);
}
小结:
以上就是SpringBoot + Spring Security + JWT的认证授权过程了
一开始看还很懵他都是自己内置的页面,那前后分离怎么办
其实就像原本用拦截器做token校验一样,加个token过滤器解析成Security需要的认证信息即可,后面框架会来认证
而鉴权方面,则像上面一样做即可
JWT的特点和使用场景
JWT的主要优点是它的紧凑性和自包含性,由于JSON的详细性小于XML,因此编码其大小也较小。JWT适用于需要跨域认证的场景,因为它可以通过HTTP请求头或Cookie传输,并且由于数字签名的存在,信息是可信的。