SpringSecurity配置 1

spring security整合步骤

过滤器链

SpringSecurity的本质就是一个过滤器链,内部包含了提供各种功能的过滤器,基本案例中的过滤器链如下图所示:

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。基本案例的认证工作主要有它负责

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException

FilterSecurityInterceptor:负责权限校验的过滤器

认证流程

认证流程中的核心类

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息

AuthenticationManager接口:定义了认证Authentication的方法

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中

UsernamePasswordAuthenticationFilter实现类:实现了我们最常用的基于用户名和密码的认证逻辑,封装Authentication对象

DaoAuthenticationProvider实现类:是AuthenticationManager中管理的其中一个Provider,因为是要访问数据库,所以叫Dao

1.自定义 Spring Security 配置

要更好地控制 Spring Security 的行为,你可以创建一个自定义的 SecurityConfig 类,继承自 WebSecurityConfigurerAdapter。通过覆盖方法,您可以配置认证、授权规则、自定义登录页面、注销等。要加上 @EnableWebSecurity 注解和 @Configuration

java 复制代码
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/admin/**").authenticated() // 认证所有以 /admin 为前缀的 URL 资源
                .anyRequest().permitAll().and() // 其他都需要放行,无需认证
            .formLogin().and() // 使用表单登录
            .httpBasic(); // 使用 HTTP Basic 认证
    }

可以在 .yaml 文件中配置账号密码 spring.security.user.name|password

2.封装 JwtTokenHelper 工具类, 封装所有 JWT 相关的功能

java 复制代码
@Component
public class JwtTokenHelper implements InitializingBean {

    /**
     * 签发人
     */
    @Value("${jwt.issuer}")
    private String issuer;
    /**
     * 秘钥
     */
    private Key key;

    /**
     * JWT 解析
     */
    private JwtParser jwtParser;

    /**
     * 解码配置文件中配置的 Base 64 编码 key 为秘钥
     * @param base64Key
     */
    @Value("${jwt.secret}")
    public void setBase64Key(String base64Key) {
        key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(base64Key));
    }


    /**
     * 初始化 JwtParser
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        // 考虑到不同服务器之间可能存在时钟偏移,setAllowedClockSkewSeconds 用于设置能够容忍的最大的时钟误差
        jwtParser = Jwts.parserBuilder().requireIssuer(issuer)
                .setSigningKey(key).setAllowedClockSkewSeconds(10)
                .build();
    }

    /**
     * 生成 Token
     * @param username
     * @return
     */
    public String generateToken(String username) {
        LocalDateTime now = LocalDateTime.now();
        // Token 一个小时后失效
        LocalDateTime expireTime = now.plusHours(1);

        return Jwts.builder().setSubject(username)
                .setIssuer(issuer)
                .setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
                .setExpiration(Date.from(expireTime.atZone(ZoneId.systemDefault()).toInstant()))
                .signWith(key)
                .compact();
    }

    /**
     * 解析 Token
     * @param token
     * @return
     */
    public Jws<Claims> parseToken(String token) {
        try {
            return jwtParser.parseClaimsJws(token);
        } catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
            throw new BadCredentialsException("Token 不可用", e);
        } catch (ExpiredJwtException e) {
            throw new CredentialsExpiredException("Token 失效", e);
        }
    }

    /**
     * 生成一个 Base64 的安全秘钥
     * @return
     */
    private static String generateBase64Key() {
        // 生成安全秘钥
        Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);

        // 将密钥进行 Base64 编码
        String base64Key = Base64.getEncoder().encodeToString(secretKey.getEncoded());

        return base64Key;
    }

    public static void main(String[] args) {
        String key = generateBase64Key();
        System.out.println("key: " + key);
    }
}

这里 afterPropertiesSet() 是 Spring 提供的生命周期回调方法,用于在 Bean 属性注入后执行初始化逻辑。这里设置签发人和密钥,为分布式环境可以设置不同的签发人和密钥,这里的密钥在setBase64Key中会生成然后给 jwtParser, 在后面 parseToken 方法中会自动校验,generateTokenafterPropertiesSet() 都会设置签发人,一个是产生,一个事检验。

.yaml 文件中创建好 相应的签发人和密钥

yaml 复制代码
jwt:
  # 签发人
  issuer: quanxiaoha
  # 秘钥
  secret: jElxcSUj38+Bnh73T68lNs0DfBSit6U3whQlcGO2XwnI+Bo3g4xsiCIPg8PV/L0fQMis08iupNwhe2PzYLB9Xg==

3.PasswordEncoder 密码加密

用于存储加密的密码,数据库的数据安全,一般网站都会忘记密码重新设置,而不是找回原来密码

java 复制代码
@Configuration
public class PasswordEncoderConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
	    // BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入"盐",增加密码的安全性。
        return new BCryptPasswordEncoder();
    }

    public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        System.out.println(encoder.encode("quanxiaoha"));
    }
}

初始化了一个 PasswordEncoder 接口的具体实现类 BCryptPasswordEncoderBCryptPasswordEncoder 是 Spring Security 提供的密码加密器的一种实现,使用 BCrypt 算法对密码进行加密。BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入"盐",增加密码的安全性。

4. 实现 UserDetailsService:Spring Security 用户详情服务

UserDetailsService 是 Spring Security 提供的接口,用于从应用程序的数据源(如数据库、LDAP、内存等)中加载用户信息。它是一个用于将用户详情加载到 Spring Security 的中心机制。UserDetailsService 主要负责两项工作:

加载用户信息: 从数据源中加载用户的用户名、密码和角色等信息。

创建 UserDetails 对象: 根据加载的用户信息,创建一个 Spring Security 所需的 UserDetails 对象,包含用户名、密码、角色和权限等。

这里要重写 loadUserByUsername 方法,实验拿到我们自己的数据

java 复制代码
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库中查询
        // ...

		// 暂时先写死,密码为 quanxiaoha, 这里填写的密文,数据库中也是存储此种格式
		// authorities 用于指定角色,这里写死为 ADMIN 管理员
        return User.withUsername("quanxiaoha")
                .password("$2a$10$n7RJ1q.RnXx5M3O6B0i0he04fZOPjIJpyWcKuicW1bFyFHWhlGose")
                .authorities("ADMIN")
                .build();
    }
}

5. 自定义认证过滤器

接下来,我们自定义一个用于认证的过滤器,新建 /filter 包,并创建 JwtAuthenticationFilter 过滤器,代码如下:

java 复制代码
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {


    /**
     * 指定用户登录的访问地址
     */
    public JwtAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        // 解析提交的 JSON 数据
        JsonNode jsonNode = mapper.readTree(request.getInputStream());
        JsonNode usernameNode = jsonNode.get("username");
        JsonNode passwordNode =  jsonNode.get("password");

        // 判断用户名、密码是否为空
        if (Objects.isNull(usernameNode) || Objects.isNull(passwordNode)
            || StringUtils.isBlank(usernameNode.textValue()) || StringUtils.isBlank(passwordNode.textValue())) {
            throw new UsernameOrPasswordNullException("用户名或密码不能为空");
        }

        String username = usernameNode.textValue();
        String password = passwordNode.textValue();

        // 将用户名、密码封装到 Token 中
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
                = new UsernamePasswordAuthenticationToken(username, password);
        return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
    }
}

此过滤器继承了AbstractAuthenticationProcessingFilter,用于处理 JWT(JSON Web Token)的用户身份验证过程。

这里的认证流程是:调用了父类 AbstractAuthenticationProcessingFilter 的构造函数,通过 AntPathRequestMatcher 指定了处理用户登录的访问地址。这意味着当请求路径匹配 /login 并且请求方法为 POST 时,该过滤器将被触发。然后从request对象中拿到用户名密码判断合法性,通过Authentication的实现子类UsernamePasswordAuthenticatonToken 封装Authentication对象,这里只有用户名密码,还没有权限,再进行AuthenticationManagerauthenticate方法进行认证,前者是从父类中继承过来的,它提供了一群Provider,所以调用的就是一群providerauthenticate方法进行认证。

