1. 程式人生 > 實用技巧 >Spring Boot整合JWT實現使用者認證

Spring Boot整合JWT實現使用者認證

初探JWT

什麼是JWT

JWT(Json Web Token),是一種工具,格式為XXXX.XXXX.XXXX的字串,JWT以一種安全的方式在使用者和伺服器之間傳遞存放在JWT中的不敏感資訊。

為什麼要用JWT

設想這樣一個場景,在我們登入一個網站之後,再把網頁或者瀏覽器關閉,下一次開啟網頁的時候可能顯示的還是登入的狀態,不需要再次進行登入操作,通過JWT就可以實現這樣一個使用者認證的功能。當然使用Session可以實現這個功能,但是使用Session的同時也會增加伺服器的儲存壓力,而JWT是將儲存的壓力分佈到各個客戶端機器上,從而減輕伺服器的壓力。

JWT長什麼樣

JWT由3個子字串組成,分別為Header

Payload以及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.首先進行一個新的測試使用者的註冊,可以看到註冊成功的提示返回

2.再讓該使用者進行登入,可以看到登入成功之後返回的JWT字串

3.直接申請訪問/secure/users/user ,這時候肯定是無法訪問的,伺服器返回500錯誤

4.將獲取到的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/