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

3,441 阅读6分钟

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接

前言

对于JWT(JSON Web Token),可以先自己思考如下几个问题来测试一下对JWT的了解情况:

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

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

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

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

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

1、 JWT原理探究

什么是JWT?官方的定义是

JSON Web 令牌 (JWT) 是一个开放标准‎[‎(RFC 7519‎](https://tools.ietf.org/html/rfc7519)‎),它定义了一种紧凑且自包含的方式,用于将信息作为 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<MeyerUser> {
}
@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;
}