使用SpringSecurity爲API接口添加鑑權token

當我們使用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);
    }
  1. 通過url拿到用戶傳過來的數據,賬戶和密碼
  2. 對賬戶密碼進行驗證,如果不正確返回錯誤提示。
  3. 如果正確進行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;
    }
  1. 先通過用戶傳入的用戶名進行查找,看是否存在該用戶。
  2. 如果存在該用戶,判斷用戶的狀態是否可用。
  3. 如果狀態可用,那麼比較數據庫保存的密碼和用戶輸入的密碼是否匹配。

那麼如何生成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();
        }
    }
  1. 我們通過JWT工具去進行token的生成。(如果不瞭解JWT可用百度看看,非常簡單。)
  2. 通過用戶信息,我們生成一個token並返回給客戶端。
  3. 用戶拿着這個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);
    }
}
  1. 首先我們通過SpringSecurity進行url配置,也就是給URL加了過濾器。
  2. 當用戶訪問需要token的資源路徑的時候就會觸發過濾器。
  3. 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);
    }
  1. 在自定義過濾器中通過 String authToken =httpRequest.getHeader(this.tokenHeader);這一句代碼我們獲得了用戶傳入的token值。
  2. 通過對token值的解析我們可以獲取到用戶信息,這裏的信息在是在生成token的時候我們加入到token中的。(獲取到用戶信息你可以對用戶操作進行一些記錄。)同時如果獲取的用戶信息不存在我們數據庫中,那麼證明該token不是正確的,不在進行後面的業務邏輯
  3. 用戶信息正確,我們對token進行驗證看token是否過期,或者用戶是否已經被禁用了。
    this.tokenUtils.validateToken(authToken, userDetails)這一句進行驗證

這樣完整的鑑權流程我們就實現了。上面只是部分代碼。

源碼:https://github.com/xushuoAI/Springboot-SpringSecurity-Mybatis-Redis-

以上代碼參考了許多博主的博文。非常感謝各位前輩的分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章