<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    SpringSecurity+JWT 身份驗證及動態權限解決方案(很實用)

    VSole2022-08-06 17:11:47

    花了點時間寫了一個SpringSecurity集合JWT完成身份驗證的Demo,并按照自己的想法完成了動態權限問題。在寫這個Demo之初,使用的是SpringSecurity自帶的注解權限,但是這樣權限就顯得不太靈活,在實現之后,感覺也挺復雜的,歡迎大家給出建議。

    認證流程及授權流程

    我畫了個建議的認證授權流程圖,后面會結合代碼進行解釋整個流程。

    一、登錄認證階段

    實現SpringSecurityUsernamePasswordAuthenticationFilter接口(public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter),在它的實現類的構造方法里設置登錄的請求路徑和請求方式。

    this.setPostOnly(false);
    // 認證路徑 - 發送什么請求,就會進行認證
    this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/service_auth/admin/index/login","POST"));
    

    當前端發起配置的請求時,請求會被攔截,進入到attemptAuthentication方法進行驗證,在這個方法里可以從request中取出賬號、密碼,從而調用AuthenticationManagerauthenticate去校驗賬號、密碼是否正確。

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            // 也可以直接獲取賬號密碼
            String username = obtainUsername(request);
            String password = obtainPassword(request);
            log.info("TokenLoginFilter-attemptAuthentication:嘗試認證,用戶名:{}, 密碼:{}", username, password);
            // 在authenticate里去進行校驗的,校驗過程中會去把UserDetailService里返回的SecurityUser(UserDetails)里的賬號密碼和這里傳的賬號密碼進行比對
            // 并在UserDetailService里將權限進行賦予
            // 校驗通過,會進入到successfulAuthentication方法
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    那么這個authenticate方法是怎么驗證我們賬號密碼正確性的呢?

    打上斷點,跟隨源碼,我們進入到authenticate方法內部:

    然后進入這個方法內部,繼續往下走,看到一段核心代碼:

    進入retrieveUser方法里,然后往下走,看到一句核心代碼,這個核心代碼就是獲取用戶信息的:

    這里注意,調用了UserDetailsServiceloadUserByUsername方法,傳入的就是前端傳過來的username,意思就是要根據這個username去獲取UserDetails對象,所以我們就要去查詢數據庫,所以我們就要實現UserDetailsService接口并重寫loadUserByUsername方法。

    @Service("userDetailsService")
    @Slf4j
    public class UserDetailServiceImpl implements UserDetailsService {
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            log.info("根據username去數據庫查詢用戶信息,username:{}", username);
            // 1、從數據庫中取出用戶信息 - 這里模擬,直接new一個User對象
            User user = new User();
            user.setUsername(username);
            // 111111經過加密后
            user.setPassword("96e79218965eb72c92a549dd5a330112");
            SecurityUser securityUser = new SecurityUser(user);
            // 可以根據查出來的user.getId()去查詢這個用戶對應的權限集合 - 這里模擬,直接new一個結合
            List authorities = new ArrayList<>();
            // 將權限賦予用戶
            securityUser.setPermissionValueList(authorities);
            return securityUser;
        }
    }
    

    在這個方法里,我們通過查詢數據庫,獲取用戶username、password和其對應的權限并設置到UserDetails對象里(代碼里的SecurityUser是我自己implements UserDetails的,也就是它的子類)。

    獲取到userDetails對象后,回到之前的代碼(retriveUser所在的地方)里,這個user經過包裝,里面包含我們從數據庫里取出的username、password

    接著往下看,看到核心代碼:

    注意這個additionalAuthenticationChecks方法,我們進入到這個方法內部:

    可以發現,這是對比密碼的,即前端傳過來的密碼和數據庫中存儲的已經加密過的密碼是否能匹配上。然后我們回到之前的代碼里,直接到結尾,返回一個對象。

    在賬號、密碼驗證完之后的一系列操作里,SpringSecurity自己再對數據進行一些封裝放到SecurityContextHolder里。

    至此,用戶的認證流程已經走完。

    認證成功之后

    認證成功之后,我們要告訴前端登錄認證通過,會進入UsernamePasswordAuthenticationFiltersuccessfulAuthentication方法里。

    /**
     * 登錄成功
     * @param request request
     * @param response response
     * @param chain chain
     * @param auth auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        log.info("TokenLoginFilter-successfulAuthentication:認證通過!");
        SecurityUser user = (SecurityUser) auth.getPrincipal();
        // 創建token
        String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
        log.info("創建的Token為:{}", token);
        // 這里建議,以username為Key,權限集合為value將權限存入Redis,因為權限在后面會頻繁被取出來用
    //        redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
        // 響應給前端調用處
        ResponseUtil.out(response, ResponseResult.ok().data("token", token));
    }
    

    在這個方法里,我們創建一個token,并相應給前端調用者。ResponseUtil是封裝的一個響應工具,tokenManager是JWT工具,這里不做過多解釋,可以去倉庫克隆我的源碼查看,根據我這個流程走即可。

    因為我這個Demo是基于前后端分離的,因此只需響應給前端結果(比如這里的token)即可,讓前端來跳轉。如果不是前后端分離的,可以在這里進行頁面跳轉。

    如果認證失敗

    認證失敗,會進入UsernamePasswordAuthenticationFilterunsuccessfulAuthentication方法里。

    /**
     * 登錄失敗
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
        log.info("TokenLoginFilter-unsuccessfulAuthentication:認證失敗!");
        // 響應給前端調用處
        ResponseUtil.out(response, ResponseResult.error());
    }
    

    在這個方法里,直接響應給前端錯誤情況即可。因為我這個Demo是基于前后端分離的,因此只需響應給前端結果、狀態碼即可,讓前端來跳轉。如果不是前后端分離的,可以在這里進行頁面跳轉。

    二、授權階段 - 如果你要做權限控制

    繼承BasicAuthenticationFilter類,重寫doFilterInternal過濾器,在這個過濾器里獲取token并驗證,并進行動態權限控制。

    @Slf4j
    public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
        private TokenManager tokenManager;
        private AntPathMatcher antPathMatcher = new AntPathMatcher();
        public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager) {
            super(authManager);
            this.tokenManager = tokenManager;
        }
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            UsernamePasswordAuthenticationToken authentication = null;
            try {
                log.info("授權過濾器,驗證Token...");
                authentication = getAuthentication(request);
            } catch (ExpiredJwtException e) {
                // 可能token過期
                log.info("異常捕獲:{}",e.getMessage());
                ResponseUtil.out(response, ResponseResult.unauthorized());
            }
            if (authentication != null) {
                String url = request.getRequestURI();
                // setAuthentication設置不設置都行,如果需要用注解來控制權限,則必須設置
                SecurityContextHolder.getContext().setAuthentication(authentication);
                UserServiceImpl userService = new UserServiceImpl();
                List
     menuList = userService.getAllMenus();
                // 遍歷所有菜單
                for (Menu menu : menuList) {
                    // 如果url匹配上了
                    if (antPathMatcher.match(menu.getPattern(), url) && menu.getRoles().size() > 0){
                        log.info("URL匹配上了,請求URL:{},匹配上的URL:{}", url, menu.getPattern());
                        List stringList = new ArrayList<>();
                        for (GrantedAuthority authority : authentication.getAuthorities()) {
                            String authority1 = authority.getAuthority();
                            stringList.add(authority1);
                        }
                        for (Role role : menu.getRoles()) {
                            if (stringList.contains(role.getName())) {
                                log.info("角色匹配,角色為:{}", role.getName());
                                chain.doFilter(request, response);
                                return;
                            }
                        }
                        // 沒有權限
                        log.info("URL匹配上了,但無權訪問,請求URL:{},匹配上的URL:{}", url, menu.getPattern());
                        ResponseUtil.out(response, ResponseResult.noPermission());
                        return;
                    }
                }
                // url沒有匹配上菜單,可以訪問
                log.info("URL未匹配上,所有人都可以訪問!");
                chain.doFilter(request, response);
            } else {
                // 沒有登錄
                log.info("用戶Token無效!");
            }
        }
        private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
            // token置于header里
            String token = request.getHeader("X-Token");
            log.info("X-Token:{}", token);
            if (token != null && !"".equals(token.trim())) {
                // 根據token獲取用戶名
                String userName = tokenManager.getUserFromToken(token);
                // 這里可以根據用戶名去Redis中取出權限集合
                // 不應該從SecurityContextHolder獲取,會出現問題,如果你換一個token(這個token也是有效的)來調用方法,從這里取,這權限還是之前token登錄時存進來的(經過我測試)
                // 為什么呢?我的猜測是:因為JWT是無狀態的,你沒有辦法在注銷的時候,將SpringSecurity全局對象里的東西清理
                // 如果你先用賬號2登錄獲取一個token2,然后用賬號1登錄獲取一個token1,用token1去調用一次api的時候從SecurityContextHolder獲取一次權限,然后用token2去調用一次api獲取一次權限,你會發現這個權限居然是token1擁有的(我測試過)
    //            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    //            List permissionValueList = (List) redisTemplate.opsForValue().get(userName);
                // 這里直接模擬從Redis中取出權限
                List permissionValueList = new ArrayList<>();
                // 權限 - 為了測試根據權限控制訪問權限
                permissionValueList.add("admin.test");
                // 角色 - 為了測試根據角色控制訪問權限
                permissionValueList.add("ROLE_admin");
                // 需要將權限轉換成SpringSecurity認識的
                Collection authorities = new ArrayList<>();
                for(String permissionValue : permissionValueList) {
                    if(StringUtils.isEmpty(permissionValue)) {
                        continue;
                    }
                    SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
                    authorities.add(authority);
                }
                if (!StringUtils.isEmpty(userName)) {
                    log.info("授權過濾器:授權完成!");
                    return new UsernamePasswordAuthenticationToken(userName, token, authorities);
                }
                return null;
            }
            return null;
        }
    }
    

    如果不做動態權限,則可以省略那一部分對比url的代碼。但是用戶的擁有的權限,建議存儲在Redis里。每次進入到這個過濾器,就將其取出來封裝成Security認識的,放到SecurityContextHolder里,這個時候你的權限是定死了的,可以在配置文件里進行配置,也可以使用注解在Controller里進行控制。

    如果你要做動態權限,則可以根據你自己的邏輯修改那一部分對比url的代碼。基于角色控制權限,某個角色擁有某些權限(可訪問接口)的這個原則來做。

    我在寫這一部分代碼的時候,感覺還是有點復雜的。用戶的權限、所有菜單都可以存放在Redis里,兩者發生改變的時候就清除Redis里的數據,下一次讀取的時候先從數據庫里讀取,然后將其放到Redis緩存里,這一部分邏輯,由于一開始只是打算寫一個小Demo(如果真要寫的話,還需要創建相關的數據庫表、連接Redis之類的,有點麻煩),所以我沒有寫,讀者如果有興趣可以自己去實現以下,這個并不難。

    三、注銷階段

    注銷的時候,應該將Redis里存儲的權限進行刪除(如果有的話)。

    public class TokenLogoutHandler implements LogoutHandler {
        /** Token管理類 */
        private TokenManager tokenManager;
        public TokenLogoutHandler(TokenManager tokenManager) {
            this.tokenManager = tokenManager;
        }
        /**
         * 登出業務處理
         * @param request request
         * @param response response
         * @param authentication
         */
        @Override
        public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
            String token = request.getHeader("X-Token");
            if (token != null) {
                /* tokenManager.removeToken(token); */
                try{
                    String userName = tokenManager.getUserFromToken(token);
                }catch (ExpiredJwtException e){
                    // 可能token過期了,所以得捕獲
                    ResponseUtil.out(response, ResponseResult.ok());
                }
                // 清空當前用戶緩存中的權限數據
                // 如果你的權限使用userName作為key存在Redis中,可以通過userName將緩存清空
                // ....
            }
            ResponseUtil.out(response, ResponseResult.ok());
        }
    }
    

    四、未授權處理

    如果你使用了注解或是在配置文件中手動配置了權限管理,即讓SpringSecurity幫你管理權限,當你訪問到沒有權限訪問的接口時,回來到這里。

    @Slf4j
    public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
        /**
         * 未授權返回錯誤碼
         * @param request request
         * @param response response
         * @param authException authException
         * @throws IOException
         * @throws ServletException
         */
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                             AuthenticationException authException) throws IOException, ServletException {
            // 響應錯誤碼
            ResponseUtil.out(response, ResponseResult.error());
        }
    }
    

    五、SpringSecurity配置文件核心代碼

    這里主要是想說一件事,就是在我們的認證、授權、退出處理器的配置時,如果這個類里需要使用到其他類(對象),可以通過構造方法的方式傳進去,因為它們沒有被Spring管理,你是沒有辦法使用@Autowired注入的。

    /**
     * 配置設置 - 更多配置項見官方文檔
     * @param http http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthorizedEntryPoint())
                // 允許跨域
                .and().cors()
                .and().csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()
                // 退出請求路徑
                .and().logout().logoutUrl("/service_auth/admin/index/logout")
                // 退出處理器
                .addLogoutHandler(new TokenLogoutHandler(tokenManager)).and()
                // 認證過濾器
                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager))
                // 授權過濾器
                .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager)).httpBasic();
    }
    

    總結

    核心其實就是前面貼出的一、二(認證和授權),其實還是有優化空間的,我本人對于SpringSecurity沒有了解很深,所以只能寫成這樣。

    如果讀者感興趣,可以下載源碼閱讀,源碼里注釋也是非常詳細的。

    • https://github.com/liuchengyin01/JwtWithSpringSecurityDemo
    • https://gitee.com/liuchengyin_vae/JwtWithSpringSecurityDemo
    tokentoken驗證失敗
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    花了點時間寫了一個SpringSecurity集合JWT完成身份驗證的Demo,并按照自己的想法完成了動態權限問題。在寫這個Demo之初,使用的是SpringSecurity自帶的注解權限,但是這樣權限就顯得不太靈活,在實現之后,感覺也挺復雜的,歡迎大家給出建議。認證流程及授權流程我畫了個建議的認證授權流程圖,后面會結合代碼進行解釋整個流程。
    聲明 由于傳播、利用此文所提供的信息而造成的任何直接或者間接的后果及損失,均由使用者本人負責,雷神眾測以及文章作者不為此承擔任何責任。雷神眾測擁有對此文章的修改和解釋權。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用于商業目的。為了防止數據包的重放,token機制被引入了代碼中。有的token每次請求都會重新獲取,而有的會持續一段時間。
    Host Header欺騙有些人認為HTTP的聯機就是靠包里的HOST header來連接的,所以會認為如果修改掉包里的HOST, 那么就連接不到目標服務器,所以是不可控的。所以當應用使用$_SERVER['HTTP_HOST']獲取網站URL并拼接到找回密碼的鏈接當中就會產生漏洞
    為了避免下半輩子只能在幾平米的空間活動,我在某魚上淘到了一款藍牙鎖作為小藍車的平替。nRF藍牙抓包首先需要解決的問題是獲取藍牙鎖的MAC地址,因為最后我們需要使用gatttool直接通過MAC地址與藍牙鎖交互。比較好用的是nRF這個軟件,能夠直接掃描查看周圍的藍牙設備。使用gatttool嘗試連接并使用primary看查所有service:然后通過characteristics命令看查所有的特性:其中handle是特性的句柄,char properties是特性的屬性值,char value handle是特性值的句柄,uuid是特性的標識。
    2021安洵杯PWN WP詳解
    2021-12-29 16:41:08
    做了2021安洵杯線上賽題目,總體來說題目有簡單有難的,難易程度合適,這次就做了pwn,把四道pwn題思路總結一下,重點是沒幾個人做出來的最后一道pwnsky,賽后做了復現。
    內網滲透-代理篇
    2021-12-01 05:44:01
    利用代理工具將內網的流量代理到本地進行訪問,這樣就可以對內網進行下一步滲透,同時也可以利用代理工具將其他網段的流量轉發到本地進行縱向滲透。代理的本質是Socks協議(防火墻安全會話轉換協議),工作在OSI參考模型的第5層(會話層)。使用TCP協議傳輸數據,因而不提供如傳遞ICMP信息之類的網絡層相關服務。目前支持SOCKS4和SOCKS5兩個版本:
    一.前言 這是一次非常曲折的滲透測試,也讓我學到了很多。 二.邏輯漏洞 收集子域名,看到一個項目管理的網站
    重新抓包改包登入系統,在burp里仔細看了下用戶管理api返回的json發現所有用戶的密碼都是統一的,都是該公司旗下某產品的英文名。猜測可能是為了防止密碼被泄露,后端對返回的密碼進行了過濾了,統一修改了。
    Web安全常見漏洞修復建議
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类