
本文使用的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中,
@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { ...省略其他配置,公眾號Java精選,有驚喜 //安全配置 @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中
@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { ...省略其他配置 //安全配置 @Override protected void configure(HttpSecurity http) throws Exception { ...省略其他配置 //設置權限不足,無法訪問當前資源時的處理方式 http.exceptionHandling().accessDeniedHandler(new NoAccessDeniedHandler()); ... }}
這樣配置後,未登錄,登錄失敗,權限不足這些場景下springsecurity就會返回json數據給前端。springsecurity系列技術文章,待更新中:https://www.yoodb.com/spring/spring-annotate.html
三、如何發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是否有效 */@Componentpublic 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之前。面試寶典:https://www.yoodb.com
完整的springsecurity安全配置如下:
/** * 配置springsecurity */@Configurationpublic 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後就會放行請求。
完整的示例工程源碼:
https://gitee.com/zhituaishangc/cnblog-springsecurity-study/tree/master/cnblog-springsecurity05-test
作者:程序曉猿
https://www.cnblogs.com/chengxuxiaoyuan/p/14020326.html
END
關注後端面試那些事,回復【2022面經】
獲取最新大廠Java面經

