1. 程式人生 > >Spring Security學習總結

Spring Security學習總結

關於設計模式的文章在公眾號上面已經更新完了,感興趣的小夥伴可以關注公眾號每天學Java隨時檢視!

學習Spring Security是在一個客戶的專案上看到客戶在自己專案上使用Spring Security,當時很好奇這麼框架是幹嘛的,就私下問了他們的技術人員:Security是做什麼的。他大致的意思是說,Security是一個提供安全許可權控制的框架,利用這個框架可以為系統中的人員定製相關的許可權,實現無權的攔截。

所以我就私下學習一下這個框架,個人覺得還不錯(一開始就使用前後端分離,利用jquery的ajax進行訪問介面).

學習這個框架的時候我覺得最主要是要明白HttpSecurity這個類,他是允許為特定的http請求配置基於web的安全性。預設情況下,它將應用於所有請求,但是可以使用requestMatcher(requestMatcher)或其他類似的方法進行限制。換句話說,就是通過這個HttpSecurity這個類我們對於HTTP請求進行控制,比如說是否攔截。

這個類的所有方法可以去檢視官方文件,我這裡主要說幾個我用到的:

 addFilterBefore(javax.servlet.Filter filter, java.lang.Class<? extends javax.servlet.Filter> beforeFilter) 在已知的過濾器類之前新增過濾器。
 antMatcher(java.lang.String antPattern) 只有匹配提供的路徑時呼叫配置的HttpSecurity。
authorizeRequests() 允許使用HttpServletRequest限制訪問,在他的後面我們會通過antMatcher增加一些請求路徑的過濾。或者使用anyRequest()
cors() 新增要使用的CorsFilter。也就是允許跨域
csrf() 新增CSRF的支援。
formLogin() 指定支援基於表單的身份驗證。
sessionManagement() 配置會話管理。

然後就是WebSecurityConfigurerAdapter這個類,它為建立WebSecurityConfigurer例項提供了一個方便的基類。該實現允許通過覆蓋方法進行定製。看下面的程式碼:我們繼承這個類,重寫他的config方法(三個config方法引數不同),我們主要看引數是HttpSecurity的這個方法,具體的可以看一下註解。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private EntryPoint entryPoint;
    @Autowired
    private JwtAjaxFilter jwtAjaxFilter;
    @Autowired
    private MyUserService myUserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //我的專案是使用Ajax訪問介面,所以這裡取消Session。
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //httpBasic返回HttpBasicConfigurer物件,
                //HttpBasicConfigurer物件的authenticationEntryPoint方法是在身份驗證失敗時,將觸發AuthenticationEntryPoint類commence方法
                .httpBasic().authenticationEntryPoint(entryPoint)//登陸失敗執行的介面
                .and()
                //authorizeRequests返回ExpressionUrlAuthorizationConfigurer物件,.anyRequest().access會去進行URL對映使得
                .authorizeRequests().anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")//角色資源
                .and()
                //這個URL比較特殊, Security自帶無需定義
                .formLogin().loginPage("/login")
                //登入成功觸發
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        System.out.println("登入成功");
                        AjaxResponseBody responseBody = new AjaxResponseBody();
                        responseBody.setStatus("200");
                        responseBody.setMsg("Login Success!");
                        UserVO userVO = (UserVO) authentication.getPrincipal();
                        //生成Token
                        String JWT = JwtUtil.generateToken(userVO.getUsername(), 30000);
                        responseBody.setJwtToken(JWT);

                        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
                        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");
                        httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
                    }
                })
                //登入成功觸發
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        System.out.println("登入失敗");
                        AjaxResponseBody responseBody = new AjaxResponseBody();
                        responseBody.setStatus("400");
                        responseBody.setMsg("Login Failure!");
                        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
                        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");

                        httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
                    }
                })
                .permitAll()
                .and()
                .cors()
                .and()
                .csrf().disable();//允許跨域
        //403
        http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
            @Override
            public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
                System.out.println("沒有許可權");
                AjaxResponseBody responseBody = new AjaxResponseBody();
                responseBody.setStatus("403");
                responseBody.setMsg("need Authorities!");
                httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
                httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");

                httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
            }
        });
        //攔截
        http.addFilterBefore(jwtAjaxFilter, UsernamePasswordAuthenticationFilter.class);
        // 禁用快取
        http.headers().cacheControl();
    }

    //自定義認證方式
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserService).passwordEncoder(new BCryptPasswordEncoder());
    }
}

上面的程式碼剛開始讓我最為難的地方,因為在部落格找的案例完全看不懂,後來沒有辦法,根據HttpSecurity這個引數一點點去向下研究,於是大致明白Spring Boot結合Spring Security的專案中WebSecurityConfigurerAdapter是配置的關鍵,但是HttpSecurity是關鍵中關鍵,通過它我們會牽扯出很多類出來,就像上面的程式碼一樣。下面具體來說明一下自己學習的經過:

首先是通過IDEA新建一個Spring Security應用。

當應用載入完成之後我們就啟動它,我們會發現在控制檯中出現一行密碼

