SpringBoot笔记

发布于:2025-02-10 ⋅ 阅读:(84) ⋅ 点赞:(0)

1.创建

使用idea提供的脚手架创建springboot项目,选上需要的模块,会自动进行导包
打成jar包,之前直接用原生的maven打包的是一个瘦jar,不能直接跑,把服务器上部署的jar排除在外了,但是现在加上打包查件,打包是一个fat jar,安装了java环境,就可以输入命令java -jar [jar包名] 就可以跑起来了(内置了tomcat)

2.原理

1.依赖管理机制:

所有创建的项目是基于一个母模块,可以看到Pom中有一个parent,就是母包,这个依赖中有全部我们需要的依赖,以及对应版本。我们需要哪一块,只需要将start包引入即可,不用填v,母依赖已经做了版本,会自动导其版本。
Build Systems :: Spring Boot

2.自动配置机制 

原理:

1.添加启动器 spring-boot-start-XXX
   所有的start里面都有一个spring-boot-start,这个里面又有spring-boot-autoconfigure包,里面就是所有配置类。

2.主程序的注解@SpringBootApplication上面有三个注解
   其中@EnableAutoConfiguration就是告诉springboot要把哪些类放到容器中
   此注解上面有@Import({AutoConfigurationImportSelector.class}),即位置,具体位置是
spring-boot-autoconfigure
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
一共146个配置类 名字叫XXXAutoConfiguration
如,ServletWebServerFactoryAutoConfigurationbu

3.不是全部使用,会看你导入了哪些启动类,也就是看这个类在不在,在的话我就配置。
(用条件注解@ConditionalOnxxx实现
怎么配置?配置类有很多@Bean,会将配置文件的有关参数读入,然后配给这个bean
就实例化成功了。

3.最佳实践思路

1.添加启动器 spring-boot-start-XXX 
2.查看对应的XXXAutoConfiguration类,看里面注册了什么bean,我们可以用什么
3. 写配置 可以查看此bean用了什么配置类,此配置类用的配置文件前缀叫什么       eg.@EnableConfigurationProperties(RedisProperties.class)
    这里需要我们提供什么配置,配置名叫什么  
    我们想要修盖哪些默认配置,在配置文件中修改
    我们想要自定义哪些配置,实现WebMvcConfigurer接口
4.使用:
    需要用到这些注册好的bean,直接@AutoWried
    如果对springboot自动创建放到容器的bean不满意,我们可以直接写个配置类在里面写一个自己的这个Bean,加上@Order配置优先级,就会覆盖了。

@Order 注解通常用于指定某些组件的执行顺序 core包的

4.具体使用

完整版 SpringBoot3-快速入门 · 语雀

1.配置文件格式变为yaml

可以将properties改为yaml格式,更清晰

person:
  name: 张三
  age: 18
  birthDay: 2010/10/10 12:12:12
  like: true
  child:
    name: 李四
    age: 20
    birthDay: 2018/10/10
    text: ["abc","def"]
  dogs:
    - name: 小黑
      age: 3
    - name: 小白
      age: 2
  cats:
    c1:
      name: 小蓝
      age: 3
    c2: {name: 小绿,age: 2} #对象也可用{}表示

 2.日志

SpringBoot怎么把日志默认配置好的

  1. 每个starter场景,都会导入一个核心场景spring-boot-starter
  2. 核心场景引入了日志的所用功能spring-boot-starter-logging
  3. spring-boot-starter-logging导入了logback + slf4j ,所以作为默认底层日志组合
  4. 日志是系统一启动就要用xxxAutoConfiguration是系统启动好了以后放好的组件,后来用的。
  5. 日志是利用监听器机制配置好的。ApplicationListener
    可以看到在spring-boot-autoconfigure包下,找到logging相关自动配置,没有xxxAutoConfiguration文件,都是监听器
  6. 日志所有的配置都可以通过修改配置文件实现。以logging开始的所有配置。

总结:日志是系统一启动就会打印的,是用监听器机制实现的
           会读spring-boot包里的文件additional-spring-configuration-metadata.json配置日志格式
(日志是springboot很底层的东西,所以在spring-boot包下写的配置,其余系统已开启就有的东西,也会放到上述的文件中,如banner就是spring图标)

常见使用

日志所有的配置都可以通过修改配置文件实现。以logging开始的所有配置。
1.修改输出格式 logging.pattern.console
   可尝试修改为
'%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{15} ===> %msg%n'

还可以单独修改某一部分的格式,如logging.pattern.dateformat单独修改日期格式

2.修改日志级别

由低到高:ALL,TRACE, DEBUG, INFO, WARN, ERROR,FATAL,OFF
只会打印指定级别及以上级别的日志
不指定级别的所有类,都使用root指定的级别作为默认级别
SpringBoot日志默认级别是 INFO,可以用logging.level.root修改

3.日志分组

将相关的logger分组在一起,统一配置。指定一些类的级别,不用root的级别了。

logging.group.mineGroup=com.yang.controller.test,com.yang.service.test
logging.level.mineGroup=debug

4.文件输出

SpringBoot 默认只把日志写在控制台,如果想额外记录到文件,可以在application.properties中添加logging.file.name or logging.file.path配置项。

如:logging.file.name=my.log 在项目底下出现my.log(路径D:\java\projects\springboot\my.log)

5.文件归档与滚动切割(防止打开log太卡了,文件太大)

归档:每天的日志单独存到一个文档中。

切割:每个文件10MB,超过大小切割成另外一个文件。

最佳实战

  1. 导入任何第三方框架,先排除它的日志包,因为Boot底层控制好了日志
  2. 修改 application.properties 配置文件,就可以调整日志的所有行为。如果不够,可以编写日志框架自己的配置文件放在类路径下就行,比如logback-spring.xmllog4j2-spring.xml
  3. 如需对接专业日志系统,也只需要把 logback 记录的日志灌倒 kafka之类的中间件,这和SpringBoot没关系,都是日志框架自己的配置,修改配置文件即可
  4. 业务中使用slf4j-api记录日志。不要再 sout 了

3.web启动器分析(spring-starter-web)

2、SpringBoot3-Web开发 · 语雀超详细解析)

前面原理说过了,自动导入以下web相关的AutoConfiguration类

org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
====以下是响应式web场景和现在的没关系======
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
================以上没关系=================
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

点进去每个自动配置类,都在里面注册了很多bean,然后有些bean上面绑定了配置类,配置类与配置文件绑定,有如下几个绑定的配置项。

  • 1、SpringMVC的所有配置 spring.mvc
  • 2、Web场景通用配置 spring.web
  • 3、文件上传配置 spring.servlet.multipart
  • 4、服务器的配置 server: 比如:编码方式

分析WebMvcAutoConfiguration

静态资源访问

1.发送请求,用两个过滤器,将form表单发送的delete,put请求正确处理

2.DispatcherServle会分析此请求是否是静态资源路径,默认前缀为/**

3.如果是静态资源路径,会从存放静态资源的路径下查找,默认为

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/

4.所有静态资源都定义了缓存规则。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值(静态路径在注册时,就会配置好缓存规则)

 5.还定义了欢迎页面,一开始打开就会走欢迎页,在上述静态资源路径下查找,没有就在 templates下找index模板页

6.浏览器访问资源后会自动发请求要favicon.ico,服务器在静态资源目录下找 favicon.ico

修改配置

所有的默认配置都可以自定义,配置方式有两种,1.修改配置文件 2.实现WebMvcConfigurer接口
以修改静态资源为例

1.修改配置文件

spring.mvc:修改静态资源的前缀

spring.web:修改静态资源路径 以及 缓存机制

server.port=9000

#1、spring.web:
# 1.配置国际化的区域信息
# 2.静态资源策略(开启、处理链、缓存)

#开启静态资源映射规则 默认也是开启的
spring.web.resources.add-mappings=true

#设置缓存 
#spring.web.resources.cache.period=3600
##缓存详细合并项控制,覆盖period配置:
## 浏览器第一次请求服务器,服务器告诉浏览器此资源缓存7200秒,7200秒以内的所有此资源访问不用发给服务器请求,7200秒以后发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
#使用资源 last-modified 时间,来对比服务器和浏览器的资源是否相同没有变化。相同返回 304 
#浏览器规定刷新就一定要访问服务器,服务器返回最后一次修改时间,一致返回304还是走缓存
spring.web.resources.cache.use-last-modified=true

#自定义静态资源文件夹位置
spring.web.resources.static-locations=classpath:/a/,classpath:/b/,classpath:/static/

#2、 spring.mvc
## 2.1. 自定义webjars路径前缀
spring.mvc.webjars-path-pattern=/wj/**
## 2.2. 静态资源访问路径前缀
spring.mvc.static-path-pattern=/static/**

2.实现WebMvcConfigurer接口 
这种修改方式,是保留所有springboot的默认参数。自己可以额外添加规则。这中方式很常用,就是springboot中没有此规则就实现此接口,写自己的方法。springboot默认资源从static找之类的,都是生效的。

只要在容器中放WebMvcConfigurer组件就会生效,原理看雷神文章。

此接口的内容:

我们配制自己静态资源规则,就要重写addResourceHandlers方法。(仿照源码来写)

@Configuration //这是一个配置类
public class MyConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //保留以前规则
        //自己写新的规则。
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/a/","classpath:/b/")
                .setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS));
    }
}

我们也可以添加拦截器,首先实现一个HandlerInterceptor接口,接着用addInterceptors方法添加此拦截器。

public class MainInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("我是处理之前!");
        return true;   //只有返回true才会继续,否则直接结束
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("我是处理之后!");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      	//在DispatcherServlet完全处理完请求后被调用
        System.out.println("我是完成之后!");
    }
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new MainInterceptor())
      .addPathPatterns("/**")    //添加拦截器的匹配路径,只要匹配一律拦截
      .excludePathPatterns("/home");   //拦截器不进行拦截的路径
}
访问controller层的原理

DispatcherServlet接收请求(默认支持两种:ant风格和restful风格)
会用起方法准备两个东西:

  • HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值
  • HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值该怎么处理

得到执行链对象,包括要执行的方法(Controller)和拦截器,之后执行拦截器的preHandle再执行Controller,执行完毕后会返回一个对象,之后执行拦截器的postHandle方法
紧接着,给返回对象找到一个合适的返回值处理器 HandlerMethodReturnValueHandler

之后执行处理的方法

  • 如果标注了@ResponseBody注解,会找到一个能处理标注此注解的方法,找到对应消息处理器。有内容协商(2种方式),选择要求的处理器,如果有,返回即可;没有内容协商,默认json。
  • 没标此注默认统一返回ModelAndView对象,走视图解析器,ViewResolver(视图解析器)将逻辑视图转为物理视图(加上前缀和后缀),返回一个视图对象,之后视图对象将内容转为html,对应消息转化器将html文件写入响应体,DispatcherServlet发给浏览器,完成渲染

最后执行拦截器的afterCompletion方法。

WebMvcAutoConfiguration提供几种默认HttpMessageConverters

  • ByteArrayHttpMessageConverter: 支持字节数据读写
  • StringHttpMessageConverter: 支持字符串读写
  • ResourceHttpMessageConverter:支持资源读写
  • ResourceRegionHttpMessageConverter: 支持分区资源写出
  • AllEncompassingFormHttpMessageConverter:支持表单xml/json读写
  • MappingJackson2HttpMessageConverter: 支持请求响应体Json读写

 WebMvcAutoConfiguration实现了WebMvcConfigurationSupport

 WebMvcConfigurationSupport提供了很多默认配置,其中就有消息处理器添加的功能。
判断系统中是否有相应的类:如果有,就加入相应的HttpMessageConverter

jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
				ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);

 所以导入jackon的xml包也就有了xml的消息转化器。在pojo类上加上
像yaml的格式,我们即使导入了转换包,也没有消息处理器,需要自己写一个。

内容协商

1.内容协商的两种方式:
1)Accept请求头,请求头要求什么格式返回什么格式,默认开启的
 如:浏览器端

可以要的格式有这么多,优先text/html
像客户端,就会accpt就会是json优先

 2)请求参数,需要开启【看雷神的文章去】
(格式如:application/json text/html ,也可以在springboot配置文件中自定义)
                                     

分析ErrorMvcAutoConfiguration

springboot的错误机制

 SpringMVC的错误处理机制依然保留,MVC处理不了,才会交给boot进行处理,boot就是转协商转错误页面还是其他格式到。

回顾springMVC错误机制:

在某个类中写方法并标注@@ExceptionHandler(处理的异常类型.class),此类中出现此异常,都会交给此方法来处理。
也可以设置全局异常处理器类,所有的类出现对应异常都会跑到此类执行对应方法。
方法参数可以接收Exception,打印异常信息。

@ControllerAdvice
@RestController
public class AllError {
    @ExceptionHandler(Exception.class)
    public String error(Exception e) {
        return "error"+e.getMessage();
    }
}

分析ServletWebServerFactoryAutoConfiguration和EmbeddedWebServerFactoryCustomizerAutoConfiguration

容器tomcat启动就是这两,启动后ioc容器会创建web工厂,默认有tomcat所以创建了tomcat服务器

  • 修改server下的相关配置就可以修改服务器参数
  • 通过给容器中放一个ServletWebServerFactory,来禁用掉SpringBoot默认放的服务器工厂,实现自定义嵌入任意服务器

web最佳实践

4.数据整合

1.导入mybatis-spring-boot-starter和mysql-connector-java
2.编写数据源配置

spring.datasource.url=jdbc:mysql://192.168.200.100:3306/demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456

3.配置mybatis,如mapper文件位置,是否开启驼峰模式等,所有在mybaits配置文件中可配的都可配

#指定mapper映射文件位置
mybatis.mapper-locations=classpath:/mapper/*.xml
#参数项调整
mybatis.configuration.map-underscore-to-camel-case=true

4.使用时,要将mapper接口代理为实现类,可以用注解@MapperScan做扫包,或者将mapper接口上添加@Mapper注解

5.编写mapper文件或者添加注解 

  • 使用mybatisx插件,快速生成MapperXML

原理:

1.jdbc场景的自动配置:
mybatis-spring-boot-starter导入 spring-boot-starter-jdbc,jdbc是操作数据库的场景
DataSourceAutoConfiguration,数据源的自动配置,读配置文件,默认数据源是HikariDataSource
有DataSourceTransactionManagerAutoConfiguration,支持事务

2.MyBatisAutoConfiguration:配置了MyBatis的整合流程(在mybaits-spring-boot-starter包中)
数据源配置好后,会配置SqlSessionFactory和SqlSessionTemplate

​​​(要用aop功能还要导入spring-boot-starter-aop包_)

5.前后端分离

后端只是返回给前端需要的json数据就可以了。前端放在一个服务器中,后端放在另一个服务器中
详细配置:柏码 - 让每一行代码都闪耀智慧的光芒!

基于Session的分离(有状态)

前端自动带上cookie找到对应session就可以了,前后端就可以交流了。

SpringSecurity默认就是用session+cookie

使用SpringSecurity

1.勾上此模块导入相应的启动器
2.之后启动主配置类,就默认开启filter了。当然还是一样注册一个SecurityFilterChain的bean,就可以修改默认filter了。
3.认证方式也是一样使用的,配置UserDetailsService,配置PasswordEconding
4.授权的话就用注解
@PreAuthorize :方法执行前判断你有没有权限执行
@PostAuthorize :方法执行后判断结果你有没有权限拿

前端在发起请求的时候将携带Cookie信息写为true即可,填写信息
然后发送给后端服务的url,此url是ip+项目名+/api/auth/login,后面的就是在配置里面自定义的处理登录认证的url-->loginProcessingUrl("/api/auth/login"),放行此url

这里已发送就会出现不可以跨域,也是SpringSecurity来防止攻击的一种方式,可以通过配置文件允许前端的站点访问

后端值返回json要配置相关Handler:

exceptionHandling-->authenticationEntryPoint-->没登录返回json
successHandler-->认证成功返回json
failureHandler-->认证失败返回json
exceptionHandling-->accessDeniedHandler-->授权失败返回json

之后执行中有异常没有正常返回json,我们可以配置全局异常处理器,来实现不同异常,返回不同json
下面代码中的返回只是字符串,实际开发要返回Rest标准的json数据。

public  SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(conf -> {
                    conf.anyRequest().authenticated();
                })
                .formLogin(conf -> {
                    //一般分离之后,为了统一规范接口,使用 /api/模块/功能 的形式命名接口
                    conf.loginProcessingUrl("/api/auth/login");
                    conf.successHandler((request, response, authentication) -> {
                        response.getWriter().write("success");
                    });
                    conf.failureHandler((request, response, exception) -> {
                        response.getWriter().write("failure");
                    });
                    conf.permitAll();
                })
                .exceptionHandling(conf -> {
                    conf.accessDeniedHandler((request, response, exception) -> {
                        response.getWriter().write("denied");
                    });
                    conf.authenticationEntryPoint((request,response,exception)->{
                        response.getWriter().write("error");
                    });
                })
                .csrf(AbstractHttpConfigurer::disable)
                .build();
    }

基于Token的分离(无状态)

登录成功后返回一个jwt给前端,前端在访问时在请求头或者请求体总中带上这个,判断用户是谁,前后端就可以交流了。

实现思路:

前端发起登录请求,然后对应loginProcessingUrl,[经过安全filter] 之后认证,成功后返回一个jwt给前端,之后前端带着这个jwt来请求,我们在认证前写一个jwtFilter,如果jwt不为空,封装成一个Authentication对象放到SecurityContext中,然后放行,之后的认证看到SecurityContext不为空,继续放行。如果jwt失效或者用户退出了,jwt要失效放到黑名单中,将jwt的id放入即可。
(在security配置中关闭一下seesion的开启,不用这种方式了,开着浪费资源)
 

前端的请求中jwt的key叫Authorization
格式Authorization: Bearer 刚刚获取的Token
后端值返回json要配置相关Handler:

exceptionHandling-->authenticationEntryPoint-->没登录返回json
successHandler-->认证成功返回json,json要有jwt
failureHandler-->认证失败返回json
exceptionHandling-->accessDeniedHandler-->授权失败返回json

logout-->logoutUrl配置退出的url,与loginProcessingUrl一样原理
         -->logoutSuccessHandler 退出要将jwt放入黑名单

之后执行中有异常没有正常返回json,我们可以配置全局异常处理器,来实现不同异常,返回不同json

可以用postman模拟前端请求新手如何使用postman(新手使用,简单明了)_postman教程-CSDN博客

1.介绍jwt

三部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)
标头:加密算法,还有类型 -->用Base64编码
有效负载:JWT签名所使用的包括用户名称、令牌发布时间、过期时间、JWT ID等,当然我们也可以自定义添加字段,我们的用户信息一般都在这里存放。是明文的,不要存储敏感信息,如密码 --> 用Base64编码
签名:首先需要指定一个密钥,该密钥仅仅保存在服务器中,保证不能让其他用户知道。然后使用Header中指定的算法对Header和Payload进行base64加密之后的结果通过密钥计算哈希值,然后就得出一个签名哈希。这个会用于之后验证内容是否被篡改。--> 哈希值

创建jtw工具类:
(User是UserDetails的实现类)

public class JwtUtils {
  	//Jwt秘钥
    private static final String key = "abcdefghijklmn";
    //黑名单
    private static final HashSet<String> blackList = new HashSet<>();

    //加入黑名单方法
  public static boolean invalidate(String token){
        Algorithm algorithm = Algorithm.HMAC256(key);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            DecodedJWT verify = jwtVerifier.verify(token);
            Map<String, Claim> claims = verify.getClaims();
          	//取出UUID丢进黑名单中
            return blackList.add(verify.getId());
        } catch (JWTVerificationException e) {
            return false;
        }
  }

  	//根据用户信息创建Jwt令牌
    public static String createJwt(UserDetails user){
        Algorithm algorithm = Algorithm.HMAC256(key);
        Calendar calendar = Calendar.getInstance();
        Date now = calendar.getTime();
        calendar.add(Calendar.SECOND, 3600 * 24 * 7);
        return JWT.create()
                //额外添加一个UUID用于记录黑名单,将其作为JWT的ID属性jti
          		.withJWTId(UUID.randomUUID().toString())
                .withClaim("name", user.getUsername())  //配置JWT自定义信息
                .withClaim("authorities", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList())
                .withExpiresAt(calendar.getTime())  //设置过期时间
                .withIssuedAt(now)    //设置创建创建时间
                .sign(algorithm);   //最终签名
    }

  	//根据Jwt验证并解析用户信息
    public static UserDetails resolveJwt(String token){
        Algorithm algorithm = Algorithm.HMAC256(key);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            DecodedJWT verify = jwtVerifier.verify(token);  //对JWT令牌进行验证,看看是否被修改
            Map<String, Claim> claims = verify.getClaims();  //获取令牌中内容
            if(new Date().after(claims.get("exp").asDate())) //如果是过期令牌则返回null
                return null;
            else
              	//重新组装为UserDetails对象,包括用户名、授权信息等
                return User
                        .withUsername(claims.get("name").asString())
                        .password("")
                        .authorities(claims.get("authorities").asArray(String.class))
                        .build();
        } catch (JWTVerificationException e) {
            return null;
        }
    }
}

2.创建jwtFilter

public class JwtAuthenticationFilter extends OncePerRequestFilter {  
//继承OncePerRequestFilter表示每次请求过滤一次,用于快速编写JWT校验规则

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
      	//首先从Header中取出JWT
        String authorization = request.getHeader("Authorization");
      	//判断是否包含JWT且格式正确
        if (authorization != null && authorization.startsWith("Bearer ")) {
            String token = authorization.substring(7);	
          	//开始解析成UserDetails对象,如果得到的是null说明解析失败,JWT有问题
            UserDetails user = JwtUtils.resolveJwt(token);
            if(user != null) {
              	//验证没有问题,那么就可以开始创建Authentication了,这里我们跟默认情况保持一致
              	//使用UsernamePasswordAuthenticationToken作为实体,填写相关用户信息进去
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
              	//然后直接把配置好的Authentication塞给SecurityContext表示已经完成验证
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
      	//最后放行,继续下一个过滤器
      	//可能各位小伙伴会好奇,要是没验证成功不是应该拦截吗?这个其实没有关系的
      	//因为如果没有验证失败上面是不会给SecurityContext设置Authentication的,后面直接就被拦截掉了
      	//而且有可能用户发起的是用户名密码登录请求,这种情况也要放行的,不然怎么登录,所以说直接放行就好
        filterChain.doFilter(request, response);
    }
}

3.注册自定义filter,关闭seesion,配置handler

public class SecurityConfiguration {
    @Bean
    public  SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(conf -> {
                    conf.anyRequest().authenticated();
                })
                .formLogin(conf -> {
                    conf.loginProcessingUrl("/api/auth/login");
                    //使用自定义的成功失败处理器
                    conf.failureHandler(this::handleProcess);
                    conf.successHandler(this::handleProcess);
                    conf.permitAll();
                })
                .cors(conf -> {
                    CorsConfiguration cors = new CorsConfiguration();
                    //添加前端站点地址,这样就可以告诉浏览器信任了
                    cors.addAllowedOrigin("http://localhost:8080");
                    //虽然也可以像这样允许所有 cors.addAllowedOriginPattern("*");
                    //但是这样并不安全,我们应该只许可给我们信任的站点
                    cors.setAllowCredentials(true);  //允许跨域请求中携带Cookie
                    cors.addAllowedHeader("*");   //其他的也可以配置,为了方便这里就 * 了
                    cors.addAllowedMethod("*");
                    cors.addExposedHeader("*");
                    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
                    source.registerCorsConfiguration("/**", cors);  //直接针对于所有地址生效
                    conf.configurationSource(source);
                })
                .exceptionHandling(conf -> {
                    //配置授权相关异常处理器
                    conf.accessDeniedHandler(this::handleProcess);
                    //配置验证相关异常的处理器
                    conf.authenticationEntryPoint(this::handleProcess);
                })
                .sessionManagement(conf -> {
                    conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                })
                //添加我们用于处理JWT的过滤器到Security过滤器链中,注意要放在UsernamePasswordAuthenticationFilter之前
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .logout(conf->{
                        conf.logoutUrl("/api/auth/logout") ;// 指定退出登录的 URL
                        conf.logoutSuccessHandler(this::onLogoutSuccess); // 配置自定义的退出登录处理器
                        conf.permitAll();
                }
                )
                .build();
    }
    private void handleProcess(HttpServletRequest request,
                               HttpServletResponse response,
                               Object exceptionOrAuthentication) throws IOException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        if(exceptionOrAuthentication instanceof AccessDeniedException exception) {
            writer.write(RestBean.failure(403, exception.getMessage()).asJsonString());
        } else if(exceptionOrAuthentication instanceof AuthenticationException exception) {
            writer.write(RestBean.failure(401, exception.getMessage()).asJsonString());
        } else if(exceptionOrAuthentication instanceof Authentication authentication){
            //不过这里需要注意,在登录成功的时候需要返回我们生成的JWT令牌,这样客户端下次访问就可以携带这个令牌了,令牌过期之后就需要重新登录才可以
            writer.write(RestBean.success(JwtUtils.createJwt((User) authentication.getPrincipal())).asJsonString());
        }
    }
    private void onLogoutSuccess(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Authentication authentication) throws IOException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        String authorization = request.getHeader("Authorization");
        if(authorization != null && authorization.startsWith("Bearer ")) {
            String token = authorization.substring(7);
            //将Token加入黑名单
            if(JwtUtils.invalidate(token)) {
                //只有成功加入黑名单才会退出成功
                writer.write(RestBean.success("退出登录成功").asJsonString());
                return;
            }
        }
        writer.write(RestBean.failure(400, "退出登录失败").asJsonString());
    }


监听器


网站公告

今日签到

点亮在社区的每一天
去签到