Spring Security JWT

利用Spring Security實作JWT驗證

莊哲安 Chean Chuang 2019/12/10 14:42:16
15594

What is JWT?

有鑑於同樣在昕力大學已有文章已詳細介紹過JWT,本篇就省略該部分的介紹,請各位可以直接過去看看(JSON Web Token)

 

Why we need JWT?

近期接觸前後端分離的網頁架構,因為在開發上是透過API的方式來串聯前後端的資料傳遞。但是由於是前後分離的開發模式,因此資料的傳遞以及API的權限問題就免不了是直接大喇喇地攤在網路上,因此我們就遇到了誰能夠使用這個API的安全性問題。

因此我們必須對所有的請求進行加密,而對請求加密的方法有很多種,而近期最流行的方法就是透過JWT進行加密。透過在每次請求時,將Token放進request head中,server會在處理請求前先驗證Token的合法性而決定是否處理請求。

實際處理流程如下圖

 

 

Spring Security and JWT Configuration

• Gradle相依性配置如下

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'	
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'io.jsonwebtoken:jjwt-api:0.10.7',
			'io.jsonwebtoken:jjwt-impl:0.10.7',            
            'io.jsonwebtoken:jjwt-jackson:0.10.7'
}

JWT的工具類

簡單介紹JWT是什麼以及為何需要使用JWT之後,我們就開始進入我們的正題。配置基於Spring Secrutiy的JWT在JWT的工具類主要分兩個部分:

1. 產生JWT

前端透過POST與後端開放的/login API傳遞使用者的登入帳號及密碼。如果前端傳遞的使用者帳號密碼正確的話,伺服器會產生一組JWT並回傳給前端

2. 驗證JWT

前端嘗試向後端發送request時,後端會判斷此request的API是否需要驗證,如果需要則會判斷request header內的JWT是否合法

 

首先就來建立一個JwtUtil

import java.io.IOException;
import java.security.Key;
import java.util.Date;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;

import com.thinkpower.teampractice.pojo.JSONResult;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.crypto.MacProvider;


public class JwtUtil {
	static final long EXPIRATIONTIME = 432_000_000;     // 5天
    static final String TOKEN_PREFIX = "Bearer";        // Token前缀
    static final String HEADER_STRING = "Authorization";// 存放Token的Header Key
    static final Key key = MacProvider.generateKey();	//給定一組密鑰,用來解密以及加密使用
    
    
	
    // JWT產生方法
    public static void addAuthentication(HttpServletResponse response, Authentication user) {
    
    	authorize.deleteCharAt(authorize.lastIndexOf(","));
    	// 生成JWT
    	String jws = Jwts.builder()
    			// 在Payload放入自定義的聲明方法如下
    			//.claim("XXXXX",XXXXX)
    			// 在Payload放入sub保留聲明
    			.setSubject(user.getName())
    			// 在Payload放入exp保留聲明
    			.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
    			
    			.signWith(key).compact();
        // 把JWT傳回response
        try {
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_OK);
            response.getOutputStream().println(JSONResult.fillResultString(0, user.getName(), jws));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

  // JWT驗證方法
    public static Authentication getAuthentication(HttpServletRequest request) {
    	
        // 從request的header拿回token
        String token = request.getHeader(HEADER_STRING);

        if (token != null) {
            // 解析 Token
        	try {
        		Claims claims = Jwts.parser()
                        // 驗證
                        .setSigningKey(key)
                        // 去掉 Bearer
                        .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                        .getBody();

                // 拿用户名
                String user = claims.getSubject();

                // 得到權限
                List<GrantedAuthority> authorities =  
                		AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorize"));
                // 返回Token
                return user != null ?
						new UsernamePasswordAuthenticationToken(user, null, authorities) :
                        null;
        	} catch (JwtException ex) {
        		System.out.println(ex);
        	}
            
        }
        return null;
    }  

以上類別就兩個static方法,分別負責產生JWT及驗證JWT。

 

接著創建一個WebSecurityConfig進行設定,這邊繼承WebSecurityConfigureAdapter並自行設定規則即可,僅針對JWT的地方在註解特別說明。

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private CustomAuthenticationProvider customAuthenticationProvider;

	// 設置HTTP請求驗證
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 因為是JWT,無須csrf驗證
		http.csrf().disable()
				// 對請求進行驗證
				.authorizeRequests()
				// 所有/login的请求放行
				.antMatchers("/login").permitAll()
			// ... 中間配置省略
				.and()
				// 添加過濾器,針對/login的請求,交給LoginFilter處理
				.addFilterBefore(new LoginFilter("/login", authenticationManager()),
						UsernamePasswordAuthenticationFilter.class)
				// 添加過濾器,針對其他請求進行JWT的驗證
				.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// 使用自定義的驗證
		auth.authenticationProvider(customAuthenticationProvider);
	}
}

