SpringBoot 集成 Security
Spring Security 介绍
-
Spring Security 是基于 Spring 框架的权限管理框架
-
Spring Security 的前身是 Acegi Security
Acegi Security 以配置繁琐而被诟病,投入 Spring 怀抱后,随着 SpringBoot 的崛起,Spring Security 的易用性得到了极大的提升,经常被用于 SpringBoot 及 SpringCloud 项目
-
Spring Security 的基本功能
- 认证:提供多种常见的认证方式
- 授权:提供基于 URL 的请求授权、支持方法访问授权以及对象访问授权
基本原理
-
Spring Security 是通过一层层 Filter 来处理 web 请求的
在 Filter 组成的链条中,逐步完成认证和授权,发现异常则抛给异常处理器处理
-
过滤器链中的核心概念
-
springSecurityFilterChain
Spring Security 的核心过滤器叫 springSecurityFilterChain,类型是 FilterChainProxy
-
WebSecurity、HttpSecurity
WebSecurity 构建了 FilterChainProxy 对象
HttpSecurity 构建了 FilterChainProxy 中的一个 SecurityFilterChain
-
WebSecurityConfiguration
@EnableWebSecurity 注解,导入了 WebSecurityConfiguration 类
WebSecurityConfiguration 中创建了建造者对象 WebSecurity 和核心过滤器 FilterChainProxy
-
-
Spring Security 常用组件
- Authentication:认证接口,定义了认证对象的数据形式。
- AuthenticationManager:用于校验 Authentication,返回一个认证完成后的
- SecurityContext:上下文对象,用来存储 Authentication
- SecurityContextHolder:用来访问 SecurityContext
- GrantedAuthority:代表权限
- UserDetails:代表用户信息
- UserDetailsService:获取用户信息
简单使用
-
引入 Spring Security 依赖
<!--引入 Spring Security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
引入依赖后,不做任何配置,Spring Security 会自动生效,请求将跳转登录页面
默认用户名、密码和权限可在 application.yaml 中配置
spring: security: user: name: ming password: 123456 roles: admin
-
基于内存的认证
@Configuration @EnableWebSecurity // 开启注解设置权限 @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 配置密码加密器 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 配置认证管理器 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin") .password(passwordEncoder().encode("123")).roles("admin") .and() .withUser("user") .password(passwordEncoder().encode("456")).roles("user"); } // 配置安全策略 @Override protected void configure(HttpSecurity http) throws Exception { // 设置路径及要求的权限,支持 ant 风格路径写法 http.authorizeRequests() // 设置 OPTIONS 尝试请求直接通过 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() .antMatchers("/api/demo/user").hasAnyRole("user", "admin") // 注意使用 hasAnyAuthority 角色需要以 ROLE_ 开头 .antMatchers("/api/demo/admin").hasAnyAuthority("ROLE_admin") .antMatchers("/api/demo/hello").permitAll() .and() // 开启表单登录 .formLogin().permitAll() .and() // 开启注销 .logout().permitAll(); } }
前后端分离
关闭 CSRF 防御和会话管理
CSRF 防御要求表单登录时携带 CSRF Token,前后端分离时不需要开启
会话管理设置为 STATELESS,使用无状态的 JWT 进行鉴权
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭 csrf 防御
http.csrf().disable();
// 关闭会话管理
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// ...
}
自定义登录逻辑
Spring Security 默认使用表单登录,若要支持 JSON 请求,可继承UsernamePasswordAnthenticationFilter
,并使用HttpSecurity
的addFilterAt
替换原有
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 判断是否为 JSON 格式请求
if(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
// ...
} else {
return super.attemptAuthentication(request, response);
}
}
}
通过配置 AuthenticationManagerBuilder,设置自定义的 UserDetailsService
@Autowired
private CustomUserDetailsService customUserDetailsService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
实现 UserDetailsService 的 loadUserByUsername 方法
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 根据 username 查询用户
User user = userMapper.getUserByUsername(s);
if (user == null) {
// ...
}
// 查询角色或权限
List<SimpleGrantedAuthority> authorities = userMapper.listRolesByUsername(s)
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 构造 UserDetails 实例并返回
}
}
自定义登录成功处理器
通过配置 HttpSecurity,设置自定义的 successHandler
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll()
.loginProcessingUrl("/login")
.successHandler(customLoginSuccessHandler)
}
CustomLoginSuccessHandler,以 JSON 形式返回前端,携带生成的 Token
@Component
@RequiredArgsConstructor
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
private final JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
// 构造一个统一返回格式对象
Map<String, Object> res = new HashMap<>();
res.put("code", 200);
res.put("message": "认证成功");
res.put("path": "login");
Object principal = authentication.getPrincipal();
if (principal instanceof User) {
// 根据用户信息,使用 JWT 工具类构建 Token
// ...
// 存到返回内容中
res.put("data", "xxxxxx")
}
// 以 JSON 格式写入 response
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.print(JsonUtil.Obj2Str(res));
writer.flush();
}
}
自定义登录失败处理器
通过配置 HttpSecurity,设置自定义的 failureHandler
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll()
.loginProcessingUrl("/login")
.failureHandler(customLoginFailureHandler)
}
CustomLoginFailureHandler,返回认证失败和失败信息
@Component
public class CustomLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
// 封装的统一返回格式对象
Res<Object> res = Res.of(ResCode.TOKEN_CREATE_FAIL).path("/login");
// 根据异常设置失败信息
if (exception instanceof LockedException) {
res.errorMsg("账户被锁定");
} else if (exception instanceof CredentialsExpiredException) {
res.errorMsg("密码过期");
} else if (exception instanceof AccountExpiredException) {
res.errorMsg("账户过期");
} else if (exception instanceof DisabledException) {
res.errorMsg("账户被禁用");
} else if (exception instanceof BadCredentialsException) {
res.errorMsg("用户名或者密码输入错误");
}
// 封装的 JSON 格式写入 response 工具方法
WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
}
}
自定义未登录处理器
配置 authenticationEntryPoint
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
}
CustomAuthenticationEntryPoint
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// 构造未登录的返回内容
Res<Object> res = Res.of(ResCode.TOKEN_NOT_EXIST)
.path(request.getRequestURI());
WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
}
}
自定义权限不足处理器
配置 accessDeniedHandler
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.accessDeniedHandler(customAccessDeniedHandler);
}
CustomAccessDeniedHandler
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 构造权限不足的返回内容
Res<Object> res = Res.of(ResCode.TOKEN_NO_AUTHORITY)
.path(request.getRequestURI());
WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
}
}
自定义注销成功逻辑
配置 logoutSuccessHandler
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout().permitAll()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler);
}
CustomLogoutSuccessHandler
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// 构造注销成功的返回内容
Res<String> res = Res.ok("注销成功").path("/logout");
WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
}
}
也可以使用 HttpSecurity 的 addLogoutHandler,配置注销的处理逻辑
自定义 JWT 过滤器
添加 JWT 过滤器到过滤器链
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);
}
JwtAuthenticationTokenFilter
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
// 取出 header 中的 token 进行校验
String authHeader = httpServletRequest.getHeader(jwtUtil.getHeader());
if (authHeader != null && !StringUtil.isEmpty(authHeader)) {
String username = jwtUtil.getUsernameFromToken(authHeader);
if (username != null
&& SecurityContextHolder.getContext().getAuthentication() == null) {
// 根据 username 查询用户,可以从缓存、数据库中获取
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 校验
if (jwtUtil.validateToken(authHeader, userDetails)) {
// 构建 authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails,
null,
userDetails.getAuthorities());
// 设置 details,其中包含地址、session 等
authentication.setDetails(new
WebAuthenticationDetails(httpServletRequest));
// 设置 authentication 到上下文对象中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
动态配置 URL 权限
Spring Security 的过滤器链中包含了许多过滤器,其中 FilterSecurityInterceptor 非常重要,完成了主要的鉴权逻辑
beforeInvocation 方法
attemptAuthorization
从源码可以看出,动态配置 URL 权限有两种途径
-
自定义 SecurityMetadataSource,从数据源加载 ConfigAttribute
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private final AntPathMatcher antPathMatcher = new AntPathMatcher(); private final FilterInvocationSecurityMetadataSource superMetadataSource; private final Map<String, String[]> urlRoleMap = new HashMap<>(); public MySecurityMetadataSource( FilterInvocationSecurityMetadataSource metadataSource) { this.superMetadataSource = metadataSource; // 此处可以从数据库加载权限配置 urlRoleMap.put("/api/demo/admin", new String[]{"ROLE_admin"}); urlRoleMap.put("/api/demo/user", new String[]{"ROLE_user", "ROLE_admin"}); } @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { FilterInvocation fi = (FilterInvocation) object; String url = fi.getRequestUrl(); for (Map.Entry<String, String[]> entry : urlRoleMap.entrySet()) { if (antPathMatcher.match(entry.getKey(), url)) { // 生成 ConfigAttribute return SecurityConfig.createList(entry.getValue()); } } // 返回配置类定义的默认权限配置 return superMetadataSource.getAttributes(object); } }
由于 SecurityConfig.createList 返回的是 SecurityConfig 类型的 ConfigAttribute,默认使用的 WebExpressionVoter 投票器用于验证 WebExpressionConfigAttribute 类型,因此还需要配置一个 RoleVoter
WebExpressionConfigAttribute 是指在配置类中通过 HttpSecurity 配置的权限
配置 HttpSecurity
http.authorizeRequests() .anyRequest().authenticated() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { // 设置为自定义的 SecurityMetadataSource object.setSecurityMetadataSource(mySecurityMetadataSource); // AffirmativeBased 是 AccessDecisionManager 的一种 // AffirmativeBased,有一个投票器通过就通过 // UnanimousBased,有一个投票器不通过就不通过,全部弃权也不通过 object.setAccessDecisionManager(new AffirmativeBased( Arrays.asList( new WebExpressionVoter(), new RoleVoter() ))); return object; } }) /** * 如果使用 UnanimousBased * 到达 RoleVoter 的 ConfigAttribute 是从数据库动态获取的,可能有多个 * UnanimousBased 对每个 ConfigAttribute 进行投票,即所有权限都有才算通过 */
-
自定义一个投票器,在投票器中可以获取 URL,动态加载权限,可参考 RoleVoter
public class CustomRoleVoter extends RoleVoter { @Override public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { if (authentication == null) { return ACCESS_DENIED; } List<ConfigAttribute> dbAttributes = new ArrayList<>(); FilterInvocation fi = (FilterInvocation) object; String url = fi.getRequestUrl(); // 根据 url 从数据源获取权限,存到 dbAttributes // ... int result = ACCESS_ABSTAIN; // 获取 authentication 的权限 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 判断 authentication 是否包含权限 for (ConfigAttribute attribute : dbAttributes) { if (attribute.getAttribute() == null) { continue; } if (this.supports(attribute)) { result = ACCESS_DENIED; for (GrantedAuthority authority : authorities) { if (attribute.getAttribute().equals(authority.getAuthority())) { return ACCESS_GRANTED; } } } } return result; } }
配置 HttpSecurity
http.authorizeRequests() .anyRequest().authenticated() .accessDecisionManager(new UnanimousBased( Arrays.asList( new WebExpressionVoter(), new CustomRoleVoter() ))); // 此处使用 UnanimousBased 表示配置类和数据源的权限都满足才通过
转载自:https://juejin.cn/post/7037014454067789860