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 会自动生效,请求将跳转登录页面
![202301012053083832.png][]
默认用户名、密码和权限可在 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 表示配置类和数据源的权限都满足才通过