在configure(AuthenticationManagerBuilder auth)內,我們使用我們自定義的provider,為了讓各位可以簡單上手,我也提供最簡單的provider給各位,有想深入了解其運作內容的可以在自行深入研究。

CustomAuthenticationProvider

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import com.thinkpower.teampractice.config.security.UserLoginService;
@Service
public class CustomAuthenticationProvider implements AuthenticationProvider {
	
	@Autowired
	private UserLoginService userLoginService;
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// 獲得使用者帳號及密碼
		String account = authentication.getName();
		String password = authentication.getCredentials().toString();
		UserDetails user = userLoginService.loadUserByUsername(account);
		// 帳號密碼驗證邏輯
		if (account.equals(user.getUsername()) && password.equals(user.getPassword())) {

			// 生成Authentication令牌
			Authentication auth = new UsernamePasswordAuthenticationToken(account, password, user.getAuthorities());
			return auth;
		} else {
			throw new BadCredentialsException("Password error");
		}
	}

	
	@Override
	public boolean supports(Class<?> authentication) {
		return authentication.equals(UsernamePasswordAuthenticationToken.class);
	}
}  

其中UserLoginService是透過帳號去與資料庫獲得此帳號登入所需的資訊。

 

接著我們針對兩個過濾器進行講解

LoginFilter

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONObject;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.thinkpower.teampractice.pojo.JSONResult;
import com.thinkpower.teampractice.service.TokenAuthenticationService;



public class LoginFilter extends AbstractAuthenticationProcessingFilter{
	
	public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }
    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException, IOException, ServletException {

    	String username = req.getParameter("username");
        String password = req.getParameter("password");

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();
        // 觸發AuthenticationManager
        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                		username,password
                )
        );
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest req,
            HttpServletResponse res, FilterChain chain,
            Authentication auth) throws IOException, ServletException {
    	// 登入成功,將token透過JwtUtil放到res中
        JwtUtil.addAuthentication(res, auth);
    }


    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {

        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getOutputStream().println(JSONResult.fillResultString(401, "Loing error!!!", JSONObject.NULL));
    }
}

此LoginFilter繼承AbstractAuthenticationProcessingFilter負責/login的請求進行驗證,此方法包含一個abstract的attemptAuthentication的方法,而我們在子類重寫這個驗證方法。最後呼叫 AuthenticationManager 的 authenticate 方法進行驗證,如果驗證成功則會呼叫successfulAuthentication()這個方法內的JwtUtil去取得產生的token。

 

JWTAuthenticationFilter

 

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import com.thinkpower.teampractice.service.TokenAuthenticationService;

public class JWTAuthenticationFilter extends GenericFilterBean{
	
	@Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain filterChain)
            throws IOException, ServletException {
		
		//驗證token,獲得授權
        Authentication authentication = JwtUtil
                .getAuthentication((HttpServletRequest)request);

        SecurityContextHolder.getContext()
                .setAuthentication(authentication);
        filterChain.doFilter(request,response);
    }

}

此Filter就是負責除了/login以外其他受到Spring Security保護的url,也就是需要攜帶JWT來進行驗證後才可以使用的API,而解析過程已放在JwtUtil之中。

 

透過以上類別的設定即為最簡便的Spring security with JWT

 

 

莊哲安 Chean Chuang