引言
Spring Security 6
是一个功能强大的安全框架,用于在 Spring
应用中实现身份验证(Authentication
)和授权(Authorization
)。它提供了灵活的配置方式,支持多种认证机制(如 表单登录
、OAuth2
、JWT
等)和细粒度的权限控制。
核心概念:
- 认证(Authentication):验证用户身份(如用户名和密码)。
- 授权(Authorization):根据用户角色或权限控制资源访问。
- 过滤器链(SecurityFilterChain):通过过滤器链处理安全相关的请求。
添加依赖
在 pom.xml
配置文件中添加以下依赖:
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.3.0</version>
</dependency>
在 xiaomayi-common/xiaomayi-security
模块中已经引入此依赖,在实际使用时直接引入以下依赖即可:
<!-- 安全认证依赖模块 -->
<dependency>
<groupId>com.xiaomayi</groupId>
<artifactId>xiaomayi-security</artifactId>
</dependency>
鉴权忽略注解
在 xiaomayi-common/xiaomayi-security
模块中自定义鉴权忽略注解:
package com.xiaomayi.security.annotation;
import java.lang.annotation.*;
/**
* <p>
* 请求Security鉴权忽略放行注解
* 备注:
* 1、在IgnoreUrlsProperties忽略属性配置文件中会做统一集中的处理;
* 2、方法有此注解并且值为true时安全认证器会自动忽略直接放行,无需做鉴权认证
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestIgnore {
/**
* 忽略接口的名称,默认使用Controller访问路径+接口的的访问路径
* 使用场景匹配url传参的动态参数匹配 例如: /user/{id},像这种需要配置注解值为 /user/*
*/
boolean value() default true;
}
鉴权忽略设置
在 xiaomayi-modules/xiaomayi-admin
模块资源目录 application-security.yml
配置文件中设置需要忽略放行的URL地址。
# 安全配置
security:
# 放行URL配置
ignored-urls:
- /demo/**
- /websocket/**
- /websocket2/**
- /gitee/**
- /error
- /login
- /register
- /captcha
- /actuator
- /actuator/**
- /admin
- /admin/**
- /instances
- /instances/**
- /druid/**
- /doc.html
- /v3/api-docs/**
- /webjars/**
- /swagger-ui/**
- /swagger-resources/**
- /file/**
- /favicon.ico
- /*.html
- /*.css
- /*.js
鉴权忽略配置 SecurityIgnoreConfig
文件:
package com.xiaomayi.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* <p>
* 读取YML文件服务器资源忽略URL配置类
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Order(-1)
@Data
@Configuration
@ConfigurationProperties(prefix = "security")
public class SecurityIgnoreConfig {
/**
* 服务器资源忽略URL集合
*/
private String[] ignoredUrls;
}
资源服务器对外暴露放行URL配置类:
package com.xiaomayi.security.config;
import cn.hutool.core.util.ReUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.security.annotation.RequestIgnore;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.*;
import java.util.regex.Pattern;
/**
* <p>
* 资源服务器对外暴露放行URL配置类
* 此处处理分为两块:
* 1、设置需要对外放行的URL地址和相关资源路径地址,如:/login,/captcha等等
* 2、设置需要对外放行的方法,这个后续可以动态读取自定义注解实现,如:@RequestIgnore等
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Order(2)
@Import(SecurityIgnoreConfig.class)
@Configuration
public class IgnoreUrlsProperties implements InitializingBean {
@Autowired
private SecurityIgnoreConfig securityIgnoreConfig;
/**
* 定义过滤匹配正则
*/
private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
/**
* 定义对外暴露URL地址列表
*/
@Getter
@Setter
private static List<String> ignoreUrls = new ArrayList<>();
/**
* 继承重写属性设置
*
* @throws Exception 异常处理
*/
@Override
public void afterPropertiesSet() throws Exception {
// 获取配置文件中的忽略对外暴露的URL集合
String[] urlList = securityIgnoreConfig.getIgnoredUrls();
if (StringUtils.isNotEmpty(urlList)) {
// 配置文件中对外暴露的URL地址统一加入列表
ignoreUrls.addAll(Arrays.asList(urlList));
}
// 获取Bean对象
RequestMappingHandlerMapping mapping = SpringUtil.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
// 获取执行句柄方法Map集合
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
// 遍历数据源,忽略的方法加入URL集合
map.keySet().forEach(info -> {
HandlerMethod handlerMethod = map.get(info);
// 此处获取捕捉《方法》上@RequestIgnore,并将访问地址替换为 * 进行放行操作
RequestIgnore method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), RequestIgnore.class);
Optional.ofNullable(method)
.ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition())
.getPatternValues()
.forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*"))));
// 此处获取捕捉《类》上@RequestIgnore,并将访问地址替换为 * 进行放行操作
RequestIgnore controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), RequestIgnore.class);
Optional.ofNullable(controller)
.ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition())
.getPatternValues()
.forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*"))));
});
}
}
配置安全规则
创建一个 SecurityConfig
配置类,继承 SecurityFilterChain
,并配置安全规则。
package com.xiaomayi.security.config;
import com.xiaomayi.security.filter.JwtAuthenticationTokenFilter;
import com.xiaomayi.security.filter.ResponseFilter;
import com.xiaomayi.security.handler.*;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.CorsFilter;
import java.util.stream.Collectors;
/**
* <p>
* 安全认证配置类
* 特别提醒:新版本中WebSecurityConfigurerAdapter已启用,目前采用新的写法
* 注意:SpringSecurity 6 没有了需要继承类这个做法,但是需要配置注解
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableGlobalAuthentication
@AllArgsConstructor
public class SecurityConfig {
private final ResponseFilter responseFilter;
/**
* 用户认证
*/
private final UserDetailsService userDetailsService;
/**
* 自定义认证过滤器
*/
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 自定义认证失败处理
*/
private final AuthenticationEntryPointImpl authenticationEntryPoint;
/**
* 自定义认证成功处理器
*/
private final AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
/**
* 自定义认证失败处理器
*/
private final AuthenticationFailureHandlerImpl authenticationFailureHandler;
/**
* 自定义退出处理
*/
private final LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* 自定义正在退出处理
*/
private final LogoutHandlerImpl logoutHandler;
/**
* 自定义访问权限不足处理
*/
private final AccessDeniedHandlerImpl accessDeniedHandler;
/**
* 跨域过滤器
*/
private final CorsFilter corsFilter;
/**
* 取消ROLE_前缀
*/
@Bean
public GrantedAuthorityDefaults grantedAuthorityDefaults() {
// Remove the ROLE_ prefix
return new GrantedAuthorityDefaults("");
}
/**
* 设置密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 认证管理器,登录的时候参数会传给 authenticationManager
*
* @param config 认证配置
* @return 返回结果
* @throws Exception 异常处理
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* 设置默认认证提供
* 调用loadUserByUsername获得UserDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
*
* @return 返回结果
*/
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
// DaoAuthenticationProvider 从自定义的 userDetailsService.loadUserByUsername 方法获取UserDetails
authenticationProvider.setUserDetailsService(userDetailsService);
// 设置密码编辑器
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
/**
* 安全认证配置
*
* @param http 安全请求
* @throws Exception 异常处理
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 忽略数据源转对象数组
AntPathRequestMatcher[] requestMatchers = IgnoreUrlsProperties.getIgnoreUrls()
.stream()
.map(AntPathRequestMatcher::new)
.collect(Collectors.toSet())
.toArray(new AntPathRequestMatcher[]{});
// 关闭CSRF,前后端分离不需要CSRF保护
http.csrf(AbstractHttpConfigurer::disable)
// 请求认证
.authorizeHttpRequests(request -> {
// 第一部分:从YML配置文件读取,忽略公开访问URL集合
request.requestMatchers(requestMatchers).permitAll();
// 第二部分:直接此处写死放行配置,如登录、错误页面等,强制默认放行
// 允许所有OPTIONS请求
request.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允许直接访问授权登录接口
.requestMatchers(HttpMethod.POST, "/login","/register").permitAll()
// 允许 SpringMVC 的默认错误地址匿名访问
.requestMatchers("/error").permitAll()
// 其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailsImpl对象中默认设置“ROLE_USER”
.requestMatchers("/**").hasRole("ROLE_ADMIN");
// 除上述放行URL外,其他全部请求都必须认证授权
request.anyRequest().authenticated();
})
// 解决“X-Frame-Options“指令设为“deny“,地址无法访问问题
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
// 添加Web过滤器之前添加过滤器统一输出格式
.addFilterBefore(responseFilter, WebAsyncManagerIntegrationFilter.class) // 在 Web...过滤器之前添加过滤器
// 前后端分离是是无状态的,因此设置会话管理策略为无状态,即不创建和使用会话;基于token,所以不需要session,此处直接禁用
.sessionManagement(request -> {
request.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
})
.exceptionHandling(request -> {
// 添加无权限访问处理
request.accessDeniedHandler(accessDeniedHandler);
}).exceptionHandling(request -> {
// 添加未登录处理,如认证/授权异常
request.authenticationEntryPoint(authenticationEntryPoint);
})
// 添加退出处理器
.logout(request -> {
// 设置退出URL地址
request.logoutUrl("/logout")
// 退出成功处理器
.logoutSuccessHandler(logoutSuccessHandler)
// 正在退出处理器
.addLogoutHandler(logoutHandler);
})
.authenticationProvider(authenticationProvider())
// 添加自定义JWT验证过滤器,替代UsernamePasswordAuthenticationFilter
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
// 添加CROS跨域过滤器
.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
.addFilterBefore(corsFilter, LogoutFilter.class);
// 返回结果
return http.build();
}
}
温馨提示
上述配置规则中,/login
和 /register
已忽略放行操作,即可此网络请求可以直接访问,无需鉴权认证。
鉴权认证核心
在 xiaomayi-modules/xiaomayi-admin
模块下自定义实现 UserDetailsServiceImpl
继承了 UserDetailsService
实现用户鉴权信息。
package com.xiaomayi.admin.service.impl;
import com.xiaomayi.core.exception.user.UserAccountBlockedException;
import com.xiaomayi.core.exception.user.UserAccountDeleteException;
import com.xiaomayi.core.exception.user.UserNotExistException;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.security.security.LoginUser;
import com.xiaomayi.security.vo.UserVO;
import com.xiaomayi.system.enums.UserStatusEnum;
import com.xiaomayi.system.mapper.MenuMapper;
import com.xiaomayi.system.service.UserService;
import com.xiaomayi.system.vo.user.UserInfoVO;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 安全认证登录实现
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Slf4j
@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final PasswordService passwordService;
private final UserService userService;
private final MenuMapper menuMapper;
/**
* 根据用户名查询用户
*
* @param username 用户名
* @return 返回结果
* @throws UsernameNotFoundException 异常处理
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户
UserInfoVO user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new UserNotExistException();
} else if (!user.getDelFlag().equals(0)) {
log.info("登录用户:{} 已被删除.", username);
throw new UserAccountDeleteException();
} else if (!UserStatusEnum.OK.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被锁定禁用.", username);
throw new UserAccountBlockedException();
}
// 检测登录密码
passwordService.check(user.getSalt(), user.getPassword());
// 查询用户的权限列表
List<String> permissions = new ArrayList<>();
// 用户判断
if (user.getId().equals(1)) {
// 超级管理员,全部放行
permissions.add("*:*:*");
} else {
// 非超管人员
permissions = menuMapper.getPermissions(user.getId());
}
// 查询用户角色权限列表
List<String> roles = new ArrayList<>();
roles.add("ROLE_ADMIN");
// 实例化登录用户VO
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
// 返回登录用户
return new LoginUser(userVO, permissions, roles);
}
}
鉴权核心
基于 Spring Security 6
鉴权认证方案,核心是通过 UserDetailsService
和 PasswordEncoder
实现用户身份验证。
用户登录认证
如何验证认证规则?
已集成 Spring Security 6
的鉴权认证体系,那么如何使用或者验证安全认证规则?
现有规则是除了 /login
、/register
以及忽略配置中的放行URL地址外,任何其他网络请求需要经过鉴权认证体系的过滤,包括权限节点、角色权限等权限体系。
用户发起登录请求:
1. 接口地址:http://127.0.0.1:8081/api/login
2. 请求方式:POST
3. 请求参数:
{
"username": "admin2",
"password": "123456",
"code": "618",
"key": "554d07ed-32a7-44a3-b255-ac1068c33d6b"
}
4. 结果输出:
{
"code": 0,
"msg": "操作成功",
"data": {
"access_token": "eyJuYW1lIjoi5bCP6JqC6JqBIiwiYWxnIjoiSFMyNTYifQ.eyJqdGkiOiIzM2M2ZjczOTk2OTM0N2Q5YTg4ZGFiYzNlMTJmMWUwNSIsImlzcyI6IuWwj-iaguiagSIsImlhdCI6MTc0MTU4OTk1Mywia2V5IjoidmFsdWUiLCJ1c2VyS2V5IjoidXNlclZhbHVlIiwiZXhwIjoxNzQxNjc2MzUzfQ._KCq3hNCElViowTK87L3jxn-qUi3aGJUdF3j-FZ88bk"
},
"ok": true
}
用户登录响应结果:
- 用户信息:
{
"avatar": "/user/20241016/1729046126088.png",
"deptId": 7,
"deptName": "研发事业部",
"email": "18000000001@163.com",
"gender": 1,
"id": 1,
"mobile": "18000000001",
"password": "$2a$10$fu5OfJ0FcEb9ft0FMc82W.bRzSPRAGAbGk5lc/x5jcit2p2ogfIYq",
"realname": "管理员",
"status": 1,
"tenantId": 1,
"type": 0,
"username": "admin"
}
- 权限节点
[
"sys:user:page",
"sys:user:list",
"sys:user:detail",
"sys:user:add",
"sys:user:update",
"sys:user:status",
"sys:user:delete",
"sys:user:batchDelete",
"sys:user:resetPwd",
"sys:user:import",
"sys:user:export",
"sys:role:page",
"sys:role:list",
"sys:role:detail",
"sys:role:add",
"sys:role:update",
"sys:role:delete",
"sys:role:batchDelete",
"sys:role:getPermission",
"sys:role:savePermission",
"sys:menu:page",
"sys:menu:list",
"sys:menu:detail",
"sys:menu:add",
"sys:menu:update",
"sys:menu:delete",
"sys:menu:batchDelete",
"sys:menu:addz",
"sys:menu:expand",
"sys:menu:collapse",
"sys:dept:page",
"sys:dept:list",
"sys:dept:detail",
"sys:dept:add",
"sys:dept:update",
"sys:dept:delete",
"sys:dept:batchDelete",
"sys:dept:addz",
"sys:dept:expand",
"sys:dept:collapse",
"sys:level:page",
"sys:level:list",
"sys:level:detail",
"sys:level:add",
"sys:level:update",
"sys:level:status",
"sys:level:delete",
"sys:level:batchDelete",
"sys:level:import",
"sys:level:export",
"sys:position:page",
"sys:position:list",
"sys:position:detail",
"sys:position:add",
"sys:position:update",
"sys:position:status",
"sys:position:delete",
"sys:position:batchDelete",
"sys:tenant:page",
"sys:tenant:list",
"sys:tenant:detail",
"sys:tenant:add",
"sys:tenant:update",
"sys:tenant:status",
"sys:tenant:delete",
"sys:tenant:batchDelete",
"sys:tenant:account",
"sys:loginLog:page",
"sys:loginLog:list",
"sys:loginLog:detail",
"sys:loginLog:add",
"sys:loginLog:update",
"sys:loginLog:delete",
"sys:loginLog:batchDelete",
"sys:operLog:page",
"sys:operLog:list",
"sys:operLog:detail",
"sys:operLog:add",
"sys:operLog:update",
"sys:operLog:delete",
"sys:operLog:batchDelete"
]
- 鉴权角色
["ROLE_ADMIN"]
权限节点控制
用户登录时会查询用户信息(账号信息)、权限节点集合、权限角色集合等完成鉴权认证和权限授予,在后续用户访问权限方法时会进行鉴权认证和节点权限控制。
总结
1. 核心功能:
认证:通过 UserDetailsService 和 PasswordEncoder 实现用户身份验证。
授权:通过 authorizeHttpRequests 配置细粒度的权限控制。
过滤器链:通过 SecurityFilterChain 自定义安全规则。
2. 优点:
高度可定制化,支持多种认证和授权方式。
与 Spring Boot 无缝集成,配置简单。
3. 适用场景:
需要身份验证和授权的 Web 应用。
需要支持 OAuth2、JWT 等现代化安全协议的应用。
4. 注意事项:
生产环境中不建议使用 InMemoryUserDetailsManager,应集成数据库或 LDAP 等持久化存储。
密码应使用加密存储(如 BCrypt),避免明文存储。
权限配置应遵循最小权限原则,避免过度授权。
通过以上步骤,你可以在项目中集成 Spring Security 6
,实现强大的鉴权和认证功能。