6. 自定义用户名或密码不能为空异常

上面过滤器代码中,有个动作是校验用户名、密码是否为空,为空则抛出 UsernameOrPasswordNullException 异常,此类是自定义的得来的。新建包 /exception, 在此包中创建该类:

java 复制代码
public class UsernameOrPasswordNullException extends AuthenticationException {
    public UsernameOrPasswordNullException(String msg, Throwable cause) {
        super(msg, cause);
    }

    public UsernameOrPasswordNullException(String msg) {
        super(msg);
    }
}

注意,需继承自 AuthenticationException,只有该类型异常,才能被后续自定义的认证失败处理器捕获到。

7. 自定义认证成功处理器

新建 /handler 包,并创建 RestAuthenticationSuccessHandler 类:

java 复制代码
@Component
@Slf4j
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private JwtTokenHelper jwtTokenHelper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 从 authentication 对象中获取用户的 UserDetails 实例,这里是获取用户的用户名
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();

        // 通过用户名生成 Token
        String username = userDetails.getUsername();
        String token = jwtTokenHelper.generateToken(username);

        // 返回 Token
        LoginRspVO loginRspVO = LoginRspVO.builder().token(token).build();

        ResultUtil.ok(response, Response.success(loginRspVO));
    }
}

此类实现了 Spring Security 的 AuthenticationSuccessHandler 接口,用于处理身份验证成功后的逻辑。首先,从 authentication 对象中获取用户的 UserDetails 实例,这里是主要是获取用户的用户名,然后通过用户名生成 Token 令牌,最后返回数据。这里的getPrincipal:用户的主体信息(通常是 UserDetails 对象,存储用户名、权限等)。
credentials:用户的凭证(如密码,认证成功后通常会被擦除)。
authorities:用户的权限集合(如角色 ROLE_USER)。

7. ResultUtil 返参工具类

为了在过滤器中方便的返回 JSON 参数,我们需要封装一个工具类 ResultUtil, 放置在 /utils 包下,代码如下:

java 复制代码
public class ResultUtil {

    /**
     * 成功响参
     * @param response
     * @param result
     * @throws IOException
     */
    public static void ok(HttpServletResponse response, Response<?> result) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpStatus.OK.value());
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();

        ObjectMapper mapper = new ObjectMapper();
        writer.write(mapper.writeValueAsString(result));
        writer.flush();
        writer.close();
    }

    /**
     * 失败响参
     * @param response
     * @param result
     * @throws IOException
     */
    public static void fail(HttpServletResponse response, Response<?> result) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpStatus.OK.value());
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();

        ObjectMapper mapper = new ObjectMapper();
        writer.write(mapper.writeValueAsString(result));
        writer.flush();
        writer.close();
    }

    /**
     * 失败响参
     * @param response
     * @param status 可指定响应码,如 401 等
     * @param result
     * @throws IOException
     */
    public static void fail(HttpServletResponse response, int status, Response<?> result) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setStatus(status);
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();

        ObjectMapper mapper = new ObjectMapper();
        writer.write(mapper.writeValueAsString(result));
        writer.flush();
        writer.close();
    }
}

8. 自定义认证失败处理器

/handler 包下,创建 RestAuthenticationFailureHandler 认证失败处理器:

java 复制代码
@Component
@Slf4j
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.warn("AuthenticationException: ", exception);
        if (exception instanceof UsernameOrPasswordNullException) {
            // 用户名或密码为空
            ResultUtil.fail(response, Response.fail(exception.getMessage()));
			return;
        } else if (exception instanceof BadCredentialsException) {
            // 用户名或密码错误
            ResultUtil.fail(response, Response.fail(ResponseCodeEnum.USERNAME_OR_PWD_ERROR));
			return;
        }

        // 登录失败
        ResultUtil.fail(response, Response.fail(ResponseCodeEnum.LOGIN_FAIL));
    }
}

