【Springboot】路由鉴权

【JavaWeb】过滤器与拦截器-Spring Security 

【JavaWeb】过滤器与拦截器 

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 作用

  1. 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算 PV(Page View)等;
  2. 权限检查:如登录检测,进入处理器检测是否登录;
  3. 性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。(反向代理,如 Apache 也可以自动记录)
  4. 通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的即可使用拦截器实现。

目前了解的 Spring 中的拦截器有:

  1. HandlerInterceptor
  2. MethodInterceptor
1.5 HandlerInterceptor 拦截器

HandlerInterceptor 类似 Filter,拦截的是请求地址 ,但提供更精细的的控制能力,这里注意下必须过DispatcherServlet 的请求才会被拦截。
它允许你在请求处理前、处理后以及视图渲染完成前执行自定义逻辑,可以用来对请求地址做一些认证授权、预处理,也可以计算一个请求的响应时间等,还可以处理跨域(CORS)问题。

简单的执行流程描述:

  1. 请求到达 DispatcherServlet,然后发送至 Interceptor,执行 preHandler;
  2. 请求到达 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 请求做编码处理、过滤无用参数、安全校验(比如登陆态校验),如果涉及业务逻辑上的,还是建议用拦截器。

  1. 多个过滤器的执行顺序跟定义的先后关系有关。通过@Order控制过滤器的级别,值越小级别越高越先执行。
  2.  多个拦截器执行顺序跟注册先后顺序有关。
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的核心功能包括:

  1. Authentication(认证):验证用户是他们声明的身份。
  2. Authorization(授权):访问控制过程,决定用户是否有权限进行某个操作。
  3. SessionManagement(会话管理):管理用户会话,确保用户在多次请求间保持登录状态。
  4. Cryptography(加密):安全的密码编码。
  5. Web Security(Web安全性):提供web请求的安全性,如CSRF保护。
  6. 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,它的大概逻辑应该是这样的:

  1. 从HTTP请求中获取用户名和密码,来源包括标准的Basic Auth HTTP Header,表单字段或者cookie等等。
  2. 身份认证,也就是校验用户名和密码。
  3. 认证通过后,需要检查当前登录的用户有没有访问当前HTTP请求的权限,也就是鉴权逻辑。
  4. 权限校验也通过后,就继续执行其它Filter,所有Filter都通过后,进入Servlet,最终到达具体的Controller。

在安全领域,由于攻防手段的多样性和认证鉴权方式的复杂性,将所有功能都放在一个Filter中会导致该Filter迅速演变为一个庞大而复杂的类。因此,在实际应用场景中,我们常常将这个庞大的Filter拆分成多个小Filter,并将它们链接在一起。每个Filter都只负责特定领域的功能,比如CsrfFilterAuthenticationFilterAuthorizationFilter等。

这种概念被称为FilterChain,实际上JarkataEE规范也有相识的概念。通过使用FilterChain,你就可以以插拔的方式添加或移除特定功能的Filter,而无需改动现有的代码。

2.5 FilterChain介绍

Spring Security通过DefaultSecurityFilterChain类来完成安全相关的功能,而该类本身又由其它Filter组成。默认情况下,Spring Security Starter引入了15个Filter,如下图所示:

下面我们简要介绍下其中几个重要的Filter:

  1. CsrfFilter:这个Filter用于防止跨站点请求伪造攻击,这也是导致所有POST请求都失败的原因。基于Token验证的API服务可以选择关闭CsrfFilter,而一般Web页面需要开启。
  2. BasicAuthenticationFilter:支持HTTP的标准Basic Auth的身份验证模块。
  3. UsernamePasswordAuthenticationFilter:支持Form表单形式的身份验证模块。
  4. DefaultLoginPageGeneratingFilter和DefaultLogoutPageGeneratingFilter:用于自动生成登录页面和注销页面。
  5. 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传输,并且由于数字签名的存在,信息是可信的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

常生果

喜欢我,请支持我

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值