JWT认证:实例演示|Java 开发实战

90 阅读7分钟

JWT认证:实例演示|Java 开发实战 前言 对于JWT(JSON Web Token),可以先自己思考如下几个问题来测试一下对JWT的了解情况:

1、JWT功能实现的原理是什么?

2、使用JWT带来的优缺点有哪些?

3、项目中如果使用JWT,它各个部分的必要性是否能够认知?

如果有不清楚的,可以带着问题往下看。

最后也会结合Spring Boot做一个JWT实例

1、 JWT原理探究 什么是JWT?官方的定义是

JSON Web 令牌 (JWT) 是一个开放标准‎‎(RFC 7519‎‎),它定义了一种紧凑且自包含的方式,用于将信息作为 JSON 对象安全地在各方之间传输信息。 去除各种费脑子的描述之后就是:客户端和服务器端安全传输信息的一个标准。

1.1、JWT的结构组成 JWT由三部分组成,中间使用句点"."连接,即header.payload.signature,这三个部分都是由base64编码的,这么做的目的是为了保证url中安全的传输。

第一部分header中是由两部分信息组成,即声明类型jwt和声明加密的算法(例如SHA256),所以说header在base64URL编码之前是如下的JSON

{ 'typ':'jwt', 'alg':'SHA256' } base64URL编码之后为:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 第二部分payload是存放想要传递的信息,例如经常用到的用户ID和过时时间exp,所以说payload在base64URL编码之前是如下的JSON

{ 'id':'10', 'exp':'2301597433' } base64URL编码之后为:

ewoJJ2lkJzonMTAnLAoJJ2V4cCc6JzIzMDE1OTc0MzMnCn0= 第三部分signature最是关键,它的组成原理是:1、将header和payload分别base64URL编码之后组合到一起(通过"."连接);2、添加一个只有服务器知道的签名字符串;3、再使用header中的签名算法SHA256加密步骤1、2。可以看成下面的公式:

signature = SHA256(base64encode(header) + '.' + base64encode(payload), 'SEVER_SECRET_KEY') 最终可以得到签名信息为

05dd35b4d20c95430cd1b63406f861de7e4c1476f9dbffa25f30fe08baf8f530 为啥第三部分是关键呢?因为第三部分只能由服务器生成,而只能由服务器生成的根本原因就是没有人知道签名字符串--SEVER_SECRET_KEY,如果有人只篡改了第一和第二部分,服务器能够正常解析里面的内容,但是作为验证的第三部分显然是不匹配的;如果有人篡改了所有部分,服务器是没法解析第三部分的,因为SEVER_SECRET_KEY一定不一样。

以上三个部分组合在一起就构成了完成的JWT了,如下所示:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ewoJJ2lkJzonMTAnLAoJJ2V4cCc6JzIzMDE1OTc0MzMnCn0=.05dd35b4d20c95430cd1b63406f861de7e4c1476f9dbffa25f30fe08baf8f530 1.2、JWT缺点: 1、安全性:从以上的描述中我们可以看到,payload中的信息只是进行了base64URL编码,并没有加密,所以不能存放敏感信息;同时,

JWT如果泄露会被人冒用身份,为防止盗用,JWT应尽量使用https协议传输。

2、性能:JWT为了做到安全性,导致其本身很长,即http请求开销增加。

1.3、JWT优点: 1、扩展性:应用程序分布式部署的情况下,客户端的一个令牌可以访问所有的服务器的同时,也避免了服务器之间做session id的多机数据共享。

2、性能:JWT中信息的存储,可以有效的减少服务器查询数据库的次数。

2、Spring boot + JWT实现 创建Spring boot项目以及数据库配置等等细节直接跳过,下图是接下来项目中会用到的包的结构及其说明:

image.png

bean:存放实体对象

common:存放拦截器等公共模块

controller:控制层

dto:控制层接收前端属性时定义的对象

mapper:数据层

service:业务层

utils:工具类

此时可以完全不管层级结构,因为接下来的实例中会有具体的应用场景,更容易理解。

做好了前期的准备:

第一步:写一个生成以及验证 JWT 的工具类 JWTUtils(显然应该放到utils包底下):

@Component public class JWTUtils {

/**
 * 生成 token
 */
public static String getToken(Map<String,String> map) {
    Date date = new Date(System.currentTimeMillis() + 86400*1000);
    Algorithm algorithm = Algorithm.HMAC256("Xinmachong666");
    //创建jwt builder
    JWTCreator.Builder builder = JWT.create();
    //payload
    map.forEach((k,v)->{
        builder.withClaim(k,v);
    });
    // 附带username信息
    return builder
            //到期时间
            .withExpiresAt(date)
            //创建一个新的JWT,并使用给定的算法进行标记
            .sign(algorithm);
}

/**
 * 校验 token 是否正确
 */
public static DecodedJWT verify(String token) {
    return JWT.require(Algorithm.HMAC256("Xinmachong666")).build().verify(token);
}

其中 86400*1000 是设定JWT有效期为1天,“Xinmachong666”就是前面所说的非常隐秘的签名字符串。

第二步:写一个 JWT 的拦截器,从而对所有的 JWT 进行验证合法性:

public class JWTInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取请求头中令牌 ApiResponse apiResponse = null; String token = request.getHeader("token"); try { JWTUtils.verify(token); return true; }catch (SignatureVerificationException e){ apiResponse = new ApiResponse(505,"无效签名",false); }catch (TokenExpiredException e){ apiResponse = new ApiResponse(505,"token过期",false); }catch (AlgorithmMismatchException e){ apiResponse = new ApiResponse(505,"token算法不一致",false); }catch (Exception e){ apiResponse = new ApiResponse(505,"token无效",false); } String json = new ObjectMapper().writeValueAsString(apiResponse); response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(json); return false; } } 该类实现了HandlerInterceptor,并且重写了预处理的实现部分。

第三步:虽然实现了 JWT 验证的预处理,但是现在服务器不知道怎么以及何时拦截下 JWT 。举个栗子:JWT 验证的预处理就好比收费站的小姐姐,但是现在没有收费站,你让她咋收费呢?拿着黑李逵的斧子去收过路费?那所有车辆都要收费?显然消防车不用吧?所以说第三步就是写一个配置类对配置的路由进行选择性拦截,如下所示:

@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) .addPathPatterns("/") .excludePathPatterns("/v1/user/login"); } } 其中 addPathPatterns("/") 表示拦截所有路由(要想过此路,留下买路财),excludePathPatterns("/v1/user/login") 表示可以放行的路由(消防员:救火也要收费?小姐姐:失误失误)。

第四部分:写一个登陆接口进行测试一下,首先写控制层代码(显然 UserController 类放到controller包中):

@RestController @RequestMapping("/v1/user") @Validated @CrossOrigin public class UserController {

@Resource
private UserService userService;


@PostMapping("/login")
public ApiResponse login(@RequestBody @Validated LoginDTO loginDTO) {
    String salt = this.userService.getSaltByAccount(loginDTO);
    String password = new SimpleHash("MD5",loginDTO.getPassword(),salt,1).toHex();
    String realPassword = this.userService.getPasswordByAccount(loginDTO);
    if(StringUtils.isEmpty(realPassword)){
        return new ApiResponse(400,"用户名错误",0);
    } else if (!realPassword.equals(password)) {
        return new ApiResponse(400, "密码错误", 0);
    } else {
        LoginVO loginVO = this.userService.getUserMsg(loginDTO);
        return new ApiResponse(200,"success",loginVO);
    }
}

@GetMapping("/test")
public ApiResponse test(HttpServletRequest request) {
    String username = this.userService.getUsernameByJWT(request);
    return new ApiResponse(200,"success",username);
}

} 其中 LoginDTO 是控制层将要接收的对象:

@Data @NoArgsConstructor public class LoginDTO { @NotBlank(message = "用户名不能为空") private String account; @NotBlank(message = "密码不能为空") private String password; } ApiResponse是自定义的返回值格式:

@Data public class ApiResponse {

private int code;
private String msg;
private Object data;

public ApiResponse(int code, String msg, Object data) {
    this.code = code;
    this.msg = msg;
    this.data = data;
}

} 而 getSaltByAccount()、getPasswordByAccount() 和 getUserMsg() 方法的具体实现在业务层,如下所示(显然 UserService 类放到service包中):

@Service public class UserService {

@Resource
private UserMapper userMapper;
@Resource
private GetInformationFromJWT getInformationFromJWT;

public String getSaltByAccount(LoginDTO loginDTO) {
    QueryWrapper<MeyerUser> wrapper = new QueryWrapper<>();
    wrapper.lambda().eq(MeyerUser::getAccount,loginDTO.getAccount());
    return this.userMapper.selectOne(wrapper).getSalt();
}

public String getPasswordByAccount(LoginDTO loginDTO) {
    QueryWrapper<MeyerUser> wrapper = new QueryWrapper<>();
    wrapper.lambda().eq(MeyerUser::getAccount,loginDTO.getAccount());
    return this.userMapper.selectOne(wrapper).getPassword();
}

public LoginVO getUserMsg(LoginDTO loginDTO) {
    QueryWrapper<MeyerUser> wrapper = new QueryWrapper<>();
    wrapper.lambda().eq(MeyerUser::getAccount,loginDTO.getAccount());
    MeyerUser meyerUser = this.userMapper.selectOne(wrapper);
    String token = this.getToken(meyerUser);

    LoginVO loginVO = new LoginVO();
    loginVO.setToken(token);
    BeanUtils.copyProperties(meyerUser,loginVO);
    return loginVO;
}

public String getToken(MeyerUser meyerUser) {
    Map<String,String> payload = new HashMap<>();
    payload.put("userId", meyerUser.getId()+"");
    payload.put("username", meyerUser.getUsername());
    payload.put("account", meyerUser.getAccount());
    return JWTUtils.getToken(payload);
}

public String getUsernameByJWT(HttpServletRequest request) {
    return this.getInformationFromJWT.getUsernameByJWT(request);
}

} 因为项目中经常会获取 JWT 中存储的信息,所以我提取了该功能到新的类中:

@Component public class GetInformationFromJWT {

public int getUserIdByJWT(HttpServletRequest request){
    String token = request.getHeader("token");
    DecodedJWT verify = JWTUtils.verify(token);
    return Integer.parseInt(verify.getClaim("userId").asString());
}

public int getRoleIdByJWT(HttpServletRequest request){
    String token = request.getHeader("token");
    DecodedJWT verify = JWTUtils.verify(token);
    return Integer.parseInt(verify.getClaim("roleId").asString());
}

public String getUsernameByJWT(HttpServletRequest request){
    String token = request.getHeader("token");
    DecodedJWT verify = JWTUtils.verify(token);
    return verify.getClaim("username").asString();
}

} 接下来是UserMapper和User实体:

@Repository public interface UserMapper extends BaseMapper { } @Data @Accessors(chain = true) public class User { @TableId(type = IdType.AUTO) private int id; private String username; private String account; private String password; private String salt; private String staffNo; private String nickname; private String avatar; @JsonIgnore private Date createTime; @JsonIgnore private Date updateTime; @JsonIgnore @TableLogic private Date deleteTime; }