04.11 Spring Boot + Spring Security + RESTful API的使用方法

最近在使用前後端分離的方式進行開發時,需要引入權限控制,因為後臺是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依賴

 <parent>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-parent/<artifactid>
<version>1.5.9.RELEASE/<version>
<relativepath>
/<parent>
<properties>
<project.build.sourceencoding>UTF-8/<project.build.sourceencoding>

<project.reporting.outputencoding>UTF-8/<project.reporting.outputencoding>
<java.version>1.8/<java.version>
/<properties>
<dependencies>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-web/<artifactid>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-data-jpa/<artifactid>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-security/<artifactid>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-jdbc/<artifactid>
/<dependency>
/<dependencies>

然後重點是SpringSpring的配置

@EnableWebSecurity
@EnableGlobalMethodSecurity(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);
}
}

有討論,才有進步,大家各抒己見,讓每位同學學到不一樣的!


分享到:


相關文章: