今天就來完成登入驗證的部分!
昨天已經完成發送帳號密碼到api,驗證ok即發送一筆JWT給client,
接下來要實作的部分就是要讓除了登入url(/api/v1/user/login)以外的url
都需先驗證是否帶有正確的JWT,否則不放行。
為了做到以上功能,需要實作filter,這邊繼承OncePerRequestFilter,確保request只會被filter過濾一次
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
public class AuthorizationCheckFilter extends OncePerRequestFilter{
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException {
                //如果不是登入就攔截
                if(!req.getServletPath().equals("/api/v1/user/login")){
                    String authorHeader =  req.getHeader(AUTHORIZATION);
                    String bearer ="Bearer ";
                    //以jjwt驗證token,只要驗證成功就放行
                    //驗證失敗會拋exception,直接將錯誤訊息傳回
                    if(authorHeader!= null && authorHeader.startsWith(bearer)){
                        try{
                        String token = authorHeader.substring(bearer.length());
                        Claims claims = Jwts.parser().setSigningKey("MySecret")
                        .parseClaimsJws(token).getBody();
                        System.out.println("JWT payload:"+claims.toString());
                        chain.doFilter(req, res);
                        
                        }catch(Exception e){
                            System.err.println("Error : "+e);
                            res.setStatus(FORBIDDEN.value());
                            
                            Map<String, String> err = new HashMap<>();
                            err.put("jwt_err", e.getMessage());
                            res.setContentType(APPLICATION_JSON_VALUE);
                            new ObjectMapper().writeValue(res.getOutputStream(), err);
                        }
                    }else{
                        res.setStatus(UNAUTHORIZED.value());
                    }
                }else{
                    chain.doFilter(req, res);
                }
        
    }
}
這裡我以最符合JWT精神的方式來做驗證,只要驗證傳來的token確實是我發出的,就直接放行,
查資料時看有人分享的實作範例除了驗證jwt之外,還會取出sub來跟資料庫做比對,確認是不是真的有這個用戶,個人覺得算是比較嚴謹的作法。因為就是已經驗證過了,才會發出jwt給客戶,除非我們的私鑰被盜,否則不可能會有不存在的客戶的jwt出現。
說到私鑰,正常來講應該要把它放在資料庫中才是符合資安規範的做法
之後有空再改為從資料庫讀取參數![]()
接下來就是Spring Security
我在[Day 06] - 用Spring Boot 建立Controller建立了WebSecurityConfig,java
將剛剛寫好的filter加進來:
import com.rei1997.vault.util.filter.AuthorizationCheckFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //關掉csrf保護
        http.csrf().disable();
        //不寫session了
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //加上剛剛寫的filter
        http.addFilterBefore(new AuthorizationCheckFilter(), BasicAuthenticationFilter.class);
    }  
}
就這樣
來驗證一下,我在Controller寫了一個簡單的getMapping的方法,會回傳User的資訊
當不帶Authorization header時,會回傳401,表示有成功擋下來未授權的request
帶上jwt token時,回200,看來ok
當jwt過期也會被阻擋:
說來慚愧,這樣寫下來發現其實根本沒什麼沒用到Spring Security的功能![]()
未來有機會再看看有什麼地方可以用到
今天就先這樣啦~