一、鉴权流程
1.1 配置受保护资源
当请求受保护资源时,进入到权限校验流程。通常我们可以通过下述两种方式来配置受保护资源:
- @PreAuthorize("权限"):通过在接口上添加该注解,表示需要有对应的权限才能访问
- eg:你可以使用
@PreAuthorize("hasRole('ROLE_ADMIN')")
来限制只有具有 "ROLE_ADMIN" 角色的用户才能访问该方法或接口
- eg:你可以使用
- authorizeRequests().antMatchers():自定义HttpSecurity,使用
authorizeRequests()
方法来开始配置请求的权限限制,并使用antMatchers()
方法指定要匹配的 URL 或者路径模式。- eg:你可以使用
antMatchers("/admin/**").hasRole("ADMIN")
来限制只有具有 "ADMIN" 角色的用户才能访问以 "/admin/" 开头的路径。
- eg:你可以使用
上面是常用的配置受保护资源的访问权限的两种方式。接下来我们看看eladmin中是如何配置的:
- 使用权限注解
@PreAuthorize
+Spring EL
表达式,避免每个接口都需要给超级管理员放行的重复操作。具体实现如下:
java
@Service(value = "el")
public class ElPermissionConfig {
public Boolean check(String ...permissions){
// 获取当前用户的所有权限
List<String> elPermissions = SecurityUtils.getCurrentUser().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 判断当前用户的所有权限是否包含接口上定义的权限
return elPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(elPermissions::contains);
}
}
使用方式:@PreAuthorize("@el.check('user:list','user:add')")
上述代码中个人理解anyMatch
替换为allMatch
更加合理些?
- anyMatch表达的含义:对于被check的权限列表,只要用户有其中一个就返回true,比如check('user:list','user:add'),那么用户只有要其中一个权限即可访问接口;
- allMatch表达的含义:用户需要有被check的权限列表中所有权限;比如check('user:list','user:add'),那么用户必须两个权限都有才能访问接口。
- 继承
WebSecurityConfigurerAdapter
自定义HttpSecurity,下节详细介绍
1.2 自定义处理AccessDeniedException
java
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
//当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
}
}
注意 : 不是所有AccessDeniedException都会被这个JwtAccessDeniedHandler
处理,比如使用@PreAuthorize校验权限失败时,是不会被这个自定义的Handler处理的。而是在全局异常处理器中处理的。 猜测:@PreAuthorize
注解使用的是 Spring 框架的 AOP 功能,它在方法执行之前进行权限检查,如果权限不足,则会抛出 AccessDeniedException
异常。这个异常会被 Spring 框架的全局异常处理机制捕获,而不会被 Spring Security 捕获。
二、自定义spring security配置
2.1 禁用 CSRF
java
httpSecurity.csrf().disable()
CSRF(Cross-Site Request Forgery)跨站请求伪造是一种常见的网络攻击方式,它利用用户在其他网站上已经登录的身份权限来伪造请求发送到目标网站。为了防止这种攻击,Spring Security 默认启用 CSRF 保护机制,通过生成和验证 CSRF Token 来确保请求的合法性。然而,在某些情况下(例如前后端分离的架构),使用 CSRF Token 可能会导致一些麻烦,比如请求头设置或者跨域请求的处理。因此,如果应用程序没有特定的需求需要使用 CSRF 保护,可以选择禁用 CSRF。
2.2 跨域处理
java
httpSecurity.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
arduino
@Configuration
@EnableWebMvc
public class ConfigurerAdapter implements WebMvcConfigurer {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
将 corsFilter
添加在 UsernamePasswordAuthenticationFilter
前面,确保跨域请求的处理发生在认证之前。这样做的原因是,跨域请求往往需要在请求到达后端之前进行一些处理,例如跨域请求的验证、响应头的设置等。
2.3 允许前端使用iframe标签
java
// 禁用 X-Frame-Options即支持iframe
httpSecurity.headers().frameOptions().disable()
X-Frame-Options 是一个 HTTP 响应头,用于指示浏览器是否允许该页面在 iframe 中加载。通过设置 X-Frame-Options,可以提供一定程度的点击劫持和数据窃取防护。在这段代码中,.headers()
表示开始配置响应头相关的安全策略,.frameOptions()
表示对 frameOptions 进行配置。.disable()
的作用是禁用 frameOptions,即不设置 X-Frame-Options 响应头。这样做的目的是为了允许页面在 iframe 中加载,即取消对 iframe 的限制,spring security默认是禁用iframe的。
Q1:什么是iframe?
A1:iframe(Inline Frame)是 HTML 中的一个标签,用于在网页中嵌入另一个网页或者外部资源。通过使用 iframe,可以将一个网页嵌入到另一个网页的指定位置,形成一个内嵌的框架。
2.4 自定义认证授权过程中的异常处理
java
httpSecurity.exceptionHandling()
//处理认证过程中的异常
.authenticationEntryPoint(authenticationErrorHandler)
//处理授权过程中的异常
.accessDeniedHandler(jwtAccessDeniedHandler)
2.5 关闭HttpSession
java
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
HttpSession简介
HttpSession 是一种用于维护跨多个 HTTP 请求的会话状态的机制。当用户通过浏览器发送第一个请求时,服务器会为该用户创建一个唯一的 HttpSession 对象,并将其关联到该用户的浏览器会话。服务器会为该 HttpSession 分配一个唯一的标识符(称为 Session ID),并在响应中使用 Cookie 或 URL 重写的方式将该 Session ID 发送给客户端。
随后,客户端在每个后续的请求中都会在请求头或请求参数中发送 Session ID,以便服务器能够识别该用户的会话状态。
HttpSession 的工作方式如下:
- 创建会话:当用户首次访问应用程序时,服务器会在内存中创建一个新的 HttpSession 对象,并分配一个唯一的 Session ID。
- 存储数据:开发人员可以使用
setAttribute(String name, Object value)
方法将数据存储在 HttpSession 中。这些数据将在整个会话期间保持不变,并可以在后续的请求中访问。 - 获取数据:开发人员可以使用
getAttribute(String name)
方法从 HttpSession 中获取存储的数据。 - 销毁会话:会话可以通过调用
invalidate()
方法来销毁,或者在会话超时或用户注销时自动销毁。销毁会话后,相关的会话数据将被清除。
需要注意的是,默认情况下,HttpSession 存储在服务器的内存中。这意味着当服务器重新启动或重启时,所有的会话数据都会丢失。为了实现持久化会话存储,可以将 HttpSession 存储在数据库或其他外部存储中。
为什么要关闭HttpSession
eladmin通过jwt+redis来记录用户的登入信息,替代传统的HttpSession。因此告诉 Spring Security 在处理请求时不需要创建 HttpSession,避免服务端不必要的开销。
eladmin使用jwt的方式是我目前看到看到比较正确的姿势,大部分人把jwt等同token。对jwt+redis实现登入的感兴趣的同学可以私信我,或评论区留言,一起交流成长。
2.6 放行权限配置
java
httpSecurity.authorizeRequests()
// 静态资源等等
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/webSocket/**"
).permitAll()
// swagger 文档
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
// 文件
.antMatchers("/avatar/**").permitAll()
.antMatchers("/file/**").permitAll()
// 阿里巴巴 druid
.antMatchers("/druid/**").permitAll()
// 放行OPTIONS请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 自定义匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型
// GET
.antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
// POST
.antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
// PUT
.antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
// PATCH
.antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
// DELETE
.antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
// 所有类型的接口都放行
.antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()
// 所有请求都需要认证
.anyRequest().authenticated()
上述配置实现了对静态资源、Swagger文档等资源的放行。值得注意的是 对自定义匿名访问的url放行操作,eladmin中可以通过@AnonymousAccess
注解来灵活的标注对哪个url放行。详解如下:
@AnonymousAccess实现匿名访问
自定义注解要生效,不管是AOP、还是拦截器还是其它形式,无非两个步骤:
- 定义注解
less
@Inherited
@Documented
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}
- 扫描注解
java
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
private final ApplicationContext applicationContext;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
//获取RequestMappingHandlerMapping
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
//获取所有的 Controller 类中的 @RequestMapping 注解所映射的 URL 请求和对应的方法
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
//扫描使用了@AnonymousAccess注解的URL
Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap);
//给使用了@AnonymousAccess注解的URL放行
httpSecurity.authorizeRequests()
// GET
.antMatchers(HttpMethod.GET,anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
// POST
.antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
.....
}
/**
*
* @param handlerMethodMap:key->RequestMappingInfo 对象表示请求映射的详细信息,包括请求路径、请求方法、请求参数等。通过这个对象,可以获得与处理器方法相关的所有请求映射信息.
* value->HandlerMethod 对象表示实际处理请求的方法,包括方法本身、所属的类、方法参数等。通过这个对象,您可以获取和操作处理请求的方法信息
* @return
*/
private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
Map<String, Set<String>> anonymousUrls = new HashMap<>(8);
Set<String> get = new HashSet<>();
Set<String> post = new HashSet<>();
Set<String> put = new HashSet<>();
Set<String> patch = new HashSet<>();
Set<String> delete = new HashSet<>();
Set<String> all = new HashSet<>();
// 遍历每个HandlerMethod对象,判断是否添加了@AnonymousAccess
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
if (null != anonymousAccess) {
//获取URL对应的Http请求方法,如GET、POST等
List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
switch (Objects.requireNonNull(request)) {
case GET:
get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case POST:
post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
......
}
}
}
//把运行匿名访问的URL统一放到anonymousUrls中
anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
......
return anonymousUrls;
}
}
2.7 添加自定义token过滤器进spring security的过滤器链中
java
//应用自定义的安全配置
httpSecurity.apply(securityConfigurerAdapter());
private TokenConfigurer securityConfigurerAdapter() {
return new TokenConfigurer(tokenProvider, properties, onlineUserService, userCacheManager);
}
java
@RequiredArgsConstructor
public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
private final SecurityProperties properties;
private final OnlineUserService onlineUserService;
private final UserCacheManager userCacheManager;
@Override
public void configure(HttpSecurity http) {
TokenFilter customFilter = new TokenFilter(tokenProvider, properties, onlineUserService, userCacheManager);
//把TokenFilter添加到认证前,如果有token,说明认证过就不用再重新认证了
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}