認證和授權學習6:前后端分離狀態下使用springsecurity


認證和授權學習6:前后端分離狀態下使用springsecurity

本文使用的springboot版本是2.1.3.RELEASE

一、簡要描述

默認情況下,spring security登錄成功或失敗,都會返回一個302跳轉。登錄成功跳轉到主頁,失敗跳轉到登錄頁。如果未認證直接訪問受保護資源也會跳轉到登錄頁 。

而在前后端分離項目中,前后端是通過json數據進行交互,前端通過ajax請求和后端進行交互,ajax是無法處理302跳轉的,所以我們希望不管是未登錄還是登錄成功,spring security都給前端返回json數據,而前端自己根據返回結果進行邏輯控制。

springsecurity默認采用的是表單登錄,而我們希望的登錄流程是這樣的:

(1) 前端帶着用戶名和密碼用ajax請求登錄,認證成功后返回一個token值給前端

(2) 下次請求時在請求頭中攜帶這個token,后端校驗這個token通過后放行請求,否則提示未登錄(返回json數據)

二、配置讓springsecurity返回 json數據

2.1 未登錄時訪問受限資源的處理

未登錄時訪問資源,請求會被FilterSecurityInterceptor這個過濾器攔截到,然后拋出異常,這個異常會被

ExceptionTranslationFilter這個過濾器捕獲到,並最終交給AuthenticationEntryPoint接口的commence方法處理。

所以處理辦法是自定義一個AuthenticationEntryPoint的實現類並配置到springsecurity中

/**
 * 未登錄時訪問受限資源的處理方式
 */
public class UnLoginHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode objectNode = mapper.createObjectNode();
        if(authException instanceof BadCredentialsException){
            //賬號或密碼錯誤
            objectNode.put("code", "501");
            objectNode.put("message", "賬號或者密碼錯誤");
        }else {
            objectNode.put("code", "500");
            objectNode.put("message", "未登錄或token無效");
        }

        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        response.getWriter().print(objectNode);
    }
}

配置到spring security中,

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...省略其他配置
        //安全配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...省略其他配置
        //設置未登錄或登錄失敗時訪問資源的處理方式
        http.exceptionHandling().authenticationEntryPoint(new UnLoginHandler());
        ...
    }
}

2.2 訪問資源權限不足時的處理

當一個已登錄用戶訪問了一個沒有權限的資源時,springsecurity默認會重定向到一個403頁面。可以通過自己實現 AccessDeniedHandler接口然后配置到springsecurity中來自定義

/**
 * 當前登錄的用戶沒有權限訪問資源時的處理器
 */
public class NoAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode objectNode = mapper.createObjectNode();
        objectNode.put("code","500");
        objectNode.put("message","訪問失敗,權限不夠");
        response.setHeader("Content-Type","application/json;charset=UTF-8");
        response.getWriter().print(objectNode);
    }
}

配置到springsecurity中

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...省略其他配置
        //安全配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...省略其他配置
        //設置權限不足,無法訪問當前資源時的處理方式
        http.exceptionHandling().accessDeniedHandler(new NoAccessDeniedHandler());
        ...
    }
}

這樣配置后,未登錄,登錄失敗,權限不足這些場景下springsecurity就會返回json數據給前端。

三、如何發token

這一節來解決發token的問題。現在已經去掉了表單登錄的功能,那如何讓springsecurity驗證賬號和密碼並創建token呢。

可以自定義一個接口給前端請求,用來發token,前端提交賬號和密碼到這個接口,在其中調用springsecurity的認證管理器來認證賬號密碼,認證成功后創建一個token返回給前端

@RestController
@RequestMapping("/authenticate")
public class AuthenticationController {

    private final static ObjectMapper MAPPER=new ObjectMapper();

