目录
[Controller Service Mapper](#Controller Service Mapper)
1.实现用户登录
在之前的项目登录中,我使用的是Session传递用户信息实现校验登录
现在学习了Jwt令牌技术后我尝试用Jwt来完成校验工作
Jwt令牌
令牌一词在网络编程一节我就有所耳闻,现在又拾了起来。
这里讲应用:令牌也就用于身份标识,类似于身份证,本质是一个字符串(类比身份证号)
JWT全称: JSON Web Token
官⽹: https://jwt.io/

1、Header(头部)头部包括令牌的类型 (即JWT)及使用的哈希算法 (如HMACSHA256或RSA)
2、Payload(负载)负载部分是存放有效信息 的地方,里面是一些自定义内容
3、Signature(签名 )此部分⽤于防⽌jwt内容被篡改, 确保安全性
1.引入依赖
使用前我们需要引入依赖:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is
preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2.生成令牌(token)
接下来就是生成令牌了
既然JWT分了三部分,那我们就从这三个部分说起
Payload(负载):我们使用Map装用户的信息
Map<String, Object> claims = new HashMap<>();
claims.put("name", "zhangsan");
claims.put("id", 1);
然后通过这个Map信息生成一个简单的token字符串:
//生成Jwt令牌 无签名版
String compact = Jwts.builder().addClaims(claims).compact();
System.out.println(compact);
Jwts.builder().addClaims(claims):将Map信息整合为一个JwtBuilder对象
compact():使用JwtBuilder生成一个token
还可以传入JSON数据
//生成Jwt令牌 还可以传入JSON数据(转成String)两者不能共存 但不建议使用setPayload() 它和后续的一些方法不兼容
// String compact = Jwts.builder().setPayload("JSON_Data").compact();
将结果打印出来发现只有两段字符串
正常有三段分别存储令牌的三个部分
完整生成流程:
String compact = Jwts.builder().addClaims(claims)
.setIssuedAt(new Date()) //设置签发时间
.signWith(key) //设置签名
.setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))//设置过期时间 30分钟
.compact(); //生成token
Signature(签名):
这个过程还需要一个变量Key(就是一个很长的字符串 用于加密,生成签名)
Key也是解析的关键,所以我们要掌握 生成与获取
在这过程中还用到了Header(头部)中的算法
//生成签名所需要的Key 手动版生成
// Key key = Keys.hmacShaKeyFor("zxcvbnmasdfghjklqwertyuiop_zxcvbnmasdfghjklqwertyuiop".getBytes(StandardCharsets.UTF_8));
//生成签名所需要的Key 自动版生成
// Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
两者结合一下,我们可以选择先自动生成一个再存为static final对象(或者使用对自己有特殊好意的字符串)
private static final String secretString = "7C4CHbWRCam1zetXXwpk0NY8SAkhDlBO171kHnJSWTQ=";
//根据生成的secretString实现静态Key完成多服务器共享
private static final Key key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
如何获取生成Key的String呢?
通过 Encoders.BASE64.encode(key.getEncoded())换成String
//借助这个方法生成一个String
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //生成Key
String encode = Encoders.BASE64.encode(key.getEncoded()); //获取生成Key的密钥
System.out.println(encode);
SecretKey secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(encode)); //进行设置
然后将这个生成的String 放在secretString中
获取到token后我们怎么反向解析识别这个token呢?就像公安系统能识别身份证
因为token与Key联系紧密(token就是根据Key生成)
所以key变化就解析不成功 token变化也解析不成功(这就是实现强制登录的关键)
//解析token
String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6MSwiaWF0IjoxNzQ1OTc3ODg3LCJleHAiOjE3NDU5Nzk2ODd9.wHE7XqoZOL1hIj1qCdBJu-K6iXSCfckb-qX1ndbrnuM";
//根据Key来对token进行解析获得所存储的数据 key变化就解析不成功 token变化也解析不成功
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build(); //此时key已经固定
Claims body = build.parseClaimsJws(token).getBody();
System.out.println(body);
这就是JWT的部分实现,项目中大概就展示使用这些。
接下来我们将这些知识运用在项目中:

