springboot JWT项目实战demo
本文是一篇实战demo,使用框架为io.jsonwebtoken的jjwt。你会了解到token的生成,解析过程,最后将在项目中体验jwt的使用过程。如果不是很了解jwt,可以参考以下文章补充一下。
目录
1、引入所用到的库
<!-- jwt相关 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
(可以在 https://github.com/jwtk/jjwt
找到最新版)
2、生成一个token
使用JwtBuilder可以很方便的帮助我们创建出一个jws,也就是我们平时拿来传的token
// 1、创建私钥,这里的私钥是建议随机生成的,这里只是个例子
String secretString = "12345678901234567890123456789012";
SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
// 2、创建jwtBuilder
JwtBuilder jwtBuilder = Jwts.builder().setId("自定义id")
.setSubject("自定义subject")
.setIssuedAt(new Date()) // 签发时间
.signWith(key); // 签名
// 3、获取token
String token = jwtBuilder.compact();
3、解析Token
这里我们可以通过定义的私钥和token来解析出来有用的数据
// 1、解析token,这里的key要和私钥是一个
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
// 2、获取参数
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getIssuedAt());
4、自定义加密数据
自定义数据的加密和解密和1、2步相同,只是使用了claim(String, Object)来定义数据。使用claims.get(String)来解释数据。
// 加密过程
JwtBuilder jwtBuilder = Jwts.builder().claim("para1","value1").claim("参数2","值2").signWith(key);
// 解析过程
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
claims.get("para1");
claims.get("参数2");
5、实际运用
概述
内容:在知道了jwt是如何生成和解析token的,接下来,作者将会举一个例子,其中包括
- 用户登录获取token,token中包含用户id,用户昵称,用户权限信息
- 访问无token拦截(使用拦截器)
- 请求有token时进行解析,获取
目录:
过程简述:用户在登录时获取token。在访问其他链接时检查是否存在token,如果存在,进行解析以便后继使用,如果不存在,不允许访问。
①封装JwtUtils工具类
为便于操作,我们把jwt生成token和解析封装成一个工具类
package com.xxx.demo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private final String secretString = "12345678901234567890123456789012";
private final SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
public String generateToken(String userId, String userNick, Map<String, Object> other) {
// 设置有效时间
long period = 7200000;
JwtBuilder jwtBuilder = Jwts.builder()
.setClaims(other) // 使用setClaims可以将Map数据进行加密,必须放在其他变量之前
.setId(userId)
.setSubject(userNick)
.setExpiration(new Date(System.currentTimeMillis() + period)) // 设置有效期
.signWith(key);
return jwtBuilder.compact();
}
public Claims parseToken(String token){
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}
②定义并配置拦截器
在这一步主要是为了完成对于无token请求的拦截,你将使用到如下三个类,如果对拦截器还不是很了解,可以看看这篇文章https://blog.csdn.net/weixin_49736959/article/details/107843179。
定义拦截器
// InterceptorConfig.java
package com.xxx.demo.config.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.xxx.demo.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
@Component
public class TokenInterceptor implements HandlerInterceptor {
// 自动注入一下
@Resource
private JwtUtils jwtUtils;
// 这个方法是在访问接口之前执行的,我们只需要在这里写验证登陆状态的业务逻辑,就可以在用户调用指定接口之前验证登陆状态了
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 设置返回值属性
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
String token = request.getHeader("token");
PrintWriter out;
// 对于注解的判断
HandlerMethod handlerMethod = (HandlerMethod) handler;
if(handlerMethod.getMethodAnnotation(NoNeedToken.class)!=null || handlerMethod.getBeanType().isAnnotationPresent(NoNeedToken.class)){
// 如果自己拥有NoNeedToken标注或者所属的class拥有NoNeedToken 就直接放行
return true;
}
// 在这里写你的判断逻辑 return true是通过拦截器,可以继续访问controller,return false是不通过
if (token != null) {
Claims claims = null;
try{
claims = jwtUtils.parseToken(token);
}catch (Exception ignored){
}
if(claims != null){
request.setAttribute("user_claims", claims);
return true;
}
}
JSONObject res = new JSONObject();
res.put("state","false");
res.put("msg","token is null or wrong");
out = response.getWriter();
out.append(res.toString());
return false;
}
}
将拦截器配置到项目中
// InterceptorConfig.java
package com.xxx.demo.config.interceptor;
import com.xxx.demo.util.JwtUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
TokenInterceptor tokenInterceptor;
@Bean
public JwtUtils jwtUtils(){
return new JwtUtils();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 设置所有的路径都要进行拦截,除了/test/login
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
}
}
定义免token访问的注解
// NoNeedToken.java
package com.xxx.demo.config.interceptor;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE}) //注解的范围是类、接口、枚举的方法上
@Retention(RetentionPolicy.RUNTIME)//被虚拟机保存,可用反射机制读取
@Documented
public @interface NoNeedToken {
}
③使用BaseController将解析的内容存储一下
一个请求访问进行访问的顺序是,拦截器 -> @ModelAttribute修饰的方法 -> 业务处理的controller, 所以在这里我们定义BaseController,写入@ModelAttribute 来统一解析token,后继需要进行token解析的controller,都继承这个类。
// BaseController
package com.xxx.demo.controller;
import io.jsonwebtoken.Claims;
import org.springframework.web.bind.annotation.ModelAttribute;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class BaseController {
protected HttpServletRequest request;
protected HttpServletResponse response;
protected String UserId; // 用户id
protected String authority; // 用户权限
@ModelAttribute
public void parseClaims(HttpServletRequest request, HttpServletResponse response){
this.request = request;
this.response = response;
// 获取到在拦截器里设置的 user_claims, 并将其保存到类的成员变量中
Claims userClaims = (Claims) request.getAttribute("user_claims");
if(userClaims != null) {
this.UserId = userClaims.getId();
this.authority = userClaims.get("authority").toString();
}
}
}
④设置用户controller测试
//
package com.xxx.demo.controller;
import com.xxx.demo.config.interceptor.NoNeedToken;
import com.xxx.demo.model.Users;
import com.xxx.demo.service.UsersService;
import com.xxx.demo.util.JwtUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class UsersController extends BaseController{
@Resource
UsersService usersService; // 提供查询功能,根据用户nick查询到用户
@Resource
JwtUtils jwtUtils;
// 用户登录方法
@NoNeedToken // 用于取消token验证
@RequestMapping(value = "/login")
public String login(@RequestBody Map<String, String> arg){
String token = "";
if(arg.get("nick")!= null){
Users user = usersService.getPassByNick(arg.get("nick"));
if(user.getPassword()!=null && user.getPassword().equals(arg.get("password"))){
Map<String, Object> jwtArg = new HashMap<>();
jwtArg.put("authority",user.getAuthority());
System.out.println("" + user.getId().toString() + user.getNick());
token = jwtUtils.generateToken(user.getId().toString(), user.getNick(), jwtArg);
}
}
return token;
}
// 获取用户信息方法
@RequestMapping(value = "/test")
public Map<String, String> test(){
Map<String, String> res = new HashMap<>();
res.put("userID", this.UserId); // 这里可以用this.UserId是继承了BaseController
res.put("authority",this.authority);
return res;
}
}
⑤设置用户实例类,和usersService
- user实体类包含userid, nick, password, authority字段,
- dao层设计有查询功能,可以通过用户nick找到用户
- UsersService 使用getPassByNick调用dao层查询功能
由于jpa实现的种类比较多,大家使用自己的方式实现一下就好
⑥测试结果
登录获取token
直接访问test失败
使用token进行访问,成功获取用户id和权限
6、注意事项
setClaims方法
在使用 setClaims方法自定义加密数据时,会覆盖掉之前声明的加密数据,请务必把setClaim写在第一位
7、其他可选项
1、自动生成签名
这个方法也是官方推荐的,用于生成一个足够强的密钥用于JWT hmc-ha算法,请使用密钥. secretkeyfor (signaturealgalgorithm)助手方法
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
2、常用错误
在本文中,没有对错误类型进行区分,大家在使用时可以自行选择
- SignatureException:签名错误异常
- MalformedJwtException:JWT格式错误异常
- ExpiredJwtException:JWT过期异常
- UnsupportedJwtException:不支持的JWT异常
结语:希望对大家有帮助