使用瀏覽器訪問127.0.0.1:8080的時候會預設到登入介面,這個登入介面是應用自帶的,我們輸入user和控制檯的密碼就登入成功了。(上面的步驟在公眾號文章中配有圖,可關注檢視)

這個時候應用就算是跑起來了,但是這個時候的登入是使用框架自帶的並非我們自定義登入驗證的。

這是我遇到的第一個問題:如何實現自定義登入驗證

由於Spring Security自帶的有認證方式,所以如果我們使用這個框架,我們肯定要重寫這些認證方式,最終我們得知UserDetails,UserDetailsService類是進行人員角色認證的。所以我們要實現這兩個類:

然後我們再配合 WebSecurityConfigurerAdapter的自定義子類成功的實現了使用者登入(程式碼在最上面),但是後續我就發現一個問題:在我登入成功之後,我訪問資料介面的時候仍然會報錯,因為請求被攔截了,其實很好理解:因為前後端分離,每一次請求都是無狀態的,服務端無法通過Session獲取我的使用者資訊,怎麼辦呢?Token處理。思路如下:

在我們登入請求/login這個介面的時候服務端是不會進行攔截的,所以我們首先利用這個介面登入,但是不同的是,我們在登入成功後要返回一個Token,這個Token儲存的有使用者資訊。如下程式碼,通過JwTUtil生成字串,這裡我是使用網上的程式碼,我們自己可以通過JDK生成jks檔案,然後進行RSA方式加密使用者資訊。(當然我們還要引用JWT的依賴)

 //登入成功觸發
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        System.out.println("登入成功");
                        AjaxResponseBody responseBody = new AjaxResponseBody();
                        responseBody.setStatus("200");
                        responseBody.setMsg("Login Success!");
                        UserVO userVO = (UserVO) authentication.getPrincipal();
                        //生成Token,有效時常3分鐘
                        String JWT = JwtUtil.generateToken(userVO.getUsername(), 30000);
                        responseBody.setJwtToken(JWT);

                        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
                        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");
                        httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
                    }
                })

然後Ajax會接收到這個Token。然後我們將Token儲存到sessionStorage中,在下一次我們訪問非login介面時,我們header中要攜帶Token,但是問題又來了,服務端如何拿到這個Token呢?雖然Spring Boot攔截器可以實現,但是Spring Security該怎麼辦?我們在最開始說HttpSecurity的時候又這個方法addFilterBefore,我們可以通過它增加一個攔截器在Security自己的攔截器之前執行,然後在這裡面進行判斷Token是否合法或者是否登入。具體程式碼如下:

@Component
public class JwtAjaxFilter extends OncePerRequestFilter {

    @Autowired
    private MyUserService myUserService;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String authHeader = httpServletRequest.getHeader("Authorization");
        System.out.println(authHeader);
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            System.out.println("獲取Bearer");
            final String openId = authHeader.substring("Bearer ".length());
            String username = JwtUtil.parseToken(openId);
            System.out.println("username==="+username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                System.out.println("userName=" + username);
                UserDetails userDetails = myUserService.loadUserByUsername(username);
                if (userDetails != null) {
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        System.out.println(1);
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

到這裡我們基本上就實現了前後端分離的形式來達成安全訪問。但是許可權控制好像沒有體現到。怎麼做呢?

   .authorizeRequests().anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")//角色資源
               

在我的配置程式碼中有上面這麼一行程式碼。這裡實現了URL和角色的關聯,更具體的實現就看下面的程式碼

@Component("rbacauthorityservice")
public class RbacAuthorityService {
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {

        //得到的principal的資訊
        Object o = authentication.getPrincipal();
        //預設是未授權角色
        boolean hasPermission = false;
        //獲得的物件是否實現UserDetails
        if (o instanceof UserDetails) {
            //取得使用者名稱
            String userName = ((UserDetails) o).getUsername();
            // Authentication是一個介面,用來表示使用者認證資訊的
            Collection<? extends GrantedAuthority> authorities = ((UserDetails) o).getAuthorities();
            Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
            for (GrantedAuthority authority : authorities) {
                //輸出角色
                System.out.println(authentication.getAuthorities());
                //管理員角色
                if (authority.getAuthority().equals("ROLE_ADMIN")) {
                    //管理員可以訪問的URL
                    Set<String> urls = new HashSet();
                    urls.add("/sys/**");
                    urls.add("/test/**");
                    //AntPathMatcher用來做類URLs字串匹配
                    AntPathMatcher antPathMatcher = new AntPathMatcher();
                    for (String url : urls) {
                        if (antPathMatcher.match(url, request.getRequestURI())) {
                            hasPermission = true;
                            break;
                        }
                    }

                }
            }
            //普通使用者可以訪問的URL
            Set<String> urls = new HashSet();
            urls.add("/test/**");
            urls.add("/pay/**");
            AntPathMatcher antPathMatcher = new AntPathMatcher();
            for (String url : urls) {
                if (antPathMatcher.match(url, request.getRequestURI())) {
                    hasPermission = true;
                    break;
                }
            }
            return hasPermission;
        }
        return hasPermission;
    }
}

更多文章請關注公眾號:每天學Java。想獲得更多最新面試提醒請進入小程式:每天學Java

 公眾號二維碼:                                                                                          小程式二維碼: