基础项目总结
MyBatis-plus
设置逻辑删除
由于数据库中所有表均采用逻辑删除策略,所以查询数据时均需要增加过滤条件is_deleted=0
。
上述操作虽不难实现,但是每个查询接口都要考虑到,也显得有些繁琐。为简化上述操作,可以使用Mybatis-Plus提供的逻辑删除功能,它可以自动为查询操作增加is_deleted=0
过滤条件,并将删除操作转为更新语句。具体配置如下,详细信息可参考官方文档。
-
步骤一:在
application.yml
中增加如下内容mybatis-plus: global-config: db-config: logic-delete-field: isDeleted # 全局逻辑删除的实体字段名(配置后可以忽略不配置步骤二) logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
-
步骤二:在实体类中的删除标识字段上增加
@TableLogic
注解@Data public class BaseEntity { @Schema(description = "主键") @TableId(value = "id", type = IdType.AUTO) private Long id; @Schema(description = "创建时间") @JsonIgnore private Date createTime; @Schema(description = "更新时间") @JsonIgnore private Date updateTime; @Schema(description = "逻辑删除") @JsonIgnore // 该字段不参与序列化 @TableLogic // 逻辑删除字段 @TableField("is_deleted") private Byte isDeleted; }
注意:
逻辑删除功能只对Mybatis-Plus自动注入的sql起效,也就是说,对于手动在Mapper.xml
文件配置的sql不会生效,需要单独考虑。
忽略特定字段
通常情况下接口响应的Json对象中并不需要create_time
、update_time
、is_deleted
等字段,这时只需在实体类中的相应字段添加@JsonIgnore
注解,该字段就会在序列化时被忽略。
具体配置如下,详细信息可参考Jackson官方文档。
@Data
public class BaseEntity {
@Schema(description = "主键")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "创建时间")
@JsonIgnore
@TableField(value = "create_time")
private Date createTime;
@Schema(description = "更新时间")
@JsonIgnore
@TableField(value = "update_time")
private Date updateTime;
@Schema(description = "逻辑删除")
@JsonIgnore
@TableField("is_deleted")
private Byte isDeleted;
}
字段自动填充
保存或更新数据时,前端通常不会传入isDeleted
、createTime
、updateTime
这三个字段,因此我们需要手动赋值。但是数据库中每张表都有上述字段,所以手动去赋值就显得有些繁琐。为简化上述操作,我们可采取以下措施。
-
is_deleted
字段:可将数据库中该字段的默认值设置为0。 -
create_time
和update_time
:可使用mybatis-plus的自动填充功能,所谓自动填充,就是通过统一配置,在插入或更新数据时,自动为某些字段赋值,具体配置如下,详细信息可参考官方文档。-
为相关字段配置触发填充的时机,例如
create_time
需要在插入数据时填充,而update_time
需要在更新数据时填充。具体配置如下,观察@TableField
注解中的fill
属性。@Data public class BaseEntity { @Schema(description = "主键") @TableId(value = "id", type = IdType.AUTO) private Long id; @Schema(description = "创建时间") @JsonIgnore @TableField(value = "create_time", fill = FieldFill.INSERT) private Date createTime; @Schema(description = "更新时间") @JsonIgnore @TableField(value = "update_time", fill = FieldFill.UPDATE) private Date updateTime; @Schema(description = "逻辑删除") @JsonIgnore @TableLogic @TableField("is_deleted") private Byte isDeleted; }
-
配置自动填充的内容,具体配置如下
在common模块下创建
com.atguigu.lease.common.mybatisplus.MybatisMetaObjectHandler
类,内容如下:@Component public class MybatisMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); this.strictInsertFill(metaObject, "isDeleted", Byte.class, (byte)(0)); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); } }
在做完上述配置后,当写入数据时,Mybatis-Plus会自动将实体对象的
create_time
字段填充为当前时间,当更新数据时,则会自动将实体对象的update_time
字段填充为当前时间。 -
配置转换器
示例:
@Operation(summary = "(根据类型)查询标签列表")
@GetMapping("list")
public Result<List<LabelInfo>> labelList(@RequestParam(required = false) ItemType type) {
LambdaQueryWrapper<LabelInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(type != null, LabelInfo::getType, type);
List<LabelInfo> list = service.list(queryWrapper);
return Result.ok(list);
}
出现问题:上述接口的功能是根据type(公寓/房间),查询标签列表。由于这个type字段在数据库,实体类、前后端交互的过程中有多种不同的形式,因此在请求和响应的过程中,type字段会涉及到多吃类型转换。
解决方案:
public interface BaseEnum {
Integer getCode();
String getName();
}
@Component
public class StringToBaseEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
@Override
public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) {
return new Converter<String, T>() {
@Override
public T convert(String source) {
for (T enumConstant : targetType.getEnumConstants()) {
if (enumConstant.getCode().equals(Integer.valueOf(source))) {
return enumConstant;
}
}
throw new IllegalArgumentException("非法的枚举值:" + source);
}
};
}
}
注册上述的转换器
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired
private StringToBaseEnumConverterFactory stringToBaseEnumConverterFactory;
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(this.stringToBaseEnumConverterFactory);
}
}
Mybatis-plus提供了一个通用的处理枚举类类型的TypeHandler,其使用十分简单,只需在ItemType
枚举类的code
属性上增加一个注解@EnumValue
,Mybatis-Plus便可完成从ItemType
对象到code
属性之间的相互映射
配置分页
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/**
* 新的分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
实现方案
-
基本使用方式
// 创建分页参数 Page<User> page = new Page<>(1, 10); // 当前页,每页大小 // 执行分页查询 Page<User> result = userMapper.selectPage(page, null); // 获取分页数据 List<User> records = result.getRecords(); // 当前页数据 long total = result.getTotal(); // 总记录数 long pages = result.getPages(); // 总页数
-
带条件的分页查询
// 创建分页参数 Page<User> page = new Page<>(1, 10); // 创建查询条件 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(User::getName, "张") .ge(User::getAge, 18); // 执行分页查询 Page<User> result = userMapper.selectPage(page, wrapper);
-
自定义sql分页查询
Mapper接口方法
@Mapper public interface UserMapper extends BaseMapper<User> { // 自定义分页查询 IPage<User> selectUserPage(IPage<User> page, @Param("state") Integer state); }
XML 映射文件:
<select id="selectUserPage" resultType="com.example.entity.User"> SELECT * FROM user WHERE state = #{state} </select>
service调用:
Page<User> page = new Page<>(1, 10); IPage<User> userPage = userMapper.selectUserPage(page, 1);
文件上传
图片上传
参考文档:
-
配置Minio Client
-
引入依赖
<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> </dependency>
-
xml配置Minio相关参数
minio: endpoint: http://<hostname>:<port> access-key: <access-key> secret-key: <secret-key> bucket-name: <bucket-name>
-
创建相关参数实体类
@ConfigurationProperties(prefix = "minio") @Data public class MinioProperties { private String endpoint; private String accessKey; private String secretKey; private String bucketName; }
-
创建配置类
@Configuration @EnableConfigurationProperties(MinioProperties.class) public class MinioConfiguration { @Autowired private MinioProperties properties; @Bean public MinioClient minioClient() { return MinioClient.builder().endpoint(properties.getEndpoint()).credentials(properties.getAccessKey(), properties.getSecretKey()).build(); } }
-
-
开发图片上传接口
-
编写controller层逻辑
在
FileUploadController
中增加如下内容@Tag(name = "文件管理") @RequestMapping("/admin/file") @RestController public class FileUploadController { @Autowired private FileService service; @Operation(summary = "上传文件") @PostMapping("upload") public Result<String> upload(@RequestParam MultipartFile file) { String url = service.upload(file); return Result.ok(url); } }
说明:
MultipartFile
是Spring框架中用于处理文件上传的类,它包含了上传文件的信息(如文件名、文件内容等)-
编写service层逻辑
-
在
FileService
中增加如下内容String upload(MultipartFile file);
-
在
FileServiceImpl
中增加如下内容@Autowired private MinioProperties properties; @Autowired private MinioClient client; @Override public String upload(MultipartFile file) { try { boolean bucketExists = client.bucketExists(BucketExistsArgs.builder().bucket(properties.getBucketName()).build()); if (!bucketExists) { client.makeBucket(MakeBucketArgs.builder().bucket(properties.getBucketName()).build()); client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(properties.getBucketName()).config(createBucketPolicyConfig(properties.getBucketName())).build()); } String filename = new SimpleDateFormat("yyyyMMdd").format(new Date()) + "/" + UUID.randomUUID() + "-" + file.getOriginalFilename(); client.putObject(PutObjectArgs.builder(). bucket(properties.getBucketName()). object(filename). stream(file.getInputStream(), file.getSize(), -1). contentType(file.getContentType()).build()); return String.join("/", properties.getEndpoint(), properties.getBucketName(), filename); } catch (Exception e) { e.printStackTrace(); } return null; } private String createBucketPolicyConfig(String bucketName) { return """ { "Statement" : [ { "Action" : "s3:GetObject", "Effect" : "Allow", "Principal" : "*", "Resource" : "arn:aws:s3:::%s/*" } ], "Version" : "2012-10-17" } """.formatted(bucketName); }
注意:
上述
createBucketPolicyConfig
方法的作用是生成用于描述指定bucket访问权限的JSON字符串。最终生成的字符串格式如下,其表示,允许(Allow
)所有人(*
)获取(s3:GetObject
)指定桶(<bucket-name>
)的内容。{ "Statement" : [ { "Action" : "s3:GetObject", "Effect" : "Allow", "Principal" : "*", "Resource" : "arn:aws:s3:::<bucket-name>/*" } ], "Version" : "2012-10-17" }
由于公寓、房间的图片为公开信息,所以将其设置为所有人可访问。
-
-
-
异常处理
-
问题说明:
上述代码只是对
MinioClient
方法抛出的各种异常进行了捕获,然后打印异常信息,目前这种处理逻辑,无论Minio是否发生异常,前端在上传文件时,总是会受到成功的响应信息。可按照以下步骤进行操作,查看具体现象关闭虚拟机中的Minio服务
systemctl stop minio
启动项目,并上传文件,观察接收的响应信息
-
问题解决思路
-
为保证前端能够接收到正常的错误提示信息,应该将Service方法的异常抛出到Controller方法中,然后在Controller方法中对异常进行捕获并处理。具体操作如下
Service层代码
@Override public String upload(MultipartFile file) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException{ boolean bucketExists = minioClient.bucketExists( BucketExistsArgs.builder() .bucket(properties.getBucketName()) .build()); if (!bucketExists) { minioClient.makeBucket( MakeBucketArgs.builder() .bucket(properties.getBucketName()) .build()); minioClient.setBucketPolicy( SetBucketPolicyArgs.builder() .bucket(properties.getBucketName()) .config(createBucketPolicyConfig(properties.getBucketName())) .build()); } String filename = new SimpleDateFormat("yyyyMMdd").format(new Date()) + "/" + UUID.randomUUID() + "-" + file.getOriginalFilename(); minioClient.putObject( PutObjectArgs.builder() .bucket(properties.getBucketName()) .stream(file.getInputStream(), file.getSize(), -1) .object(filename) .contentType(file.getContentType()) .build()); return String.join("/",properties.getEndpoint(),properties.getBucketName(),filename); }
Controller层代码
public Result<String> upload(@RequestParam MultipartFile file) { try { String url = service.upload(file); return Result.ok(url); } catch (Exception e) { e.printStackTrace(); return Result.fail(); } }
-
-
-
全局异常处理
-
按照上述写法,所有的Controller层方法均需要增加
try-catch
逻辑,使用Spring MVC提供的全局异常处理功能,可以将所有处理异常的逻辑集中起来,进而统一处理所有异常,使代码更容易维护。具体用法如下,详细信息可参考官方文档:
在common模块中创建
com.atguigu.lease.common.exception.GlobalExceptionHandler
类,内容如下@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(LeaseException.class) @ResponseBody public Result error(LeaseException e){ e.printStackTrace(); return Result.fail(e.getCode(), e.getMessage()); } }
上述代码中的关键注解的作用如下
@ControllerAdvice
用于声明处理全局Controller方法异常的类@ExceptionHandler
用于声明处理异常的方法,value
属性用于声明该方法处理的异常类型@ResponseBody
表示将方法的返回值作为HTTP的响应体注意:
全局异常处理功能由SpringMVC提供,因此需要在common模块的
pom.xml
中引入如下依赖<!--spring-web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
修改Controller层代码
由于前文的
GlobalExceptionHandler
会处理所有Controller方法抛出的异常,因此Controller层就无需关注异常的处理逻辑了,因此Controller层代码可做出如下调整。public Result<String> upload(@RequestParam MultipartFile file) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { String url = service.upload(file); return Result.ok(url); }
-
-
-
自定义异常
@Data public class LeaseException extends RuntimeException { //异常状态码 private Integer code; /** * 通过状态码和错误消息创建异常对象 * @param message * @param code */ public LeaseException(String message, Integer code) { super(message); this.code = code; } /** * 根据响应结果枚举对象创建异常对象 * @param resultCodeEnum */ public LeaseException(ResultCodeEnum resultCodeEnum) { super(resultCodeEnum.getMessage()); this.code = resultCodeEnum.getCode(); } @Override public String toString() { return "LeaseException{" + "code=" + code + ", message=" + this.getMessage() + '}'; } }
密码处理
用户的密码通常不会直接以明文的形式保存到数据库中,而是会先经过处理,然后将处理之后得到的'密文'保存到数据库,这样能够降低数据库泄露导致的用户账号安全问题。
密码通常会使用一些简单单向函数处理,常用于处理密码的单向函数(算法)有MD5,SHA-256等,
-
Apache Commons提供了一个工具类
DigestUtils
,其中就包含上述算法的实现。Apache Commons是Apache软件基金会下的一个项目,其致力于提供可重用的开源软件,其中包含了很多易于使用的现成工具。
使用该工具类需引入
commons-codec
依赖,在common模块的pom.xml中增加如下内容<dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency>
-
编码
@Operation(summary = "保存或更新后台用户信息") @PostMapping("saveOrUpdate") public Result saveOrUpdate(@RequestBody SystemUser systemUser) { if(systemUser.getPassword() != null){ //将密码的明文加密变成密文 systemUser.setPassword(DigestUtils.md5Hex(systemUser.getPassword())); } service.saveOrUpdate(systemUser); return Result.ok(); }
登录管理
有两种常见的认证方案,分别基于session的认证
和基于token
的认证:
-
基于session的认证
该方案的特点:
- 登录用户信息保存在服务端内存中,若访问量增加,单台节点压力会较大
- 随用户规模增大,若后台升级为集群,则需要解决集群中各服务器登录状态共享的问题。
-
基于token
该方案的特点
- 登录状态保存在客户端,服务器没有存储开销
- 客户端发起的每个请求自身均携带登录状态,所以即使后台为集群,也不会面临登录状态共享的问题。
token详解
我们所说的Token,通常指JWT(JSON Web TOKEN)。JWT是一种轻量级的安全传输方式,用于在两个实体之间传递信息,通常用于身份验证和信息传递。
JWT是一个字符串,如下图所示,该字符串由三部分组成,三部分由.
分隔。三个部分分别被称为
header
(头部)payload
(负载)signature
(签名)
各部分的作用如下
-
Header(头部)
Header部分是由一个JSON对象经过
base64url
编码得到的,这个JSON对象用于保存JWT 的类型(typ
)、签名算法(alg
)等元信息,例如{ "alg": "HS256", "typ": "JWT" }
-
Payload(负载)
也称为 Claims(声明),也是由一个JSON对象经过
base64url
编码得到的,用于保存要传递的具体信息。JWT规范定义了7个官方字段,如下:- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
除此之外,我们还可以自定义任何字段,例如
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
-
Signature(签名)
由头部、负载和秘钥一起经过(header中指定的签名算法)计算得到的一个字符串,用于防止消息被篡改。
登录流程
登录管理共需三个接口,分别是获取图形验证码、登录、获取登录用户个人信息,除此之外,我们还需为所有受保护的接口增加验证JWT合法性的逻辑,这一功能可通过HandlerInterceptor
来实现。
-
验证码生成工具
本项目使用开源的验证码生成工具EasyCaptcha,其支持多种类型的验证码,例如gif、中文、算术等,并且简单易用,具体内容可参考其官方文档。
在common模块的pom.xml文件中增加如下内容
<dependency> <groupId>com.github.whvcse</groupId> <artifactId>easy-captcha</artifactId> </dependency>
-
获取图形验证码
@Autowired private StringRedisTemplate redisTemplate; @Override public CaptchaVo getCaptcha() { SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4); specCaptcha.setCharType(Captcha.TYPE_DEFAULT); String code = specCaptcha.text().toLowerCase(); String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID(); String image = specCaptcha.toBase64(); redisTemplate.opsForValue().set(key, code, RedisConstant.ADMIN_LOGIN_CAPTCHA_TTL_SEC, TimeUnit.SECONDS); return new CaptchaVo(image, key); }
-
登录接口
-
登录接口需要为登录成功的用户创建并返回JWT,本项目使用开源的JWT工具Java-JWT,配置如下,具体内容可参考官方文档。
-
引入Maven依赖
在common模块的pom.xml文件中增加如下内容
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <scope>runtime</scope> </dependency>
-
创建JWT工具类
在common模块下创建
com.atguigu.lease.common.utils.JwtUtil
工具类,内容如下public class JwtUtil { private static long tokenExpiration = 60 * 60 * 1000L; private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes()); // key 需要有32位 public static String createToken(Long userId, String username) { String token = Jwts.builder(). setSubject("USER_INFO"). setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)). claim("userId", userId). claim("username", username). signWith(tokenSignKey). compressWith(CompressionCodecs.GZIP). compact(); return token; } public static Claims parseToken(String token) { try { Jws<Claims> claimsJws = Jwts.parserBuilder(). setSigningKey(tokenSignKey). build().parseClaimsJws(token); return claimsJws.getBody(); } catch (ExpiredJwtException e) { throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED); } catch (JwtException e) { throw new LeaseException(ResultCodeEnum.TOKEN_INVALID); } } }
-
-
编写Controller层逻辑
在
LoginController
中增加如下内容@Operation(summary = "登录") @PostMapping("login") public Result<String> login(@RequestBody LoginVo loginVo) { String token = service.login(loginVo); return Result.ok(token); }
-
编写Service层逻辑
-
在
LoginService
中增加如下内容String login(LoginVo loginVo);
-
在
LoginServiceImpl
中增加如下内容@Override public String login(LoginVo loginVo) { //1.判断是否输入了验证码 if (!StringUtils.hasText(loginVo.getCaptchaCode())) { throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOT_FOUND); } //2.校验验证码 String code = redisTemplate.opsForValue().get(loginVo.getCaptchaKey()); if (code == null) { throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_EXPIRED); } if (!code.equals(loginVo.getCaptchaCode().toLowerCase())) { throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_ERROR); } //3.校验用户是否存在 LambdaQueryWrapper<SystemUser> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SystemUser::getUsername, loginVo.getUsername()); SystemUser systemUser = systemUserMapper.selectOne(queryWrapper); if (systemUser == null) { throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST_ERROR); } //4.校验用户是否被禁 if (systemUser.getStatus() == BaseStatus.DISABLE) { throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR); } //5.校验用户密码 if (!systemUser.getPassword().equals(DigestUtils.sha256Hex(loginVo.getPassword()))) { throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR); } //6.创建并返回TOKEN return JwtUtil.createToken(systemUser.getId(), systemUser.getUsername()); }
-
-
编写HandlerInterceptor
我们需要为所有受保护的接口增加校验JWT合法性的逻辑,否则,登录功能将没有任何意义。具体实现如下,有关
HanderInterceptor
的相关内容,可参考官方文档。-
编写HandlerInterceptor
在web-admin模块中创建
com.atguigu.lease.web.admin.custom.interceptor.AuthenticationInterceptor
类,内容如下@Component public class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("access_token"); if (token == null) { throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH); } else { JwtUtil.parseToken(token); } return true; } }
注意:
我们约定,前端登录后,后续请求都将JWT,放置于HTTP请求的Header中,其Header的key为
access_token
。 -
注册HandlerInterceptor
在web-admin模块的
com.atguigu.lease.web.admin.custom.config.WebMvcConfiguration
中增加如下内容@Autowired private AuthenticationInterceptor authenticationInterceptor; @Value("${admin.auth.path-patterns.include}") private String[] includePathPatterns; @Value("${admin.auth.path-patterns.exclude}") private String[] excludePathPatterns; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(this.authenticationInterceptor).addPathPatterns(includePathPatterns).excludePathPatterns(excludePathPatterns); }
-
在
application.yml
中增加拦截器匹配和排除的接口路径,如下admin: auth: path-patterns: include: /admin/** exclude: /admin/login/**
-
-
过滤器与拦截器
- 过滤器与拦截器触发时机不一样,过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在Servlet处理完成后,返回给前端之前。
- 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,因为拦截器是spring提供并管理的,spring的功能可以被拦截器使用,在拦截器里注入一个service,可以调用业务逻辑。而过滤器是JavaEE标准,只需依赖servlet api ,不需要依赖spring。
- 过滤器的实现基于回调函数。而拦截器(代理模式)的实现基于反射
- Filter是依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用。
- Filter的执行由Servlet容器回调完成,而拦截器通常通过动态代理(反射)的方式来执行。
- Filter的生命周期由Servlet容器管理,而拦截器则可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,因此使用会更方便。
过滤器与拦截器非常相似,但是它们有很大的区别:
最简单明了的区别就是过滤器可以修改request,而拦截器不能
过滤器需要在servlet容器中实现,拦截器可以适用于javaEE,javaSE等各种环境
拦截器可以调用IOC容器中的各种依赖,而过滤器不能
过滤器只能在请求的前后使用,而拦截器可以详细到每个方法
总体来说:
过滤器就是筛选出你要的东西,比如requeset中你要的那部分
拦截器在做安全方面用的比较多,比如终止一些流程
过滤器(Filter) :可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息。
拦截器(Interceptor):可以拿到你请求的控制器和方法,却拿不到请求方法的参数。
切片(Aspect): 可以拿到方法的参数,但是却拿不到http请求和响应的对象
过滤器
两种方式:
1、使用spring boot提供的FilterRegistrationBean注册Filter
2、使用原生servlet注解定义Filter
两种方式的本质都是一样的,都是去FilterRegistrationBean注册自定义Filter
方式一: (使用spring boot提供的FilterRegistrationBean注册Filter )
-
先定义Filter:
package com.corwien.filter; import javax.servlet.*; import java.io.IOException; public class MyFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // do something 处理request 或response // doFilter()方法中的servletRequest参数的类型是ServletRequest,需要转换为HttpServletRequest类型方便调用某些方法 System.out.println("filter1"); // 调用filter链中的下一个filter HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String ip = request.getRemoteAddr(); String url = request.getRequestURL().toString(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date d = new Date(); String date = sdf.format(d); System.out.printf("%s %s 访问了 %s%n", date, ip, url); filterChain.doFilter(request, response); } @Override public void destroy() { } }
-
注册定义Filter
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean registrationBean() { ** FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new** **MyFilter());** filterRegistrationBean.addUrlPatterns("/*"); return filterRegistrationBean; } }
-
方式一的1,2步骤可以用下面这段代码代替
@Configuration public class FilterConfig { @Bean public **FilterRegistrationBean** registFilter() { **FilterRegistrationBean registration** **= new FilterRegistrationBean(); registration.setFilter(new** **LogCostFilter());** registration.addUrlPatterns("/*"); registration.setName("LogCostFilter"); registration.setOrder(1); return registration; } } public class LogCostFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @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)); } @Override public void destroy() { }
拦截器配置
实现拦截器可以通过继承 HandlerInterceptorAdapter类也可以通过实现HandlerInterceptor这个接口。另外,如果preHandle方法return true,则继续后续处理。
public class LogCostInterceptor implements HandlerInterceptor { long start = System.currentTimeMillis();
@Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
start = System.currentTimeMillis(); return true;
}
@Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
System.out.println("Interceptor cost="+(System.currentTimeMillis()-start));
}
@Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
reHandle是请求执行前执行的,postHandler是请求结束执行的,但只有preHandle方法返回true的时候才会执行,afterCompletion是视图渲染完成后才执行,同样需要preHandle返回true,该方法通常用于清理资源等工作。
拦截器应用场景
拦截器本质上是面向切面编程(AOP),符合横切关注点的功能都可以放在拦截器中来实现,主要的应用场景包括:
- 登录验证,判断用户是否登录。
- 权限验证,判断用户是否有权限访问资源,如校验token
- 日志记录,记录请求操作日志(用户ip,访问时间等),以便统计请求访问量。
- 处理cookie、本地化、国际化、主题等。
- 性能监控,监控请求处理时长等。
- 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现)
过滤器应用场景
1)过滤敏感词汇(防止sql注入)
2)设置字符编码
3)URL级别的权限访问控制
4)压缩响应信息