今天就來完成登入驗證的部分!
昨天已經完成發送帳號密碼到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的功能
未來有機會再看看有什麼地方可以用到
今天就先這樣啦~