1. Servlet原生组件
概念: springboot支持原生的servlet组件,如servlet过滤器和servlet监听器等:
- 开发原生servlet类
c.y.s.servlet.BeanServlet,无需添加注解。 - 开发原生过滤器类
c.y.s.servlet.filter.BeanFilter,无需添加注解。 - 开发原生监听器类
c.y.s.servlet.listener.BeanListener,无需添加注解。 - 开发配置类
c.y.s.servlet.BeanServletConfig:- IOC
o.s.b.w.s.ServletRegistrationBean类,利用构造传入servlet实例和路由。
- IOC
- 开发配置类
c.y.s.servlet.filter.BeanFilterConfig:- IOC
o.s.b.w.s.FilterRegistrationBean过滤器链类。 filters.setFilter():在过滤器链中加入自定义过滤器。filters.addUrlPatterns():在过滤器链中加入自定义过滤器拦截规则。
- IOC
- 开发配置类
c.y.s.servlet.listener.BeanListenerConfig:- IOC
o.s.b.w.s.ServletListenerRegistrationBean监听器类,利用构造传入监听器实例。 - 每个监听对应一个
@Bean。
- IOC
- psm测试:
/api/servlet/bean。
概念: /springboot/
- res:
c.y.s.servlet.BeanServlet
package com.yap.springboot2.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author yap
*/
public class BeanServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("This is BeanServlet!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
- res:
c.y.s.servlet.BeanServletConfig
package com.yap.springboot2.servlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yap
*/
@Configuration
public class BeanServletConfig {
@SuppressWarnings("all")
@Bean
public ServletRegistrationBean servletRegistrationBean() {
return new ServletRegistrationBean(new BeanServlet(), "/api/servlet/bean");
}
}
- res:
c.y.s.servlet.filter.BeanFilter
package com.yap.springboot2.servlet.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author yap
*/
public class BeanFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("BeanFilter init()...");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
System.out.println("BeanFilter doFilter(): " + req.getRequestURI());
chain.doFilter(req, resp);
}
@Override
public void destroy() {
System.out.println("BeanFilter destroy()...");
}
}
- res:
c.y.s.servlet.filter.BeanFilterConfig
package com.yap.springboot2.servlet.filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yap
*/
@Configuration
public class BeanFilterConfig {
@SuppressWarnings("all")
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filters = new FilterRegistrationBean();
filters.setFilter(new BeanFilter());
// 不支持部分模糊,如 `/test` 或 `/*test`
filters.addUrlPatterns("/api/servlet/*");
return filters;
}
}
- res:
c.y.s.servlet.listener.BeanListener
package com.yap.springboot2.servlet.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* @author yap
*/
public class BeanListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("BeanListener contextInitialized()...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("BeanListener contextDestroyed()...");
}
}
- res:
c.y.s.servlet.listener.BeanListenerConfig
package com.yap.springboot2.servlet.listener;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yap
*/
@Configuration
public class BeanListenerConfig {
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
return new ServletListenerRegistrationBean<>(new BeanListener());
}
}
1.1 注解配置方案
流程:
- 开发原生servlet类
c.y.s.servlet.ScanServlet,标记@WebServlet。 - 开发原生过滤器类
c.y.s.servlet.filter.ScanFilter,标记@WebFilter。 - 开发原生监听器类
c.y.s.servlet.listener.ScanListener,标记@WebListener。 - 在启动类中使用
@ServletComponentScan扫描servlet类,过滤器类和监听器类所在包。- @ServletComponentScan("com.yap.springboot2.servlet")
- psm测试:
/api/servlet/scan。
概念: /springboot/
- res:
c.y.s.servlet.ScanServlet
package com.yap.springboot2.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author yap
*/
@WebServlet("/api/servlet/scan")
public class ScanServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("This is ScanServlet!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
- res:
c.y.s.servlet.filter.ScanFilter
package com.yap.springboot2.servlet.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author yap
*/
@WebFilter("/api/servlet/*")
public class ScanFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("ScanFilter init()...");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
System.out.println("ScanFilter doFilter(): " + req.getRequestURI());
chain.doFilter(req, resp);
}
@Override
public void destroy() {
System.out.println("ScanFilter destroy()...");
}
}
- res:
c.y.s.servlet.listener.ScanListener
package com.yap.springboot2.servlet.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
* @author yap
*/
@WebListener
public class ScanListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ScanListener contextInitialized()...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ScanListener contextDestroyed()...");
}
}
1.2 同时配置方案
流程:
- 开发原生servlet类
c.y.s.servlet.ContextServlet,无需添加注解。 - 开发原生过滤器类
c.y.s.servlet.filter.ContextFilter,无需添加注解。 - 开发原生监听器类
c.y.s.servlet.listener.ContextListener,无需添加注解。 - 让启动类实现
ServletContextInitializer接口并重写onStartup():context.addServlet().addMapping():配置servlet类并设置路由。context.addFilter().addMappingForUrlPatterns():配置过滤器类并设置拦截规则。context.addListener():配置监听器类。
- psm测试:
/api/servlet/context。
概念: /springboot/
- res:
c.y.s.servlet.ContextServlet
package com.yap.springboot2.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author yap
*/
public class ContextServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("This is ContextServlet!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
- res:
c.y.s.servlet.filter.ContextFilter
package com.yap.springboot2.servlet.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author yap
*/
public class ContextFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("ContextFilter init()...");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
System.out.println("ContextFilter doFilter(): " + req.getRequestURI());
chain.doFilter(req, resp);
}
@Override
public void destroy() {
System.out.println("ContextFilter destroy()...");
}
}
- res:
c.y.s.servlet.listener.ContextListener
package com.yap.springboot2.servlet.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* @author yap
*/
public class ContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ContextListener contextInitialized()...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ContextListener contextDestroyed()...");
}
}
- res:
c.y.s.Springboot2Application
@Override
public void onStartup(ServletContext context) {
context.addServlet("contextServlet", new ContextServlet())
.addMapping("/api/servlet/contextServlet");
context.addFilter("contextFilter", new ContextFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true,
"/api/servlet/*");
context.addListener(new ContextListener());
}
2. Spring拦截器
流程:
- 配置pom依赖
spring-boot-starter-aop。 - 开发切面配置类
c.y.s.aop.AopAspect:标记@Aspect和@Configuration。- 开发通知方法:如环绕通知
@Around("execution()")。
- 开发通知方法:如环绕通知
- 开发动作类
c.y.s.aop.AopController。 - psm测试:
/api/aop/execute。
概念: /springboot/
- res:
pom.xml
<!--spring-boot-starter-aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- src:
c.y.s.aop.AopAspect
package com.yap.springboot2.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;
/**
* @author yap
*/
@Aspect
@Configuration
public class AopAspect {
@Around("execution(* com.yap.springboot2.aop..*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("aop: before...");
Object result = null;
try {
// 放行
result = pjp.proceed(pjp.getArgs());
System.out.println("aop: after-returning...");
} catch (Throwable e) {
System.out.println("aop: throwing...");
}
System.out.println("aop: after...");
return result;
}
}
- src:
c.y.s.aop.AopController
package com.yap.springboot2.aop;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author yap
*/
@RestController
@RequestMapping("/api/aop")
public class AopController {
@RequestMapping("execute")
public String execute(Integer meta) {
if (meta == 0) {
throw new RuntimeException("execute() exception...");
}
System.out.println("execute()...");
return "success";
}
}
3. 全局异常
概念: 全局异常处理类的优先级低于异常通知,二者共存时,可以在异常通知处理中直接return错误信息,同样会返回给B端:
- 开发全局异常处理类
c.y.s.exception.GlobalException:标记@ControllerAdvice:- 异常处理方法标记
@ExceptionHandler以指定捕获哪些异常。 - 异常处理方法标记
@ResponseBody以返回异常信息内容。
- 异常处理方法标记
- 开发动作类
c.y.s.exception.ExceptionController:开发一个会爆发异常的动作方法。 - psm测试:
/api/exception/execute。 概念: /springboot/ - src:
c.y.s.exception.GlobalException
package com.yap.springboot2.exception;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
/**
* @author yap
*/
@ControllerAdvice
public class GlobalException {
@ExceptionHandler(Exception.class)
@ResponseBody
public Map<String, Object> exception(Exception e) {
System.out.println("GlobalException.exception()...");
Map<String, Object> exceptionMsg = new HashMap<>(2);
exceptionMsg.put("status", 500);
exceptionMsg.put("msg", e.getMessage());
return exceptionMsg;
}
}
- src:
c.y.s.exception.ExceptionController
package com.yap.springboot2.exception;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author yap
*/
@RestController
@RequestMapping("/api/exception")
public class ExceptionController {
@RequestMapping("/execute")
public String execute(Integer meta) {
if (meta == 0) {
throw new RuntimeException("execute() exception...");
}
return "success";
}
}
4. 定时任务
概念: springboot内置 @EnableScheduling 以替代 java.util.Timer/TimerTask 完成定时任务:
- 启动类标记
@EnableScheduling以开启定时功能。 - 开发任务类
c.y.s.schedule.ScheduleTask:标记@Component以被spring管理。 - 开发任务方法并标记
@Scheduled:cron:CRON表达式fixedDelay=1000:立即执行任务,每次任务完成后计时,每隔1秒执行一次任务。fixedRate=1000:立即执行任务,每次任务开始前计时,每隔1秒执行一次任务。
- 启动入口类,控制台观测任务执行情况。 概念: /springboot/
- src:
c.y.s.schedule.ScheduleTask
package com.yap.springboot2.schedule;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @author yap
*/
@Component
public class ScheduleTask {
@Scheduled(fixedDelay = 30000)
public void printDate() throws InterruptedException {
TimeUnit.SECONDS.sleep(20L);
System.out.println("current date: " + new Date());
}
}
5. 异步处理
概念: 异步执行方法不占用主线程资源,可以提高项目执行效率:
- 启动类上添加
@EnableAsync开启异步功能。 - 开发任务类
c.y.s.async.AsyncTask:标记@Component被spring管理。 - 开发三个任务方法,分别模拟耗时1s/2s/3s:标记
@Async以异步调用:- 若任务类中所有方法都是异步调用,则可将
@Async标记在类上。 - 方法返回值可封装在实现了
Future接口的AsyncResult类中。
- 若任务类中所有方法都是异步调用,则可将
- 开发动作类
c.y.s.async.AsyncController:依次调用任务方法:- 对返回值调用
get()可以获取任务方法的返回值。 - 对返回值调用
isDone()可以判断任务是否已完成。
- 对返回值调用
- psm:
api/async/execute。 概念: /springboot/ - src:
c.y.s.async.AsyncTask
package com.yap.springboot2.async;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* @author yap
*/
@Component
public class AsyncTask {
@Async
public Future<String> taskA() {
try {
long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(1L);
long end = System.currentTimeMillis();
System.out.println("taskA spend: " + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
return new AsyncResult<>("taskA done...");
}
@Async
public Future<String> taskB() {
try {
long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(2L);
long end = System.currentTimeMillis();
System.out.println("taskB spend: " + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
return new AsyncResult<>("taskB done...");
}
@Async
public Future<String> taskC() {
try {
long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(3L);
long end = System.currentTimeMillis();
System.out.println("taskC spend: " + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
return new AsyncResult<>("taskC done...");
}
}
- src:
c.y.s.async.AsyncController
package com.yap.springboot2.async;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* @author yap
*/
@RestController
@RequestMapping("api/async")
public class AsyncController {
private AsyncTask asyncTask;
@Autowired
public AsyncController(AsyncTask asyncTask) {
this.asyncTask = asyncTask;
}
@RequestMapping("execute")
public String execute() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
Future<String> taskA = asyncTask.taskA();
Future<String> taskB = asyncTask.taskB();
Future<String> taskC = asyncTask.taskC();
while (true) {
if (taskA.isDone() && taskB.isDone() && taskC.isDone()) {
System.out.println("all task is done...");
System.out.println("taskA return: " + taskA.get());
System.out.println("taskB return: " + taskB.get());
System.out.println("taskC return: " + taskC.get());
break;
}
}
long end = System.currentTimeMillis();
return "total spend:" + (end - start);
}
}
6. 响应式编程
概念: webflux 是Spring5中的异步非阻塞响应式编程框架,不依赖servlet,不能部署为war包,不使用webapp目录,请求响应对象使用 ServletRequest/ServletResponse:
-
响应式编程:可利用较少的线程数或硬件资源来处理任务,提高系统的伸缩性,但不会让程序运行的更快:
响应式编程举例.md
-
创建springboot-jar项目
springboot2-webflux,选择Web/Spring Reactive Web依赖:- 手动配置需要添加pom依赖
spring-boot-starter-webflux/reactor-test。 spring-boot-starter-webflux比spring-boot-starter-web优先级低,共存时失效。
- 手动配置需要添加pom依赖
-
启动入口类:启动方式由tomcat同步容器变为netty异步容器时表示成功引入webflux。
-
开发实体类
c.y.s.pojo.User。 -
开发业务接口
c.y.s.service.UserService:- 使用
Mono<User>异步不阻塞返回单条User数据。 - 使用
Flux<User>异步不阻塞返回多条User数据。
- 使用
-
开发业务类
c.y.s.service.impl.UserServiceImpl:Mono.just(user):返回Mono对象,参数为null或空时抛异常。Mono.justOrEmpty(user):返回Mono对象,参数为null或空时返回MonoEmpty对象。Flux.fromIterable(users):返回Flux集合对象。Flux.fromArray(users):返回Flux数组对象。
-
开发动作类
c.y.s.controller.UserController:动作方法可直接返回Mono/Flux对象。@RequestMaping配置produces="application/stream+json"可以开启流响应。- 对Flux数据调用
delayElements(Duration.ofSeconds(2))可以设置响应流速为2秒一次。
-
psm测试
api/webflux/select-by-id。 -
cli测试
api/webflux/select-all,postman无法观察流响应效果。 -
开发WebClient类
c.y.s.user.UserTest:用于模拟发送webflux请求,必须先手动启动项目后测试:WebClient.create("uri").get()/post():创建并配置webflux的get/post请求。retrieve().bodyToMono/Flux(User.class):将User数据提取到Mono/Flux对象中。mono.block():立刻取出Mono中的数据。flux.collectList().block():将Flux中的数据收集到list中。 源码: /springboot-webflux/
-
src:
c.y.s.pojo.User
package com.yap.springboot2webflux.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author yap
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private Integer id;
private String name;
}
- src:
c.y.s.service.UserService
package com.yap.springboot2webflux.service;
import com.yap.springboot2webflux.pojo.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author yap
*/
public interface UserService {
/**
* 通过主键查询用户记录
*
* @param id 主键
* @return 满足条件的唯一记录
*/
Mono<User> selectById(Integer id);
/**
* 查询全部用户记录
*
* @return 全部用户记录
*/
Flux<User> selectAll();
}
- src:
c.y.s.service.impl.UserServiceImpl
package com.yap.springboot2webflux.service.impl;
import com.yap.springboot2webflux.pojo.User;
import com.yap.springboot2webflux.service.UserService;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* @author yap
*/
@Service
public class UserServiceImpl implements UserService {
private static List<User> users;
static {
users = new ArrayList<>();
users.add(new User(1, "zhaosi"));
users.add(new User(2, "liuneng"));
users.add(new User(3, "dajiao"));
}
@Override
public Mono<User> selectById(Integer id) {
User user = null;
for (User e : users) {
if (e.getId().equals(id)) {
user = e;
break;
}
}
return Mono.justOrEmpty(user);
}
@Override
public Flux<User> selectAll() {
return Flux.fromIterable(users);
}
}
- src:
c.y.s.controller.UserController
package com.yap.springboot2webflux.controller;
import com.yap.springboot2webflux.pojo.User;
import com.yap.springboot2webflux.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
/**
* @author yap
*/
@RestController
@RequestMapping("/api/webflux")
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping("/select-by-id")
public Mono<User> selectById(Integer id) {
return userService.selectById(id);
}
@RequestMapping(value = "/select-all", produces = "application/stream+json")
public Flux<User> selectAll() {
return userService.selectAll().delayElements(Duration.ofSeconds(2));
}
}
- src:
c.y.s.user.UserTest
package com.yap.springboot2webflux.user;
import com.yap.springboot2webflux.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author yap
*/
class UserTest {
@Test
void webClientMono() {
/*Mono<User> userMono = WebClient.create()
.get().uri("http://localhost:8080/api/webflux/select-by-id?id={id}", 1)
.retrieve().bodyToMono(User.class);*/
Mono<User> userMono = WebClient.create("http://localhost:8080/api/webflux/select-by-id?id=1")
.get().retrieve().bodyToMono(User.class);
System.out.println(userMono.block());
}
@Test
void webClientFlux() {
Flux<User> userFlux = WebClient
.create("http://localhost:8080/api/webflux/select-all")
.get().retrieve().bodyToFlux(User.class);
System.out.println(userFlux.collectList().block());
}
}
7. JWT校验
概念: Json Web Token 是一种基于JSON的高效,简洁,安全的开放标准,使用一个 token进行通信,常用于登录鉴权,单点登录等分布式场景:
- token结构:
header.payload.signature:- header:token头,描述使用哪种加密算法,如MD5,SHA,HMAC等。
- payload:token负载,可携带部分用户信息以避免多次查库:
z-res/jwt内置负载项.md
- signature:token签名,利header中声明的加密算法配合秘钥secretKey(保存在S端)生成:
HMAC(base64(header).base64(payload), secretKey)
- jwt校验流程:
- B端首次登录成功时,S端将某些用户信息组合加密成一个token返回B端。
- B端将该token保存在cookie,sessionStorage或localStorage中。
- 后续B端请求均携带此token,S端解密成功时执行请求,否则阻止。
流程: 新建项目 springboot2-jwt:
-
配置pom依赖:
java-jwt -
开发工具类
c.y.s.util.TokenUtil:JWT.create():返回一个可配置的Builder对象。builder.withClaim(K, V):设置payload中的自定义键值对。builder.sign(Algorithm.HMAC256(secretKey)):通过指定算法和秘钥设置签名。JWT.require(Algorithm.HMAC256(secretKey)).build():通过指定算法和秘钥获取token验证对象。
-
开发注解
c.y.s.annotation.TokenAuth:标记此注解的方法会进行token验证。 -
开发业务方法
c.y.s.service.UserService/UserServiceImpl.login():登陆成功返回User实例。 -
开发动作方法
c.y.s.controller.UserController.login(): -
开发动作方法
c.y.s.controller.UserController.execute():标记@TokenAuth。 -
开发拦截器
c.y.s.interceptor.AuthInterceptor:重写HandlerInterceptor.preHandle():JWT.decode(token).getClaim(K).asString():解码token并获取指定的payload值并转成字符串。JWT.decode(token).getClaim(K).asInt():解码token并获取指定的payload值并转成整型。
-
开发配置类
c.y.s.config.AuthInterceptorConfig:重写WebMvcConfigurer.addInterceptors():registry.addInterceptor():添加自定义拦截器类实例。registry.addPathPatterns():添加拦截器拦截规则。registry.excludePathPatterns():添加拦截器排除规则。
-
psm:
api/user/execute,不加token,登录失败,控制台抛出异常。 -
psm:
api/user/login正常登录,返回token。 -
psm:
api/user/execute,请求头携带token,通过验证。 源码: :/springboot2-jwt/ -
pom:
pom.xml
<!--java-jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
- src:
c.y.s.util.TokenUtil
package com.yap.springboot2jwt.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.yap.springboot2jwt.entity.User;
import java.util.Date;
/**
* @author yap
*/
public class TokenUtil {
/**
* token令牌过期时间,单位毫秒
*/
private static final long EXPIRE = 1000 * 60 * 60 * 24 * 7;
/**
* token令牌秘钥
*/
private static final String SECRET_KEY = "my-secret";
/**
* token令牌发行者
*/
private static final String ISSUER = "yap";
/**
* token令牌主题
*/
private static final String SUBJECT = "login auth";
public static String build(User user){
return JWT.create()
.withClaim("id", user.getId())
.withClaim("username", user.getUsername())
.withClaim("avatar", user.getAvatar())
.withSubject(SUBJECT)
.withIssuer(ISSUER)
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE))
.sign(Algorithm.HMAC256(SECRET_KEY));
}
public static DecodedJWT verify(String token){
try{
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();
try {
return jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("token verify error...");
}
}catch (Exception e){
return null;
}
}
}
- src:
c.y.s.annotation.TokenAuth
package com.yap.springboot2jwt.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author yap
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenAuth {
}
- src:
c.y.s.service.UserService
package com.yap.springboot2jwt.service;
import com.yap.springboot2jwt.entity.User;
/**
* @author yap
*/
public interface UserService {
/**
* 登录
*
* @param user 用户实体
* @return 对应用户的一条记录
*/
User login(User user);
}
- src:
c.y.s.service.impl.UserServiceImpl.login()
package com.yap.springboot2jwt.service.impl;
import com.yap.springboot2jwt.entity.User;
import com.yap.springboot2jwt.service.UserService;
import org.springframework.stereotype.Service;
/**
* @author yap
*/
@Service
public class UserServiceImpl implements UserService {
@Override
public User login(User user) {
String username, password;
if (user == null || (username = user.getUsername()) == null || (password = user.getPassword()) == null) {
return null;
}
String usernameFromDb = "admin";
String passwordFromDb = "123";
if (usernameFromDb.equals(username) && passwordFromDb.equals(password)) {
return new User(1, "admin", "123", "admin.jpg");
} else {
return null;
}
}
}
- src:
c.y.s.controller.UserController.login()and.execute()
package com.yap.springboot2jwt.controller;
import com.yap.springboot2jwt.annotation.TokenAuth;
import com.yap.springboot2jwt.entity.User;
import com.yap.springboot2jwt.service.UserService;
import com.yap.springboot2jwt.util.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author yap
*/
@RestController
@RequestMapping("/api/user")
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping("login")
public String login(User user) {
User userFromDb = userService.login(user);
if (userFromDb != null) {
return TokenUtil.build(userFromDb);
}
return "login fail...";
}
@TokenAuth
@RequestMapping("execute")
public String execute() {
return "execute()...";
}
}
- src:
c.y.s.interceptor.AuthInterceptor
package com.yap.springboot2jwt.interceptor;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.yap.springboot2jwt.annotation.TokenAuth;
import com.yap.springboot2jwt.util.TokenUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* @author yap
*/
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object obj) throws IOException {
// 若请求URL不指向动作方法,直接放行
if (!(obj instanceof HandlerMethod)) {
return true;
}
// 若动作方法上没有标记@TokenAuth,直接放行
Method method = ((HandlerMethod) obj).getMethod();
if (!method.isAnnotationPresent(TokenAuth.class)) {
return true;
}
// 从请求头中获取token,若没获取成功,尝试从请求参数中获取,若均失败返回错误。
String token = req.getHeader("token");
if (token == null || "".equals(token)) {
token = req.getParameter("token");
if (token == null || "".equals(token)) {
throw new RuntimeException("token is null...");
}
}
DecodedJWT decodedJwt = TokenUtil.verify(token);
if (decodedJwt == null) {
throw new RuntimeException("token verify error...");
}
int id = decodedJwt.getClaim("id").asInt();
String username = decodedJwt.getClaim("username").asString();
String avatar = decodedJwt.getClaim("avatar").asString();
req.setAttribute("id", id);
req.setAttribute("username", username);
req.setAttribute("avatar", avatar);
return true;
}
}
- src:
c.y.s.config.AuthInterceptorConfig
package com.yap.springboot2jwt.config;
import com.yap.springboot2jwt.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author yap
*/
@Configuration
public class AuthInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/user/login");
}
}
jwt内置负载项
概念: jwt内置负载项:
| 标识符 | 全称 | 概述 | 备注 |
|---|---|---|---|
| iss | issuer | token发行人 | 可由 builder.withIssuer() 设置 |
| iat | issued at | token发行时间 | 可由 builder.withIssuedAt() 设置 |
| sub | subject | token主题内容 | 可由 builder.withSubject() 设置 |
| aud | audience | token接收人 | 可由 builder.withAudience() 设置 |
| exp | expiration | token过期时间戳 | 可由 builder.withExpiration() 设置 |
| nbf | not before | token生效时间戳 | 可由 builder.withNotBefore() 设置 |
| jti | jwt id | token的id | 可由 builder.withJWTId() 设置 |
jwt自写工具类
package com.yap.z11springboot2.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
/**
* @Author Yap
*/
public class JwtUtil {
/**
* 密钥:不能暴露
*/
private static String secretKey = "wa134679";
public static String geneToken(User user) {
Integer id;
String username, avatar;
if (user == null || (id = user.getId()) == null || (username = user.getUsername()) == null || (avatar = user.getAvatar()) == null) {
return null;
}
// 设置负载(发行人,发行时间,过期时间)和签名
// 每个claim对象都对应payload中的一个kv对
return Jwts.builder()
.claim("id", id)
.claim("username", username)
.claim("avatar", avatar)
.setSubject("yap")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 3600 * 24L))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public static Claims checkToken(String token) {
final Claims claims;
try {
// 获取payload
claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
} catch (Exception e) {
e.printStackTrace();
return null;
}
return claims;
}
}
- test
package com.yap.z11springboot2.jwt;
import io.jsonwebtoken.Claims;
import org.junit.jupiter.api.Test;
/**
* @Author Yap
*/
class JwtTest {
@Test
void geneJwt() {
System.out.println(JwtUtil.geneToken(new User(1, "qaz129", "wa67890", "yap.jpg")));
}
@Test
void checkJwt() {
// 将上一个方法生成的token拿过来
// 这个字符串稍微改动一点都会返回一个null的Claims
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6IjEyMyIsInVzZXJuYW1lIjoiYWRtaW4ifQ.P6fnBFeFf01_vLBMzMrsG_5PidZSop5lry3You4yRIk";
Claims claims = JwtUtil.checkToken(token);
if (claims != null) {
//System.out.println(claims.get("id"));
System.out.println(claims.get("username"));
System.out.println(claims.get("password"));
}else {
System.out.println("illegal token...");
}
}
}