通过自定义了一个实现了 Spring Security 的 AuthenticationFailureHandler 接口类,用于在用户身份验证失败后执行一些逻辑。首先,我们打印了异常日志,方便后续定位问题,然后对异常的类型进行判断,通过 ResultUtil 工具类,返回不同的错误信息,如用户名或者密码为空、用户名或密码错误等,若未判断出异常是什么类型,则统一提示为 登录失败。

这里定义两个枚举异常

java 复制代码
LOGIN_FAIL("20000", "登录失败"),
USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"),

9. 自定义 JWT 认证功能配置

完成了以上前置工作后,我们开始配置 JWT 认证相关的配置。在 /config 包下新建 JwtAuthenticationSecurityConfig, 代码如下:

java 复制代码
@Configuration
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler;

    @Autowired
    private RestAuthenticationFailureHandler restAuthenticationFailureHandler;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        // 自定义的用于 JWT 身份验证的过滤器
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
        filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));

        // 设置登录认证对应的处理类(成功处理、失败处理)
        filter.setAuthenticationSuccessHandler(restAuthenticationSuccessHandler);
        filter.setAuthenticationFailureHandler(restAuthenticationFailureHandler);

        // 直接使用 DaoAuthenticationProvider, 它是 Spring Security 提供的默认的身份验证提供者之一
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 设置 userDetailService,用于获取用户的详细信息
        provider.setUserDetailsService(userDetailsService);
        // 设置加密算法
        provider.setPasswordEncoder(passwordEncoder);
        httpSecurity.authenticationProvider(provider);
        // 将这个过滤器添加到 UsernamePasswordAuthenticationFilter 之前执行
        httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

上述代码是一个 Spring Security 配置类,用于配置 JWT(JSON Web Token)的身份验证机制。它继承了 Spring Security 的 SecurityConfigurerAdapter 类,用于在 Spring Security 配置中添加自定义的认证过滤器和提供者。通过重写 configure() 方法,我们将之前写好过滤器、认证成功、失败处理器,以及加密算法整合到了 httpSecurity 中。

10. 应用 JWT 认证功能配置

接下来,我们编辑 Spring Security 配置 WebSecurityConfig 类,修改内容如下:

java 复制代码
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(). // 禁用 csrf
                formLogin().disable() // 禁用表单登录
                .apply(jwtAuthenticationSecurityConfig) // 设置用户登录认证相关配置, 用我们上面自己认证好的规则
             .and()
                .authorizeHttpRequests()
                .mvcMatchers("/admin/**").authenticated() // 认证所有以 /admin 为前缀的 URL 资源
                .anyRequest().permitAll() // 其他都需要放行,无需认证
             .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 前后端分离,无需创建会话
    }
}

上述代码中,在 configure() 方法中,首先禁用了 CSRF(Cross-Site Request Forgery)攻击防护。在前后端分离的情况下,通常不需要启用 CSRF 防护。同时,还禁用了表单登录,并应用了 JWT 相关的配置类 JwtAuthenticationSecurityConfig。最后,配置会话管理这块,将会话策略设置为无状态(STATELESS),适用于前后端分离的情况,无需创建会话。

11. 从数据库中查询用户信息

前面我们根据用户名查询用户信息这块,是代码中写死的。接下来,我们将其改造为从数据库中查询。首先,我们将 t_user 表中之前用于测试的记录删除干净,并执行如下语句,为用户表添加一条记录,用户名为 "quanxiaoha":

这里我们从数据库获取,编辑 UserDetailServiceImpl 类,改为从数据库中查询:

java 复制代码
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库中查询
        UserDO userDO = userMapper.findByUsername(username);

        // 判断用户是否存在
        if (Objects.isNull(userDO)) {
            throw new UsernameNotFoundException("该用户不存在");
        }

        // authorities 用于指定角色,这里写死为 ADMIN 管理员
        return User.withUsername(userDO.getUsername())
                .password(userDO.getPassword())
                .authorities("ADMIN")
                .build();
    }
}