如何在SpringBoot中自定义一个拦截器?
1、继承Spring中的的HandlerInterceptor,可以通过实现preHandle postHandle afterCompletion等方方法
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前客户id:{}", userId);
//将获取到的当前员工id存入线程存储空间当中,使其可以在其他地方调用
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
2、在SpringBoot中的WebMVC配置类中实现自定义拦截器的注册
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login","/user/shop/status");
}
}
SpringBoot中配置WebMVC的说明:
-
WebMvcConfigurer 为接口
-
WebMvcConfigurerAdapter 是 WebMvcConfigurer 的实现类大部分为空方法,是 WebMvcConfigurer的子类实现,由于Java8中可以使用default关键字为接口添加默认方法,为在源代码中spring5.0之后就已经弃用本类,如果需要我接着可以实现WebMvcConfigurer类。
-
WebMvcConfigurationSupport 是mvc的基本实现并包含了WebMvcConfigurer接口中的方法。
-
WebMvcAutoConfiguration 是mvc的自动装在类并部分包含了WebMvcConfigurer接口中的方法。
在SpringBoot中配置WebMVC的三种方法
-
方式1:@EnableWebMvc
-
方式2: 继承WebMvcConfigurationSupport 但是没有汇集项目中WebMvcConfigure接口实现类的功能的
-
方式3: 继承 DelegatingWebMvcConfiguration 是WebMvcConfigurationSupport的拓展子类,如果项目中也存在其他实现WebMvcConfigurer接口来提供配置的类,则可以继承DelegatingWebMvcConfiguration来替代@EnableWebMvc,两者提供的功能是一样的。
在苍穹外卖的项目中使用的方式二
拦截器和过滤器?
过滤器 和 拦截器的 6个区别,别再傻傻分不清了_拦截器和过滤器的区别-优快云博客
拦截器和过滤器的区别 - 跨境电商杂货铺 - 博客园 (cnblogs.com)
SpringBoot中使用过滤器
写个类实现Filter或者OncePerRequestFilter接口,也可使用@WebFilter注解实现对特定的url进行过滤(在启动类上要加上@ServletComponentScan开启组件扫描),最主要的操作是在doFilter中进行的。
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
注册都是在继承WebMvcConfigurer类的子类中的addCorsMappings方法中进行
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
JWT在SpringBoot中的配置,以及类似与SpringSecurity功能的实现
引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
Jwt工具类
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
}
外卖中使用的是拦截器+jwt,实现用户登录、权限认证
package com.sky.interceptor;
import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前客户id:{}", userId);
//将获取到的当前员工id存入线程存储空间当中,使其可以在其他地方调用
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
BaseContext是什么?
简单来说,BaseContext是基于ThreadLocal封装的一个工具类。
package com.sky.context;
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
employee登录的方法
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}
SpringBoot中配置全局处理器
1、自定义异常
可以自定义,当然也可以使用提供的异常类,这里专门对自定义的异常类来进行说明。
可以先定义一个BaseException,继承RuntimeException.
package com.sky.exception;
/**
* 业务异常
*/
public class BaseException extends RuntimeException {
public BaseException() {
}
public BaseException(String msg) {
super(msg);
}
}
别的自定义类,可以直接继承异常基类,只需生成两个构造方法即可。
package com.sky.exception;
/**
* 账号不存在异常
*/
public class AccountNotFoundException extends BaseException {
public AccountNotFoundException() {
}
public AccountNotFoundException(String msg) {
super(msg);
}
}
传参的方式主要还是通过父类的构造器进行的。
2、全局处理异常
由于自定的异常都继承了BaseException类,这样就可以通过多态,统一的处理所有的异常了;异常是由SpringMVC处理、在handler中进行的。
需要使用@RestControllerAdivce和@ExceptionHandler(放到方法上)来实现异常处理.
package com.sky.handler;
import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
/**
* 处理SQL异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'zhangsan' for key 'employee.idx_username'
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
} else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
}
@ExceptionHandler(value = 默认应该就是函数参数的class)当然也可以手动设置ExceptionType.class
@RestControllerAdvice注解加上后,出现错误,然后就会给前端响应请求了。
SpringBoot中的文档、日志配置
日志功能
主要是通过@slf4j这个注解,加上注解之后,就可以通过log.info()进行日志的输出。
文档、接口测试
主要是通过swwagger来实现。
使用文档的地址是在下面方法中设置的(WebMvcConfiguration)。
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
yaml信息配置
可以事先在application.yaml中定义一些内容:
xfxh:
apiHost: spark-api.xf-yun.com
apiPath: ws(s)://spark-api.xf-yun.com/v3.1/chat
appId: 1946577d
apiKey: ZmRlNjkzM2VjZTQ4ZGZjNGFmZTQ3YWM0
apiSecret: e323c012efc10b026f074964d67813d4
然后在写一个entity
public class config {
private String apiHost;
private String apiPath;
private String appId;
private String apiKey;
private String apiSecret;
}
加上@Configuration 和 @ConfigurationProperties(prefix = "...")
邮箱注册验证
首先我们来理一下思路,发送验证码的整个过程,前端输入邮箱账号,点击获取验证码,请求后台接口,由我们的后台java程序将验证码存储在redis并设置相应的过期时间,以及实现发送验证码到邮箱的功能,用户如果填写的自己的邮箱,就能成功获取验证码,进行注册的时候,获取用户输入邮箱对应的验证码与用户输入的验证码进行匹配,匹配成功就进行注册,否则就提示验证码无效。
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
发送验证码、认证
@GetMapping("/getCode")
@ApiOperation("获得验证码")
public ResultJson<String> getRegisterCode(@RequestParam String email) throws Exception{
//TODO:查看email是否符合注册的条件的业务代码
//TODO:生成code,并发送邮箱,最后将code放到Session和Redis中(设置2min过期),发送给前端
log.info("邮箱:{}",email);
SimpleMailMessage message = new SimpleMailMessage();
Random random = new Random();
String code = String.valueOf(random.nextInt(899999) + 10000);
message.setFrom("19846811030@163.com");
message.setTo(email);
message.setSubject("系统验证码");
message.setText("邮箱验证码为: " + code +" ,请勿发送给他人,两分钟有效!");
try {
mailSender.send(message);
//保存到Redis中
redisUtil.setex(email+":code",code,1200);
log.info("验证码邮件已发送。");
return ResultJson.success(code);
} catch (Exception e) {
log.error("发送验证码邮件时发生异常了!", e);
return ResultJson.error("验证码发送失误,请稍后再试!");
}
}
配置信息
spring:
mail:
host: smtp.163.com
username:
password:
default-encoding: utf-8
注册的逻辑代码
@PostMapping("/register")
@ApiOperation("注册")
public ResultJson<String> register(@RequestBody RegisterVO registerVO){
log.info("用户注册:{}",registerVO);
//从redis中获得code
String code = (String) redisUtil.get(registerVO.getEmail()+":code");
if (!code.equals(registerVO.getCode()))
return ResultJson.error("验证错误!");
//TODO:注册的业务逻辑代码
//最后返回
return ResultJson.success("注册成功!");
}
RestTemplate
SpringBoot中已经集成了发送http请求的工具
使用之前必须在配置类中注册RestTemplate
@Configuration
public class WebConfiguration {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder){
return builder
//设置连接超时时间
.setConnectTimeout(Duration.ofSeconds(5000))
//设置读取超时时间
.setReadTimeout(Duration.ofSeconds(5000))
//设置认证信息
.basicAuthentication("username","password")
//设置根路径
.rootUri("https://api.test.com/")
//构建
.build();
}
}
简单注入也可以
@Configuration
public class WebConfiguration {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}