SpringBoot整合JWT实现用户认证

初探 JWT

什么是 JWT

JWT(Json Web Token),是一种工具,格式为XXXX.XXXX.XXXX的字符串,JWT 以一种安全的方式在用户和服务器之间传递存放在 JWT 中的不敏感信息。

为什么要用 JWT

设想这样一个场景,在我们登录一个网站之后,再把网页或者浏览器关闭,下一次打开网页的时候可能显示的还是登录的状态,不需要再次进行登录操作,通过 JWT 就可以实现这样一个用户认证的功能。当然使用 Session 可以实现这个功能,但是使用 Session 的同时也会增加服务器的存储压力,而 JWT 是将存储的压力分布到各个客户端机器上,从而减轻服务器的压力。

JWT 长什么样

JWT 由 3 个子字符串组成,分别为HeaderPayload以及Signature,结合 JWT 的格式即:Header.Payload.Signature。(Claim 是描述 Json 的信息的一个 Json,将 Claim 转码之后生成 Payload)。

Header

Header 是由以下这个格式的 Json 通过 Base64 编码(编码不是加密,是可以通过反编码的方式获取到这个原来的 Json,所以 JWT 中存放的一般是不敏感的信息)生成的字符串,Header 中存放的内容是说明编码对象是一个 JWT 以及使用 “SHA-256” 的算法进行加密(加密用于生成 Signature)

{ "typ":"JWT", "alg":"HS256" } 

Claim

Claim 是一个 Json,Claim 中存放的内容是 JWT 自身的标准属性,所有的标准属性都是可选的,可以自行添加,比如:JWT 的签发者、JWT 的接收者、JWT 的持续时间等;同时 Claim 中也可以存放一些自定义的属性,这个自定义的属性就是在用户认证中用于标明用户身份的一个属性,比如用户存放在数据库中的 id,为了安全起见,一般不会将用户名及密码这类敏感的信息存放在 Claim 中。将 Claim 通过 Base64 转码之后生成的一串字符串称作 Payload。

{ 
"iss":"Issuer —— 用于说明该JWT是由谁签发的", 
"sub":"Subject —— 用于说明该JWT面向的对象", 
"aud":"Audience —— 用于说明该JWT发送给的用户", 
"exp":"Expiration Time —— 数字类型,说明该JWT过期的时间", 
"nbf":"Not Before —— 数字类型,说明在该时间之前JWT不能被接受与处理", 
"iat":"Issued At —— 数字类型,说明该JWT何时被签发", 
"jti":"JWT ID —— 说明标明JWT的唯一ID", 
"user-definde1":"自定义属性举例", 
"user-definde2":"自定义属性举例" 
} 

Signature

Signature 是由 Header 和 Payload 组合而成,将 Header 和 Claim 这两个 Json 分别使用 Base64 方式进行编码,生成字符串 Header 和 Payload,然后将 Header 和 Payload 以 Header.Payload 的格式组合在一起形成一个字符串,然后使用上面定义好的加密算法和一个密匙(这个密匙存放在服务器上,用于进行验证)对这个字符串进行加密,形成一个新的字符串,这个字符串就是 Signature。

总结

JWT 实现认证的原理

服务器在生成一个 JWT 之后会将这个 JWT 会以 Authorization : Bearer JWT 键值对的形式存放在 cookies 里面发送到客户端机器,在客户端再次访问收到 JWT 保护的资源 URL 链接的时候,服务器会获取到 cookies 中存放的 JWT 信息,首先将 Header 进行反编码获取到加密的算法,在通过存放在服务器上的密匙对 Header.Payload 这个字符串进行加密,比对 JWT 中的 Signature 和实际加密出来的结果是否一致,如果一致那么说明该 JWT 是合法有效的,认证成功,否则认证失败。

JWT 实现用户认证的流程图

JWT 的代码实现

这里的代码实现使用的是 Spring Boot(版本号:1.5.10)框架,以及 Apache Ignite(版本号:2.3.0)数据库。有关 Ignite 和 Spring Boot 的整合可以查看这里。

http://blog.csdn.net/ltl112358/article/details/79399026

代码说明:

代码中与 JWT 有关的内容如下:

  • config 包中 JwtCfg 类配置生成一个 JWT 并配置了 JWT 拦截的 URL
  • controller 包中 PersonController 用于处理用户的登录注册时生成 JWT,SecureController 用于测试 JWT
  • model 包中 JwtFilter 用于处理与验证 JWT 的正确性
  • 其余属于 Ignite 数据库访问的相关内容

JwtCfg 类

这个类中声明了一个 @Bean ,用于生成一个过滤器类,对 / secure 链接下的所有资源访问进行 JWT 的验证

/**
 * This is Jwt configuration which set the url "/secure/*" for filtering
 * @program: users
 * @create: 2018-03-03 21:18
 **/
@Configuration
public class JwtCfg {

    @Bean
    public FilterRegistrationBean jwtFilter() {
        final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new JwtFilter());
        registrationBean.addUrlPatterns("/secure/*");

        return registrationBean;
    }

}

JwtFilter 类

这个类声明了一个 JWT 过滤器类,从 Http 请求中提取 JWT 的信息,并使用了”secretkey” 这个密匙对 JWT 进行验证

/**
 * Check the jwt token from front end if is invalid
 * @program: users
 * @create: 2018-03-01 11:03
 **/
public class JwtFilter extends GenericFilterBean {

    public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain)
            throws IOException, ServletException {

        // Change the req and res to HttpServletRequest and HttpServletResponse
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;

        // Get authorization from Http request
        final String authHeader = request.getHeader("authorization");

        // If the Http request is OPTIONS then just return the status code 200
        // which is HttpServletResponse.SC_OK in this code
        if ("OPTIONS".equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);

            chain.doFilter(req, res);
        }
        // Except OPTIONS, other request should be checked by JWT
        else {

            // Check the authorization, check if the token is started by "Bearer "
            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                throw new ServletException("Missing or invalid Authorization header");
            }

            // Then get the JWT token from authorization
            final String token = authHeader.substring(7);

            try {
                // Use JWT parser to check if the signature is valid with the Key "secretkey"
                final Claims claims = Jwts.parser().setSigningKey("secretkey").parseClaimsJws(token).getBody();

                // Add the claim to request header
                request.setAttribute("claims", claims);
            } catch (final SignatureException e) {
                throw new ServletException("Invalid token");
            }

            chain.doFilter(req, res);
        }
    }
}

PersonController 类

这个类中在用户进行登录操作成功之后,将生成一个 JWT 作为返回

/**
 * @program: users
 * @create: 2018-02-27 19:28
 **/
@RestController
public class PersonController {

    @Autowired
    private PersonService personService;


    /**
     * User register with whose username and password
     * @param reqPerson
     * @return Success message
     * @throws ServletException
     */
    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public String register(@RequestBody() ReqPerson reqPerson) throws ServletException {
        // Check if username and password is null
        if (reqPerson.getUsername() == "" || reqPerson.getUsername() == null
                || reqPerson.getPassword() == "" || reqPerson.getPassword() == null)
            throw new ServletException("Username or Password invalid!");

        // Check if the username is used
        if(personService.findPersonByUsername(reqPerson.getUsername()) != null)
            throw new ServletException("Username is used!");

        // Give a default role : MEMBER
        List<Role> roles = new ArrayList<Role>();
        roles.add(Role.MEMBER);

        // Create a person in ignite
        personService.save(new Person(reqPerson.getUsername(), reqPerson.getPassword(), roles));
        return "Register Success!";
    }

    /**
     * Check user`s login info, then create a jwt token returned to front end
     * @param reqPerson
     * @return jwt token
     * @throws ServletException
     */
    @PostMapping
    public String login(@RequestBody() ReqPerson reqPerson) throws ServletException {
        // Check if username and password is null
        if (reqPerson.getUsername() == "" || reqPerson.getUsername() == null
                || reqPerson.getPassword() == "" || reqPerson.getPassword() == null)
            throw new ServletException("Please fill in username and password");

        // Check if the username is used
        if(personService.findPersonByUsername(reqPerson.getUsername()) == null
                || !reqPerson.getPassword().equals(personService.findPersonByUsername(reqPerson.getUsername()).getPassword())){
            throw new ServletException("Please fill in username and password");
        }

        // Create Twt token
        String jwtToken = Jwts.builder().setSubject(reqPerson.getUsername()).claim("roles", "member").setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256, "secretkey").compact();

        return jwtToken;
    }
}

SecureController 类

这个类中只是用于测试 JWT 功能,当用户认证成功之后,/secure 下的资源才可以被访问

/**
 * Test the jwt, if the token is valid then return "Login Successful"
 * If is not valid, the request will be intercepted by JwtFilter
 * @program: users
 * @create: 2018-03-01 11:05
 **/
@RestController
@RequestMapping("/secure")
public class SecureController {

    @RequestMapping("/users/user")
    public String loginSuccess() {
        return "Login Successful!";
    }

}

代码功能测试

本例使用 Postman 对代码进行测试,这里并没有考虑到安全性传递的明文密码,实际上应该用 SSL 进行加密

  1. 首先进行一个新的测试用户的注册,可以看到注册成功的提示返回

再让该用户进行登录,可以看到登录成功之后返回的 JWT 字符串

直接申请访问 / secure/users/user ,这时候肯定是无法访问的,服务器返回 500 错误

将获取到的 JWT 作为 Authorization 属性提交,申请访问 / secure/users/user ,可以访问成功

示例代码

https://github.com/ltlayx/SpringBoot-Ignite

参考

http://blog.leapoahead.com/2015/09/06/understanding-jwt/ ↩
https://aboullaite.me/spring-boot-token-authentication-using-jwt/

发表评论

电子邮件地址不会被公开。 必填项已用*标注