    //注入springsecurity的認證管理器
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 創建token
     * @return
     */
    @PostMapping("/applyToken")
    public JsonNode applyToken(@RequestBody UserDto userDto){
        ObjectNode tokenNode = MAPPER.createObjectNode();
        //1.創建UsernamePasswordAuthenticationToken對象
        UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDto.getUsername(),userDto.getPassword());
        //2.交給認證管理器進行認證
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        if(null!=authenticate){
            //認證成功,生成token返回給前端
            String token = JwtUtils.createToken(userDto.getUsername());
            if(StringUtils.isEmpty(token)){
                tokenNode.put("code","401");
                tokenNode.put("message","生成token失敗");
            }else {
                tokenNode.put("code","200");
                tokenNode.put("token", token);
                tokenNode.put("message","success");
            }
            tokenNode.put("code","200");
            tokenNode.put("token", JwtUtils.createToken(userDto.getUsername()));
            tokenNode.put("message","success");
            return tokenNode;
        }else{
            tokenNode.put("code","401");
            tokenNode.put("message","登錄失敗");
        }
        return tokenNode;
    }
}

其中JwtUtils是一個自定義的jwt 工具類,提供了生成token和驗證token的功能

四、如何讓springsecurity驗證token

上邊實現了發token的功能,那如何讓springsecurity驗證這個token,並放行請求。可以自定義一個過濾器,在springsecurity的登錄過濾器之前先攔截請求,然后進行token,如果驗證通過了就把當前用戶設置到SecurityContextHolder中,這樣就完成了驗證和登錄。

自定義過濾器


/**
 * 驗證請求攜帶的token是否有效
 */
@Component
public class TokenVerifyFilter extends GenericFilterBean {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        try {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            //從請求頭中獲取token
            String token = request.getHeader("Authorization-Token");
            if (StringUtils.hasText(token)) {
                //從token中解析用戶名
                String username = JwtUtils.getUserInfo(token);
                //查詢當前用戶
                if(!StringUtils.isEmpty(username)){
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                    if(null!=userDetails){
                        //查詢不到表示用戶不存在
                        //從token中獲取用戶信息封裝成 UsernamePasswordAuthenticationToken
                        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(token, "", userDetails.getAuthorities());
                        //設置用戶信息
 SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                    }
                }
            }
        } catch (Exception e) {
            //登錄發生異常,但要繼續走其余過濾器的邏輯
            e.printStackTrace();
        }
        //繼續執行springsecurity的過濾器
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

把這個過濾器設置到UsernamePasswordAuthenticationFilter之前。

完整的springsecurity安全配置如下


/**
 * 配置springsecurity
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    //用戶配置,
    @Bean
    public UserDetailsService userDetailsService(){
        //在內存中配置用戶
        InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("lyy").password("123").authorities("ROLE_P1").build());
        manager.createUser(User.withUsername("zs").password("456").authorities("ROLE_P2").build());
        return manager;
    }

    //配置自定義的對token進行驗證的過濾器
    @Autowired
    private TokenVerifyFilter tokenVerifyFilter;

    //密碼加密方式配置
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    //安全配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        //匹配路徑時越具體的路徑要先匹配
        http.authorizeRequests().antMatchers("/","/index.html").permitAll();
        //放行申請token的url
        http.authorizeRequests().antMatchers("/authenticate/**").permitAll();
        //需要p1權限才能訪問
        http.authorizeRequests().antMatchers("/resource/r1").hasRole("P1");
        //需要p2權限才能訪問
        http.authorizeRequests().antMatchers("/resource/r2").hasRole("P2")
        .antMatchers("/resource/r3").hasRole("P3");//需要p3權限才能訪問
        http.authorizeRequests().anyRequest().authenticated();
        http.formLogin().disable();//禁用表單登錄
        //設置未登錄或登錄失敗時訪問資源的處理方式
        http.exceptionHandling().authenticationEntryPoint(new UnLoginHandler());
        //設置權限不足,無法訪問當前資源時的處理方式
        http.exceptionHandling().accessDeniedHandler(new NoAccessDeniedHandler());
        http.addFilterBefore(tokenVerifyFilter, UsernamePasswordAuthenticationFilter.class);
        //設置不使用session,無狀態
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    /**
     * 配置認證管理器:
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

五、總結

按上邊這樣配置后,前端向先請求發token的接口獲取一個token,然后在每次訪問后端時都在請求頭中帶上這個token,后端驗證了這個token后就會放行請求。

完整的示例工程:

示例工程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM