Skip to content

引言

Spring Security 6 是一个功能强大的安全框架,用于在 Spring 应用中实现身份验证(Authentication)和授权(Authorization)。它提供了灵活的配置方式,支持多种认证机制(如 表单登录OAuth2JWT 等)和细粒度的权限控制。

核心概念:

  • 认证(Authentication):验证用户身份(如用户名和密码)。
  • 授权(Authorization):根据用户角色或权限控制资源访问。
  • 过滤器链(SecurityFilterChain):通过过滤器链处理安全相关的请求。

添加依赖

pom.xml 配置文件中添加以下依赖:

js
<!-- spring security 安全认证 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>3.3.0</version>
</dependency>

xiaomayi-common/xiaomayi-security 模块中已经引入此依赖,在实际使用时直接引入以下依赖即可:

js
<!-- 安全认证依赖模块 -->
<dependency>
    <groupId>com.xiaomayi</groupId>
    <artifactId>xiaomayi-security</artifactId>
</dependency>

鉴权忽略注解

xiaomayi-common/xiaomayi-security 模块中自定义鉴权忽略注解:

js
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地址。

js
# 安全配置
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 文件:

js
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配置类:

js
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,并配置安全规则。

js
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 实现用户鉴权信息。

js
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 鉴权认证方案,核心是通过 UserDetailsServicePasswordEncoder 实现用户身份验证。

用户登录认证

如何验证认证规则?

已集成 Spring Security 6 的鉴权认证体系,那么如何使用或者验证安全认证规则?

现有规则是除了 /login/register 以及忽略配置中的放行URL地址外,任何其他网络请求需要经过鉴权认证体系的过滤,包括权限节点、角色权限等权限体系。

用户发起登录请求:

js
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
}

用户登录响应结果:

  1. 用户信息:
js
{
    "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"
}
  1. 权限节点
js
[
    "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"
]
  1. 鉴权角色
js
["ROLE_ADMIN"]

权限节点控制

用户登录时会查询用户信息(账号信息)、权限节点集合、权限角色集合等完成鉴权认证和权限授予,在后续用户访问权限方法时会进行鉴权认证和节点权限控制

总结

1. 核心功能:
    认证:通过 UserDetailsService 和 PasswordEncoder 实现用户身份验证。
    授权:通过 authorizeHttpRequests 配置细粒度的权限控制。
    过滤器链:通过 SecurityFilterChain 自定义安全规则。
2. 优点:
    高度可定制化,支持多种认证和授权方式。
    与 Spring Boot 无缝集成,配置简单。
3. 适用场景:
    需要身份验证和授权的 Web 应用。
    需要支持 OAuth2、JWT 等现代化安全协议的应用。
4. 注意事项:
    生产环境中不建议使用 InMemoryUserDetailsManager,应集成数据库或 LDAP 等持久化存储。
    密码应使用加密存储(如 BCrypt),避免明文存储。
    权限配置应遵循最小权限原则,避免过度授权。

通过以上步骤,你可以在项目中集成 Spring Security 6,实现强大的鉴权和认证功能。

小蚂蚁云团队 · 提供技术支持

小蚂蚁云 新品首发
新品首发,限时特惠,抢购从速! 全场95折
赋能开发者,助理企业发展,提供全方位数据中台解决方案。
获取官方授权