添加JwtUtils类实现 生成token 解析token 功能
其他类直接调用即可
**生成token:**传入Map信息返回token(校验工作用的东西)
/**
* 生成token
* @param claims 在token中存储的信息
* @return token令牌
*/
public static String getToken(Map<String, Object> claims) {
return Jwts.builder().addClaims(claims)
.setIssuedAt(new Date()) //设置签发时间
.signWith(key) //设置签名
.setExpiration(new Date(System.currentTimeMillis() + Constant.EXPIRATION_TIME))//设置过期时间 30分钟
.compact(); //生成token
}
将我们所需要用的常量放在Constant中
//token过期时间
public static final long EXPIRATION_TIME=5*1000;
//Header中的token
public static final String HEADER_TOKEN = "user_token";
还有些别的的直接放在这个类中也行:
private static final String secretString = "7C4CHbWRCam1zetXXwpk0NY8SAkhDlBO171kHnJSWTQ=";
//根据生成的secretString实现静态Key完成多服务器共享 key:相当于制作身份证的一种工艺
private static final Key key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
解析token :
用于校验工作的
/**
* 解析token
* @param token "身份证"
* @return 简单理解为Map
*/
public static Claims parseToken(String token) {
//根据Key来对token进行解析获得所存储的数据 key变化就解析不成功 token变化也解析不成功
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build(); //此时key已经固定
//如果解析失败会爆出异常
Claims body = null;
try{
body = build.parseClaimsJws(token).getBody();
}catch(Exception e) {
log.error("token校验失败 token:{}",token);
}
return body;
}
当校验失败时我们可以选对不同的异常做出不同判断(这里作者太懒了)
比如:是token为空还是 内容错了
进入业务中:
Controller Service Mapper
Controller:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource(name = "UserService")
private UserService userService;
@RequestMapping("/login")
//@Validated加了这个注解就会进行参数校验
public UserLoginResponse login(@Validated @RequestBody UserLoginRequest userLoginRequest) {
log.info("用户进行登录, userName:{}",userLoginRequest.getUserName());
return userService.login(userLoginRequest);
}
}
当我们传递参数与返回参数时,我们一般操作的都是JSON对象,所以要对参数进行封装。
UserLoginResponse :返回参数
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginResponse {
private Integer userId;
private String token;
}
UserLoginRequest :传递参数
/**
* 发送用于登录的数据
*/
@Data
public class UserLoginRequest {
@NotBlank(message = "用户名不能为空")
@Length(min = 4, message = "用户名不能低于4位")
private String userName;
@NotBlank(message = "密码不能为空")
@Length(min = 4, message = "密码不能低于4位")
private String password;
}
这里使用了两个新注解@NotBlank @Length
@NotBlank:校验参数(掌握它能判断参数是否为空 message为报错时的信息)
@Length:检验参数长度是否达标(我们在登录时名字太长或太短都会出现问题吧)
Service:
完成校验工作
1.先根据用户name从数据库中获取密码用于判断
(这一步建议将数据库代码放在一个方法中,方便下一次复用)
2.检验一下获取的用户信息是否存在
(userLoginRequest这个对象不用再检验,那两个注解已经完成了检验)
3.检验密码是否正确
4.如果用户信息对上了,将这个信息生成一个token并返回
@Service("UserService")
public class UserServiceImpl implements UserService {
@Resource
private UserInfoMapper userInfoMapper;
@Override
public UserLoginResponse login(UserLoginRequest userLoginRequest) {
//参数校验 日志打印 异常捕获
//参数在Controller层就已经完成校验 只用校验从数据库中的数据
UserInfo userInfo = selectUserInfoByName(userLoginRequest.getUserName());
if(userInfo == null || userInfo.getId() == null || userInfo.getId() < 1) {
throw new BlogException("用户不存在");
}
//完成登录校验
if(!userLoginRequest.getPassword().equals(userInfo.getPassword())) {
throw new BlogException("用户密码错误");
}
账号密码正确的逻辑 往token中填装要记录的信息
Map<String, Object> claims = new HashMap<>();
claims.put("id", userInfo.getId());
claims.put("name", userInfo.getUserName());
return new UserLoginResponse(userInfo.getId(), JwtUtils.getToken(claims));
}
/**
* 根据name查询User用户
* @param userName 用户name
* @return 用户信息
*/
public UserInfo selectUserInfoByName(String userName) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserInfo::getUserName, userName).eq(UserInfo::getDeleteFlag, Constant.IS_DELETE);
return userInfoMapper.selectOne(queryWrapper);
}
}
2.实现强制登录
依然是使用拦截器完成
关于拦截器:实现
结构:即使是一个类也要创建一个包哦!

定义拦截器:
调用JwtUtils中我们写的方法进行校验
不符合预期的话再拦截之前记得向response传入一些值哦
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从header中获取token
String token = request.getHeader(Constant.HEADER_TOKEN);
//利用token获取信息进行校验
Claims claims = JwtUtils.parseToken(token);
//token不符合预期
if(claims == null) {
response.setStatus(401);
return false;
}
return true;
}
}
配置拦截器:
建议多使用List<String> excludePaths这样的结构先拦截所有路径 /**/* 然后进行排除路径
(这里工程少,为了方便测试使用了addPathPatterns("/user/**","/blog/**")排除部分路径)
/**
* 注册拦截器
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
//注入拦截器
@Autowired
private LoginInterceptor loginInterceptor;
private final List<String> excludePaths = Arrays.asList("/user/login"
,"/**/*.css"
,"/**/*.html"
,"/**/*.js"
,"/**/*.jpg");
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/user/**","/blog/**").excludePathPatterns(excludePaths);
}
}