環境:SpringBoot2.7.12
1. 簡介
Spring Security是一個功能強大且高度可定制的身份驗證和訪問控制框架,用于保護基于Spring的應用程序。它采用AOP思想,基于servlet過濾器實現安全框架。
Spring Security具有以下優勢:
- 豐富的功能:Spring Security提供了完善的認證機制和方法級的授權功能,可以輕松地擴展以滿足自定義需求。
- 強大的社區支持:與所有Spring項目一樣,Spring Security的真正強大之處在于可以輕松擴展以滿足自定義要求。此外,它擁有一個活躍的社區,提供了豐富的資源和支持。
- 與Spring生態系統的集成:Spring Security與Spring生態系統中的其他組件緊密集成,如Spring MVC、Spring Boot等,使得在構建安全應用程序時更加便捷。
- 高度可定制:Spring Security提供了大量的配置選項和擴展點,可以根據具體需求進行定制。
本篇文章將會介紹常用的配置及相應的擴展點。
2. 實戰案例
2.1 自定義配置
在Spring Security5.7之前版本通過繼承WebSecurityConfigurerAdapter類
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
5.7之后版本
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
// ...
}
在這里每定義一個SecurityFilterChain所注入的HttpSecurity都是唯一的實例對象。
后續所有的配置都是基于Spring Security5.7.8版本
2.2 自定義驗證器
@Component
public class MemeryAuthticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication ;
Object principal = token.getPrincipal() ;
Object credentials = token.getCredentials() ;
User user = users.get(principal) ;
// notNull(user, "用戶名或密碼錯誤") ;
if (user == null) {
return null ;
}
if (!user.getPassword().equals(credentials)) {
throw new RuntimeException("密碼錯誤") ;
}
return new UsernamePasswordAuthenticationToken(principal, credentials, user.getAuthorities()) ;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication) ;
}
}
通過上面自定義認證器可以實現自己的驗證邏輯。
2.2 自定義UserDetailsService
通過自定義UserDetailsService也可以實現對應的邏輯,只不過這種方式你還需要提供一個PasswordEncoder
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return users.get(username) ;
}
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.equals(encodedPassword) ;
}
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString() ;
}
};
}
2.3 攔截指定路徑的請求
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable() ;
// 該過濾器鏈,只匹配/api/**路徑的請求
http.requestMatcher(new AntPathRequestMatcher("/api/**")) ;
// 也可以這樣配置多個
// http.requestMatchers().antMatchers("/api/**", "/admin/**") ;
// ...
DefaultSecurityFilterChain chain = http.build();
return chain ;
}
2.4 攔截指定路徑及權限
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable() ;
http.authorizeHttpRequests().requestMatchers(new AntPathRequestMatcher("/api/save")).hasAnyRole("C") ;
http.authorizeHttpRequests().requestMatchers(new AntPathRequestMatcher("/api/find")).hasAuthority("ROLE_U") ;
DefaultSecurityFilterChain chain = http.build();
return chain ;
}
2.5 自定義授權決定
http.authorizeHttpRequests(registry -> {
registry.antMatchers("/api/{id}").access(new AuthorizationManager<RequestAuthorizationContext>() {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication,
RequestAuthorizationContext object) {
Map<String, String> variables = object.getVariables() ;
// 返回的路徑是/api/666則進行攔截并且指定具有'D'的權限
return new AuthorityAuthorizationDecision(variables.get("id").equals("666"), Arrays.asList(new SimpleGrantedAuthority("D"))) ;
}
}) ;
}) ;
2.6 自定義異常處理
http.exceptionHandling(customizer -> {
customizer.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
Map<String, Object> errors = new HashMap<>() ;
response.setContentType("application/json;charset=utf-8") ;
errors.put("code", -1) ;
errors.put("status", response.getStatus()) ;
errors.put("message", accessDeniedException.getMessage()) ;
errors.put("details", ExceptionUtils.getMessage(accessDeniedException)) ;
response.getWriter().println(new ObjectMapper().writeValueAsString(errors)) ;
}
}) ;
}) ;
2.7 自定義角色繼承
@Bean
public RoleHierarchy hierarchyVoter() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
// ADMIN自動擁有MANAGER的權限
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_MANAGER");
return hierarchy ;
}
2.8 自定義退出登錄邏輯
http.logout().logoutUrl("/logout").addLogoutHandler(new LogoutHandler() {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
System.out.println("退出登錄") ;
}
}).logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter() ;
out.println("<h2>退出登錄成功</h2>") ;
out.close() ;
}
}) ;
2.9 自定義登錄失敗邏輯
http
.formLogin()
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception
response.setContentType("application/json;charset=UTF-8") ;
PrintWriter out = response.getWriter() ;
out.println("{\"code\": -1, \"message\": \"" + getRootCause(exception).getMessage() + "\"}") ;
out.close();
}
});
2.10 自定義過濾器
@Bean
public PackAuthenticationFilter packAuthenticationFilter() {
return new PackAuthenticationFilter() ;
}
// 添加自定義過濾器到Security Filter Chain中
http.addFilterBefore(packAuthenticationFilter(), RequestCacheAwareFilter.class) ;
2.11 配置多個過濾器鏈
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
// 攔截/api/**
http.requestMatcher(new AntPathRequestMatcher("/api/**")) ;
}
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
// 攔截/admin/**
http.requestMatcher(new AntPathRequestMatcher("/admin/**")) ;
}
2.12 開啟全局方法攔截
@Configuration
@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {}
// 使用
@GetMapping("/find")
@PreAuthorize("hasRole('GUEST')")
public Object find(HttpServletResponse response) throws Exception {
return "find method invoke..." ;
}
2.13 國際化支持
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
// 這里會按照順序查找的
messageSource.addBasenames(
"classpath:org/springframework/security/messages",
"classpath:messages/messages"
) ;
return messageSource ;
}
2.14 防止重復登錄
http.sessionManagement().maximumSessions(1).expiredSessionStrategy(new SessionInformationExpiredStrategy() {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
HttpServletResponse response = event.getResponse() ;
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("{\"code\": -1, \"message\": \"會話已過期,或重復登錄\"}");
out.close();
}
}) ;
注意:你的UserDetails必須重寫equals和hashCode方法
總結:以上是在實際開發中經常會應用到的一些配置及相應功能的使用。Spring Security是一個強大的安全認證框架,它提供了豐富的安全功能來保護您的應用程序。通過本文的基礎配置示例,你可以輕松地開始使用Spring Security來保護你的應用程序并實現身份驗證和授權功能。
完畢!!!