尚硅谷尚庭公寓-02
MinIo实现图片上传功能
配置Minio Client
引入Minio Maven依赖
在common模块的
pom.xml
文件增加如下内容:1
2
3
4<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
</dependency>配置Minio相关参数
在
application.yml
中配置Minio的endpoint
、accessKey
、secretKey
、bucketName
等参数1
2
3
4
5minio:
endpoint: http://<hostname>:<port>
access-key: <access-key>
secret-key: <secret-key>
bucket-name: <bucket-name>注意:上述
<hostname>
、<port>
等信息需根据实际情况进行修改。在common模块中创建
com.atguigu.lease.common.minio.MinioProperties
,内容如下1
2
3
4
5
6
7
8
public class MinioProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;
}在common模块中创建
com.atguigu.lease.common.minio.MinioConfiguration
,内容如下1
2
3
4
5
6
7
8
9
10
11
12
public class MinioConfiguration {
private MinioProperties properties;
public MinioClient minioClient() {
return MinioClient.builder().endpoint(properties.getEndpoint()).credentials(properties.getAccessKey(), properties.getSecretKey()).build();
}
}
开发图片上传接口
编写Controller层逻辑
在
FileUploadController
中增加如下内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FileUploadController {
private FileService service;
public Result<String> upload( { MultipartFile file)
String url = service.upload(file);
return Result.ok(url);
}
}说明:
MultipartFile
是Spring框架中用于处理文件上传的类,它包含了上传文件的信息(如文件名、文件内容等)。编写Service层逻辑
在
FileService
中增加如下内容1
String upload(MultipartFile file);
在
FileServiceImpl
中增加如下内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private MinioProperties properties;
private MinioClient client;
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>
)的内容。1
2
3
4
5
6
7
8
9{
"Statement" : [ {
"Action" : "s3:GetObject",
"Effect" : "Allow",
"Principal" : "*",
"Resource" : "arn:aws:s3:::<bucket-name>/*"
} ],
"Version" : "2012-10-17"
}由于公寓、房间的图片为公开信息,所以将其设置为所有人可访问。
异常处理
问题说明
上述代码只是对
MinioClient
方法抛出的各种异常进行了捕获,然后打印了异常信息,目前这种处理逻辑,无论Minio是否发生异常,前端在上传文件时,总是会受到成功的响应信息。可按照以下步骤进行操作,查看具体现象关闭虚拟机中的Minio服务
1
systemctl stop minio
启动项目,并上传文件,观察接收的响应信息
问题解决思路
为保证前端能够接收到正常的错误提示信息,应该将Service方法的异常抛出到Controller方法中,然后在Controller方法中对异常进行捕获并处理。具体操作如下
Service层代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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层代码
1
2
3
4
5
6
7
8
9public Result<String> upload( { 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
类,内容如下1
2
3
4
5
6
7
8
9
10
public class GlobalExceptionHandler {
public Result error(Exception e){
e.printStackTrace();
return Result.fail();
}
}上述代码中的关键注解的作用如下
@ControllerAdvice
用于声明处理全局Controller方法异常的类@ExceptionHandler
用于声明处理异常的方法,value
属性用于声明该方法处理的异常类型@ResponseBody
表示将方法的返回值作为HTTP的响应体注意:
全局异常处理功能由SpringMVC提供,因此需要在common模块的
pom.xml
中引入如下依赖1
2
3
4
5<!--spring-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>修改Controller层代码
由于前文的
GlobalExceptionHandler
会处理所有Controller方法抛出的异常,因此Controller层就无序关注异常的处理逻辑了,因此Controller层代码可做出如下调整。1
2
3
4
5public Result<String> upload( MultipartFile file)throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
String url = service.upload(file);
return Result.ok(url);
}
Spring Boot定时任务
启用Spring Boot定时任务
在SpringBoot启动类上增加
@EnableScheduling
注解,如下1
2
3
4
5
6
7
public class AdminWebApplication {
public static void main(String[] args) {
SpringApplication.run(AdminWebApplication.class, args);
}
}编写定时逻辑
在web-admin模块下创建
com.atguigu.lease.web.admin.schedule.ScheduledTasks
类,内容如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ScheduledTasks {
private LeaseAgreementService leaseAgreementService;
public void checkLeaseStatus() {
LambdaUpdateWrapper<LeaseAgreement> updateWrapper = new LambdaUpdateWrapper<>();
Date now = new Date();
updateWrapper.le(LeaseAgreement::getLeaseEndDate, now);
updateWrapper.eq(LeaseAgreement::getStatus, LeaseStatus.SIGNED);
updateWrapper.in(LeaseAgreement::getStatus, LeaseStatus.SIGNED, LeaseStatus.WITHDRAWING);
leaseAgreementService.update(updateWrapper);
}
}知识点:
SpringBoot中的cron表达式语法如下
1
2
3
4
5
6
7
8
9┌───────────── second (0-59)
│ ┌───────────── minute (0 - 59)
│ │ ┌───────────── hour (0 - 23)
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
│ │ │ │ │ ┌───────────── day of the week (0 - 7)
│ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)
│ │ │ │ │ │
* * * * * *
密码加密处理
用户的密码通常不会直接以明文的形式保存到数据库中,而是会先经过处理,然后将处理之后得到的”密文”保存到数据库,这样能够降低数据库泄漏导致的用户账号安全问题。
密码通常会使用一些单向函数进行处理,如下图所示
常用于处理密码的单向函数(算法)有MD5、SHA-256等,Apache Commons提供了一个工具类DigestUtils
,其中就包含上述算法的实现。
Apache Commons是Apache软件基金会下的一个项目,其致力于提供可重用的开源软件,其中包含了很多易于使用的现成工具。
使用该工具类需引入commons-codec
依赖,在common模块的pom.xml中增加如下内容
1 | <dependency> |
Mybatis-Plus update strategy
使用Mybatis-Plus提供的更新方法时,若实体中的字段为null
,默认情况下,最终生成的update语句中,不会包含该字段。若想改变默认行为,可做以下配置。
全局配置
在application.yml
中配置如下参数1
2
3
4mybatis-plus:
global-config:
db-config:
update-strategy: <strategy>注:上述
<strategy>
可选值有:ignore
、not_null
、not_empty
、never
,默认值为not_null
ignore
:忽略空值判断,不管字段是否为空,都会进行更新not_null
:进行非空判断,字段非空才会进行判断not_empty
:进行非空判断,并进行非空串(””)判断,主要针对字符串类型never
:从不进行更新,不管该字段为何值,都不更新
局部配置
在实体类中的具体字段通过@TableField
注解进行配置,如下:1
2
3
private String password;
登录功能的实现
背景知识
1. 认证方案概述
有两种常见的认证方案,分别是基于Session的认证和基于Token的认证,下面逐一进行介绍
基于Session
基于Session的认证流程如下图所示
该方案的特点
- 登录用户信息保存在服务端内存中,若访问量增加,单台节点压力会较大
- 随用户规模增大,若后台升级为集群,则需要解决集群中各服务器登录状态共享的问题。
基于Token
基于Token的认证流程如下图所示
该方案的特点- 登录状态保存在客户端,服务器没有存储开销
- 客户端发起的每个请求自身均携带登录状态,所以即使后台为集群,也不会面临登录状态共享的问题。
2. Token详解
本项目采用基于Token的登录方案,下面详细介绍Token这一概念。
我们所说的Token,通常指JWT(JSON Web TOKEN)。JWT是一种轻量级的安全传输方式,用于在两个实体之间传递信息,通常用于身份验证和信息传递。
JWT是一个字符串,如下图所示,该字符串由三部分组成,三部分由.
分隔。三个部分分别被称为
header
(头部)payload
(负载)signature
(签名)
各部分的作用如下
Header(头部)
Header部分是由一个JSON对象经过
base64url
编码得到的,这个JSON对象用于保存JWT 的类型(typ
)、签名算法(alg
)等元信息,例如1
2
3
4{
"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):编号
除此之外,我们还可以自定义任何字段,例如
1
2
3
4
5{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}Signature(签名)
由头部、负载和秘钥一起经过(header中指定的签名算法)计算得到的一个字符串,用于防止消息被篡改。
登录流程
后台管理系统的登录流程如下图所示
根据上述登录流程,可分析出,登录管理共需三个接口,分别是获取图形验证码、登录、获取登录用户个人信息,除此之外,我们还需为所有受保护的接口增加验证JWT合法性的逻辑,这一功能可通过HandlerInterceptor
来实现。
接口开发
首先在LoginController
中注入LoginService
,如下
1 |
|
1. 获取图形验证码
查看响应的数据结构
查看web-admin模块下的
com.atguigu.lease.web.admin.vo.login.CaptchaVo
,内容如下1
2
3
4
5
6
7
8
9
10
11
public class CaptchaVo {
private String image;
private String key;
}配置所需依赖
验证码生成工具
本项目使用开源的验证码生成工具EasyCaptcha,其支持多种类型的验证码,例如gif、中文、算术等,并且简单易用,具体内容可参考其官方文档。
在common模块的pom.xml文件中增加如下内容
1
2
3
4<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
</dependency>Redis
在common模块的pom.xml中增加如下内容
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>在
application.yml
中增加如下配置1
2
3
4
5
6spring:
data:
redis:
host: <hostname>
port: <port>
database: 0注意:上述
hostname
和port
需根据实际情况进行修改
编写Controller层逻辑
在
LoginController
中增加如下内容1
2
3
4
5
6
public Result<CaptchaVo> getCaptcha() {
CaptchaVo captcha = service.getCaptcha();
return Result.ok(captcha);
}编写Service层逻辑
在
LoginService
中增加如下内容1
CaptchaVo getCaptcha();
在
LoginServiceImpl
中增加如下内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private StringRedisTemplate redisTemplate;
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);
}知识点:
本项目Reids中的key需遵循以下命名规范:项目名:功能模块名:其他,例如
admin:login:123456
spring-boot-starter-data-redis
已经完成了StringRedisTemplate
的自动配置,我们直接注入即可。为方便管理,可以将Reids相关的一些值定义为常量,例如key的前缀、TTL时长,内容如下。大家可将这些常量统一定义在common模块下的
com.atguigu.lease.common.constant.RedisConstant
类中1
2
3
4
5
6
7
8public class RedisConstant {
public static final String ADMIN_LOGIN_PREFIX = "admin:login:";
public static final Integer ADMIN_LOGIN_CAPTCHA_TTL_SEC = 60;
public static final String APP_LOGIN_PREFIX = "app:login:";
public static final Integer APP_LOGIN_CODE_RESEND_TIME_SEC = 60;
public static final Integer APP_LOGIN_CODE_TTL_SEC = 60 * 10;
public static final String APP_ROOM_PREFIX = "app:room:";
}
2. 登录接口
登录校验逻辑
用户登录的校验逻辑分为三个主要步骤,分别是校验验证码,校验用户状态和校验密码,具体逻辑如下
- 前端发送
username
、password
、captchaKey
、captchaCode
请求登录。 - 判断
captchaCode
是否为空,若为空,则直接响应验证码为空
;若不为空进行下一步判断。 - 根据
captchaKey
从Redis中查询之前保存的code
,若查询出来的code
为空,则直接响应验证码已过期
;若不为空进行下一步判断。 - 比较
captchaCode
和code
,若不相同,则直接响应验证码不正确
;若相同则进行下一步判断。 - 根据
username
查询数据库,若查询结果为空,则直接响应账号不存在
;若不为空则进行下一步判断。 - 查看用户状态,判断是否被禁用,若禁用,则直接响应
账号被禁
;若未被禁用,则进行下一步判断。 - 比对
password
和数据库中查询的密码,若不一致,则直接响应账号或密码错误
,若一致则进行入最后一步。 - 创建JWT,并响应给浏览器。
- 前端发送
接口逻辑实现
查看请求数据结构
查看web-admin模块下的
com.atguigu.lease.web.admin.vo.login.LoginVo
,具体内容如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LoginVo {
private String username;
private String password;
private String captchaKey;
private String captchaCode;
}配置所需依赖
登录接口需要为登录成功的用户创建并返回JWT,本项目使用开源的JWT工具Java-JWT,配置如下,具体内容可参考官方文档。
引入Maven依赖
在common模块的pom.xml文件中增加如下内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<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
工具类,内容如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class JwtUtil {
private static long tokenExpiration = 60 * 60 * 1000L;
private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());
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).
compact();
return token;
}
}
编写Controller层逻辑
在
LoginController
中增加如下内容1
2
3
4
5
6
public Result<String> login( { LoginVo loginVo)
String token = service.login(loginVo);
return Result.ok(token);
}编写Service层逻辑
在
LoginService
中增加如下内容1
String login(LoginVo loginVo);
在
LoginServiceImpl
中增加如下内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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.校验用户是否存在
SystemUser systemUser = systemUserMapper.selectOneByUsername(loginVo.getUsername());
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.md5Hex(loginVo.getPassword()))) {
throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);
}
//6.创建并返回TOKEN
return JwtUtil.createToken(systemUser.getId(), systemUser.getUsername());
}
编写Mapper层逻辑
在
LoginMapper
中增加如下内容1
SystemUser selectOneByUsername(String username);
在
LoginMapper.xml
中增加如下内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<select id="selectOneByUsername" resultType="com.atguigu.lease.model.entity.SystemUser">
select id,
username,
password,
name,
type,
phone,
avatar_url,
additional_info,
post_id,
status
from system_user
where is_deleted = 0
and username = #{username}
</select>
编写HandlerInterceptor
我们需要为所有受保护的接口增加校验JWT合法性的逻辑。具体实现如下
在
JwtUtil
中增加parseToken
方法,内容如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public static Claims parseToken(String token){
if (token==null){
throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
}
try{
JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();
return jwtParser.parseClaimsJws(token).getBody();
}catch (ExpiredJwtException e){
throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
}catch (JwtException e){
throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
}
}编写HandlerInterceptor
在web-admin模块中创建
com.atguigu.lease.web.admin.custom.interceptor.AuthenticationInterceptor
类,内容如下,有关HanderInterceptor
的相关内容,可参考官方文档。1
2
3
4
5
6
7
8
9
10
public class AuthenticationInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("access-token");
JwtUtil.parseToken(token);
return true;
}
}注意:
我们约定,前端登录后,后续请求都将JWT,放置于HTTP请求的Header中,其Header的key为
access-token
。注册HandlerInterceptor
在web-admin模块的
com.atguigu.lease.web.admin.custom.config.WebMvcConfiguration
中增加如下内容1
2
3
4
5
6
7
private AuthenticationInterceptor authenticationInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.authenticationInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/login/**");
}
Knife4j配置
在增加上述拦截器后,为方便继续调试其他接口,可以获取一个长期有效的Token,将其配置到Knife4j的全局参数中,如下图所示。
注意:每个接口分组需要单独配置
刷新页面,任选一个接口进行调试,会发现发送请求时会自动携带该header,如下图所示
3.获取登录用户个人信息
查看请求和响应的数据结构
响应的数据结构
查看web-admin模块下的
com.atguigu.lease.web.admin.vo.system.user.SystemUserInfoVo
,内容如下1
2
3
4
5
6
7
8
9
10
public class SystemUserInfoVo {
private String name;
private String avatarUrl;
}请求的数据结构
按理说,前端若想获取当前登录用户的个人信息,需要传递当前用户的
id
到后端进行查询。但是由于请求中携带的JWT中就包含了当前登录用户的id
,故请求个人信息时,就无需再传递id
。
修改
JwtUtil
中的parseToken
方法由于需要从Jwt中获取用户
id
,因此需要为parseToken
方法增加返回值,如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public static Claims parseToken(String token){
if (token==null){
throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
}
try{
JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();
return jwtParser.parseClaimsJws(token).getBody();
}catch (ExpiredJwtException e){
throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
}catch (JwtException e){
throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
}
}编写ThreadLocal工具类
理论上我们可以在Controller方法中,使用
@RequestHeader
获取JWT,然后在进行解析,如下1
2
3
4
5
6
7
8
public Result<SystemUserInfoVo> info( { String token)
Claims claims = JwtUtil.parseToken(token);
Long userId = claims.get("userId", Long.class);
SystemUserInfoVo userInfo = service.getLoginUserInfo(userId);
return Result.ok(userInfo);
}上述代码的逻辑没有任何问题,但是这样做,JWT会被重复解析两次(一次在拦截器中,一次在该方法中)。为避免重复解析,通常会在拦截器将Token解析完毕后,将结果保存至ThreadLocal中,这样一来,我们便可以在整个请求的处理流程中进行访问了。
ThreadLocal概述
ThreadLocal的主要作用是为每个使用它的线程提供一个独立的变量副本,使每个线程都可以操作自己的变量,而不会互相干扰,其用法如下图所示。
在common模块中创建
com.atguigu.lease.common.login.LoginUserHolder
工具类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class LoginUserHolder {
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
public static void setLoginUser(LoginUser loginUser) {
threadLocal.set(loginUser);
}
public static LoginUser getLoginUser() {
return threadLocal.get();
}
public static void clear() {
threadLocal.remove();
}
}同时在common模块中创建
com.atguigu.lease.common.login.LoginUser
类1
2
3
4
5
6
7
public class LoginUser {
private Long userId;
private String username;
}修改
AuthenticationInterceptor
拦截器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AuthenticationInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("access-token");
Claims claims = JwtUtil.parseToken(token);
Long userId = claims.get("userId", Long.class);
String username = claims.get("username", String.class);
LoginUserHolder.setLoginUser(new LoginUser(userId, username));
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
LoginUserHolder.clear();
}
}编写Controller层逻辑
在
LoginController
中增加如下内容1
2
3
4
5
6
public Result<SystemUserInfoVo> info() {
SystemUserInfoVo userInfo = service.getLoginUserInfo(LoginUserHolder.getLoginUser().getUserId());
return Result.ok(userInfo);
}编写Service层逻辑
在
LoginService
中增加如下内容```java
@Override
public SystemUserInfoVo getLoginUserInfo(Long userId) {SystemUser systemUser = systemUserMapper.selectById(userId); SystemUserInfoVo systemUserInfoVo = new SystemUserInfoVo(); systemUserInfoVo.setName(systemUser.getName()); systemUserInfoVo.setAvatarUrl(systemUser.getAvatarUrl()); return systemUserInfoVo;
}