當我們使用SpringBoot實現了一個簡單的API接口之後,我們如何去保證我們的API接口只讓我們運行的人調用呢。這時候就需要對我們的API接口進行保護。在別人訪問這些接口的時候,我們對訪問者進行身份的驗證,從而對接口的保護。
基本流程如下圖:
當然我們需要一個接口給用戶請求Token,要不然用戶拿不到token怎麼去請求其他資源呢。流程圖如下:
接下來我們按照流程一步一步實現。
首先實現請求token
@GetMapping("/get_token")
public JsonResult getToken(@RequestParam String username,@RequestParam String password){
//apiUser通過用戶url傳入的賬號和密碼
ApiUser apiUser=loginService.loginApiUser(username);
//這裏驗證用戶的信息是否正確
JsonResult jsonResult=checkApiUser(apiUser,password);
//如果正確那麼返回Null,不正確返回提示然後顯示給用戶
if (jsonResult!=null){
return jsonResult;
}
//到這裏證明用戶信息是正確的,那麼我們進行token的生成然後返回給用戶
String token=loginService.generateToken(apiUser);
return JsonResult.suc(token);
}
- 通過url拿到用戶傳過來的數據,賬戶和密碼
- 對賬戶密碼進行驗證,如果不正確返回錯誤提示。
- 如果正確進行token生存,然後返回給用戶
那麼我們如何驗證呢
private JsonResult checkApiUser(ApiUser apiUser,String password){
if (apiUser==null){
return JsonResult.error(434,"賬戶不存在");
}else {
if (apiUser.getEnable()==0){
return JsonResult.error(452,"賬戶在黑名單");
}
if (!apiUser.getPassword().equals(password)){
//equals相等返回true
return JsonResult.error(452,"賬戶密碼錯誤");
}
}
return null;
}
- 先通過用戶傳入的用戶名進行查找,看是否存在該用戶。
- 如果存在該用戶,判斷用戶的狀態是否可用。
- 如果狀態可用,那麼比較數據庫保存的密碼和用戶輸入的密碼是否匹配。
那麼如何生成token呢
public String generateToken(ApiUser tokenDetail) {
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("sub", tokenDetail.getUsername());
claims.put("created", this.generateCurrentDate());
return this.generateToken(claims);
}
/**
* 根據 claims 生成 Token
*
* @param claims
* @return
*/
private String generateToken(Map<String, Object> claims) {
logger.info("成功進入生產token",claims);
try {
return Jwts.builder()
.setClaims(claims)
.setExpiration(this.generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, this.secret.getBytes("UTF-8"))
.compact();
} catch (UnsupportedEncodingException ex) {
//didn't want to have this method throw the exception, would rather log it and sign the token like it was before
logger.warn(ex.getMessage());
return Jwts.builder()
.setClaims(claims)
.setExpiration(this.generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, this.secret)
.compact();
}
}
- 我們通過JWT工具去進行token的生成。(如果不瞭解JWT可用百度看看,非常簡單。)
- 通過用戶信息,我們生成一個token並返回給客戶端。
- 用戶拿着這個token去請求資源,就可以了。
接下來我們看用戶請求資源是如何實現的
@PostMapping("/courseList")
public JsonResult getAllCourse(Page<Course> page, Integer state){
//獲取當前學年的課程列表
return JsonResult.suc(courseService.getAll(page,state));
}
這是一個資源API接口,那麼用戶在訪問這個接口的時候我們就需要對身份進行驗證,那麼是在哪裏驗證的呢
@Configuration // 聲明爲配置類
@EnableWebSecurity // 啓用 Spring Security web 安全的功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 註冊 401 處理器
*/
@Autowired
private EntryPointUnauthorizedHandler unauthorizedHandler;
/**
* 註冊 403 處理器
*/
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
/**
* 註冊 token 轉換攔截器爲 bean
* 如果客戶端傳來了 token ,那麼通過攔截器解析 token 賦予用戶權限
*
* @return
* @throws Exception
*/
@Bean
public AuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
AuthenticationTokenFilter authenticationTokenFilter = new AuthenticationTokenFilter();
authenticationTokenFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationTokenFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/get_token").permitAll() // 所有人可以訪問
.anyRequest().authenticated() // 必須攜帶token
.and()
// 配置被攔截時的處理
.exceptionHandling()
.authenticationEntryPoint(this.unauthorizedHandler) // 添加 token 無效或者沒有攜帶 token 時的處理
.accessDeniedHandler(this.accessDeniedHandler) //添加無權限時的處理
.and()
.csrf()
.disable() // 禁用 Spring Security 自帶的跨域處理
.sessionManagement() // 定製我們自己的 session 策略
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 調整爲讓 Spring Security 不創建和使用 session
/**
* 本次 json web token 權限控制的核心配置部分
* 在 Spring Security 開始判斷本次會話是否有權限時的前一瞬間
* 通過添加過濾器將 token 解析,將用戶所有的權限寫入本次會話
*/
http
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
}
- 首先我們通過SpringSecurity進行url配置,也就是給URL加了過濾器。
- 當用戶訪問需要token的資源路徑的時候就會觸發過濾器。
-
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);這一句相當於把我們自定義的過濾器加入到Springsecurity過濾器鏈中。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 將 ServletRequest 轉換爲 HttpServletRequest 才能拿到請求頭中的 token
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 嘗試獲取請求頭的 token
String authToken =httpRequest.getHeader(this.tokenHeader);//獲取token=xxx
// 嘗試拿 token 中的 username
// 若是沒有 token 或者拿 username 時出現異常,那麼 username 爲 null
String username = this.tokenUtils.getUsernameFromToken(authToken);
// 如果上面解析 token 成功並且拿到了 username 並且本次會話的權限還未被寫入
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 用 UserDetailsService 從數據庫中拿到用戶的 UserDetails 類
// UserDetails 類是 Spring Security 用於保存用戶權限的實體類
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 檢查用戶帶來的 token 是否有效
// 包括 token 和 userDetails 中用戶名是否一樣, token 是否過期, token 生成時間是否在最後一次密碼修改時間之前
// 若是檢查通過
if (this.tokenUtils.validateToken(authToken, userDetails)) {
// 生成通過認證
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
// 將權限寫入本次會話
SecurityContextHolder.getContext().setAuthentication(authentication);
}
if (!userDetails.isEnabled()){
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print("{\"code\":\"452\",\"data\":\"\",\"message\":\"賬號處於黑名單\"}");
return;
}
}
chain.doFilter(request, response);
}
- 在自定義過濾器中通過 String authToken =httpRequest.getHeader(this.tokenHeader);這一句代碼我們獲得了用戶傳入的token值。
- 通過對token值的解析我們可以獲取到用戶信息,這裏的信息在是在生成token的時候我們加入到token中的。(獲取到用戶信息你可以對用戶操作進行一些記錄。)同時如果獲取的用戶信息不存在我們數據庫中,那麼證明該token不是正確的,不在進行後面的業務邏輯
- 用戶信息正確,我們對token進行驗證看token是否過期,或者用戶是否已經被禁用了。
this.tokenUtils.validateToken(authToken, userDetails)這一句進行驗證
這樣完整的鑑權流程我們就實現了。上面只是部分代碼。
源碼:https://github.com/xushuoAI/Springboot-SpringSecurity-Mybatis-Redis-
以上代碼參考了許多博主的博文。非常感謝各位前輩的分享。