最近在使用前後端分離的方式進行開發時,需要引入許可權控制,因為後臺是SpringBoot提供的RESTful API,很自然的想到引入Spring Security。但是遺憾的是Spring Security官網的文件和例子都是傳統的表單登入方式,網上也沒有找到相關文章。不得不自己進行了一番摸索,現將成果進行分享。
寫了一個例子,後端基於SpringBoot構建,僅提供JSON API服務,不提供任何頁面。
基於JSON API的登入,登出操作,基於使用者授權的RESTful API訪問。
當登入成功,登入失敗,登出成功,訪問無許可權API時均返回JSON響應,而不是302跳轉。
可以基於註解獲取前端通過post body提供的引數,如
@PostMapping(value = "/api/admin/users/{id}", produces = MEDIA_TYPE) public String editAdminUser( @PathVariable("id") Long id, @JsonArg("$.username") String username, @JsonArg("$.password") String password, @JsonArg("$.enabled") boolean enabled) {...}
前端通過create-react-app構建,通過fetch API訪問後端。
const encode = password ? md5Password(username, password) : '' const result = await this.userManager.update({ id, username, password: encode, enabled }) if (result.success) { this.setState({...this.state, users: this.replace(id, result)}) } else { Modal.error({title: 'Edit User Error', content: result.message}) }
首先按照正常的方式引入Maven依賴
org.springframework.boot spring-boot-starter-parent 1.5.9.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-jdbc
然後重點是SpringSpring的配置
@[email protected](prePostEnabled = true)public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private CustomLoginHandler customLoginHandler; @Autowired private CustomLogoutHandler customLogoutHandler; @Autowired private CustomAccessDeniedHandler customAccessDeniedHandler; protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/api/admin/**").hasRole("ADMIN") .antMatchers("/api/basic/**").hasRole("BASIC") .antMatchers("/api/session").permitAll() .antMatchers(HttpMethod.GET).permitAll() .antMatchers("/api/**").hasRole("BASIC"); http.formLogin(); http.logout() .logoutUrl("/api/session/logout") // 登出前呼叫,可用於日誌 .addLogoutHandler(customLogoutHandler) // 登出後呼叫,使用者資訊已不存在 .logoutSuccessHandler(customLogoutHandler); http.exceptionHandling() // 已登入使用者的許可權錯誤 .accessDeniedHandler(customAccessDeniedHandler) // 未登入使用者的許可權錯誤 .authenticationEntryPoint(customAccessDeniedHandler); http.csrf() // 登入API不啟用CSFR檢查 .ignoringAntMatchers("/api/session/**"); // 根據 Header Accept-Language 欄位設定 Locale // 要想啟用錯誤資訊的本地化,還需要設定MessageSource,請參閱Github原始碼 http.addFilterBefore(new AcceptHeaderLocaleFilter(), UsernamePasswordAuthenticationFilter.class); // 替換原先的表單登入 Filter http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // 繫結 CSRF TOKEN 到響應的 HEADER 上 http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class); } private CustomAuthenticationFilter customAuthenticationFilter() throws Exception { CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); filter.setAuthenticationSuccessHandler(customLoginHandler); filter.setAuthenticationFailureHandler(customLoginHandler); filter.setAuthenticationManager(authenticationManager()); filter.setFilterProcessesUrl("/api/session/login"); return filter; } private static void responseText(HttpServletResponse response, String content) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); byte[] bytes = content.getBytes(StandardCharsets.UTF_8); response.setContentLength(bytes.length); response.getOutputStream().write(bytes); response.flushBuffer(); } @Component public static class CustomAccessDeniedHandler extends BaseController implements AuthenticationEntryPoint, AccessDeniedHandler { // NoLogged Access Denied @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { responseText(response, errorMessage(authException.getMessage())); } // Logged Access Denied @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { responseText(response, errorMessage(accessDeniedException.getMessage())); } } @Component public static class CustomLoginHandler extends BaseController implements AuthenticationSuccessHandler, AuthenticationFailureHandler { // Login Success @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { LOGGER.info("User login successfully, name={}", authentication.getName()); responseText(response, objectResult(SessionController.getJSON(authentication))); } // Login Failure @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { responseText(response, errorMessage(exception.getMessage())); } } @Component public static class CustomLogoutHandler extends BaseController implements LogoutHandler, LogoutSuccessHandler { // Before Logout @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { } // After Logout @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { responseText(response, objectResult(SessionController.getJSON(null))); } } private static class AcceptHeaderLocaleFilter implements Filter { private AcceptHeaderLocaleResolver localeResolver; private AcceptHeaderLocaleFilter() { localeResolver = new AcceptHeaderLocaleResolver(); localeResolver.setDefaultLocale(Locale.US); } @Override public void init(FilterConfig filterConfig) { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Locale locale = localeResolver.resolveLocale((HttpServletRequest) request); LocaleContextHolder.setLocale(locale); chain.doFilter(request, response); } @Override public void destroy() { } }}
CustomAuthenticationFilter
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { UsernamePasswordAuthenticationToken authRequest; try (InputStream is = request.getInputStream()) { // 使用JsonPath讀取JSON請求,你也可以換成你喜歡的庫 DocumentContext context = JsonPath.parse(is); String username = context.read("$.username", String.class); String password = context.read("$.password", String.class); authRequest = new UsernamePasswordAuthenticationToken(username, password); } catch (IOException e) { e.printStackTrace(); authRequest = new UsernamePasswordAuthenticationToken("", ""); } setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }}
有討論,纔有進步,大家各抒己見,讓每位同學學到不一樣的!