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.具体使用
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怎么把日志默认配置好的
- 每个
starter
场景,都会导入一个核心场景spring-boot-starter
- 核心场景引入了日志的所用功能
spring-boot-starter-logging
spring-boot-starter-logging
导入了logback + slf4j
,所以作为默认底层日志组合日志是系统一启动就要用
,xxxAutoConfiguration
是系统启动好了以后放好的组件,后来用的。- 日志是利用监听器机制配置好的。
ApplicationListener
。
可以看到在spring-boot-autoconfigure包下,找到logging相关自动配置,没有xxxAutoConfiguration文件,都是监听器
- 日志所有的配置都可以通过修改配置文件实现。以
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,超过大小切割成另外一个文件。
最佳实战
- 导入任何第三方框架,先排除它的日志包,因为Boot底层控制好了日志
- 修改
application.properties
配置文件,就可以调整日志的所有行为。如果不够,可以编写日志框架自己的配置文件放在类路径下就行,比如logback-spring.xml
,log4j2-spring.xml
- 如需对接专业日志系统,也只需要把 logback 记录的日志灌倒 kafka之类的中间件,这和SpringBoot没关系,都是日志框架自己的配置,修改配置文件即可
- 业务中使用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.所有静态资源都定义了缓存规则。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值(静态路径在注册时,就会配置好缓存规则)
- cachePeriod: 缓存周期; 多久不用找服务器要新的。 默认没有,以s为单位
- cacheControl: HTTP缓存控制;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching
- useLastModified:是否使用最后一次修改。配合HTTP Cache规则,默认为true
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-->授权失败返回jsonlogout-->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());
}
监听器