相关推荐recommended
SpringBoot3 + SpringSecurity6 前后端分离
作者:mmseoamin日期:2024-01-22

目录

导入依赖

添加配置类

实现UserDetailsService

实现UserDetails

JWT工具类 

登录接口

自定义token过滤器

SpringContextUtils工具类

添加自定义token验证过滤器

自定义用户未登录的处理

自定义用户权限不足的处理

添加自定义处理器

静态资源放行

总结


        网上能找到的SpringBoot项目一般都是SpringBoot2 + SpringSecurity5,甚至是SSM的项目。这些老版本的教程很多已经不适用了,对于现在大部分的初学者来说,学了可能也是经典白雪。我还是不愿学那些老版本的东西,所以自己摸索了一下新版的SpringBoot项目应该怎么写。学习的过程也是非常折磨人的,看了很多的教程才知道个大概。

  • 导入依赖

    SpringSecurity依赖

    
    
        org.springframework.boot
        spring-boot-starter-security
    

    JWT依赖

    
    
        io.jsonwebtoken
        jjwt
        0.9.1
    
    • 添加配置类

      对Security进行配置,Security中很多的默认配置都可以用自定义的替换。

      import lombok.RequiredArgsConstructor;
      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.AuthenticationProvider;
      import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
      import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
      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.core.userdetails.UserDetailsService;
      import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
      import org.springframework.security.web.SecurityFilterChain;
      /**
       * @Description: SpringSecurity配置类
       * @Author: 翰戈.summer
       * @Date: 2023/11/17
       * @Param:
       * @Return:
       */
      @Configuration
      @EnableWebSecurity
      @RequiredArgsConstructor
      public class SecurityConfig {
          private final UserDetailsService userDetailsService;
          /**
           * 加载用户信息
           */
          @Bean
          public UserDetailsService userDetailsService() {
              return userDetailsService;
          }
          /**
           * 密码编码器
           */
          @Bean
          public BCryptPasswordEncoder passwordEncoder() {
              return new BCryptPasswordEncoder();
          }
          /**
           * 身份验证管理器
           */
          @Bean
          public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
              return configuration.getAuthenticationManager();
          }
          /**
           * 处理身份验证
           */
          @Bean
          public AuthenticationProvider authenticationProvider() {
              DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
              daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
              daoAuthenticationProvider.setUserDetailsService(userDetailsService);
              return daoAuthenticationProvider;
          }
          /**
           * @Description: 配置SecurityFilterChain过滤器链
           * @Author: 翰戈.summer
           * @Date: 2023/11/17
           * @Param: HttpSecurity
           * @Return: SecurityFilterChain
           */
          @Bean
          public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
              httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                      .requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() //登录放行
                      .anyRequest().authenticated()
              );
              httpSecurity.authenticationProvider(authenticationProvider());
              //禁用登录页面
              httpSecurity.formLogin(AbstractHttpConfigurer::disable);
              //禁用登出页面
              httpSecurity.logout(AbstractHttpConfigurer::disable);
              //禁用session
              httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
              //禁用httpBasic
              httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
              //禁用csrf保护
              httpSecurity.csrf(AbstractHttpConfigurer::disable);
              return httpSecurity.build();
          }
      }
      • 实现UserDetailsService

        其中UserMapper、AuthorityMapper需要自己创建,不是重点。这两个Mapper的作用是获取用户信息(用户名、密码、用户权限),封装到User中返回给Security。

        import com.demo.mapper.AuthorityMapper;
        import com.demo.mapper.UserMapper;
        import com.demo.pojo.AuthorityEntity;
        import com.demo.pojo.UserEntity;
        import lombok.RequiredArgsConstructor;
        import org.springframework.security.core.authority.AuthorityUtils;
        import org.springframework.security.core.userdetails.User;
        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.List;
        import java.util.StringJoiner;
        /**
         * @Description: 用户登录
         * @Author: 翰戈.summer
         * @Date: 2023/11/16
         * @Param:
         * @Return:
         */
        @Service
        @RequiredArgsConstructor
        public class UserLoginDetailsServiceImpl implements UserDetailsService {
            private final UserMapper userMapper;
            private final AuthorityMapper authorityMapper;
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                UserEntity userEntity = userMapper.selectUserByUsername(username);
                List authorities = authorityMapper.selectAuthorityByUsername(username);
                StringJoiner stringJoiner = new StringJoiner(",", "", "");
                authorities.forEach(authority -> stringJoiner.add(authority.getAuthorityName()));
                return new User(userEntity.getUsername(), userEntity.getPassword(),
                        AuthorityUtils.commaSeparatedStringToAuthorityList(stringJoiner.toString())
                );
            }
        }
        • 实现UserDetails

          登录操作会用到UserDetails,用于获取用户名和权限。

          import lombok.AllArgsConstructor;
          import lombok.NoArgsConstructor;
          import org.springframework.security.core.GrantedAuthority;
          import org.springframework.security.core.userdetails.UserDetails;
          import java.util.Collection;
          /**
           * @Description: SpringSecurity用户实体类
           * @Author: 翰戈.summer
           * @Date: 2023/11/18
           * @Param:
           * @Return:
           */
          @NoArgsConstructor
          @AllArgsConstructor
          public class UserDetailsEntity implements UserDetails {
              private String username;
              private String password;
              private Collection authorities;
              @Override
              public Collection getAuthorities() {
                  return authorities;
              }
              @Override
              public String getPassword() {
                  return password;
              }
              @Override
              public String getUsername() {
                  return username;
              }
              @Override
              public boolean isAccountNonExpired() {
                  return true;
              }
              @Override
              public boolean isAccountNonLocked() {
                  return true;
              }
              @Override
              public boolean isCredentialsNonExpired() {
                  return true;
              }
              @Override
              public boolean isEnabled() {
                  return true;
              }
              @Override
              public String toString() {
                  return "UserDetailsEntity{" +
                          "username='" + username + '\'' +
                          ", password='" + password + '\'' +
                          ", authorities=" + authorities +
                          '}';
              }
          }
          • JWT工具类 

            生成 jwt令牌 或解析,其中的JwtProperties(jwt令牌配置属性类)可以自己创建,不是重点。

            import io.jsonwebtoken.Claims;
            import io.jsonwebtoken.Jwts;
            import io.jsonwebtoken.SignatureAlgorithm;
            import lombok.RequiredArgsConstructor;
            import org.springframework.stereotype.Component;
            import java.util.Date;
            import java.util.Map;
            /**
             * @Description: 生成和解析jwt令牌
             * @Author: 翰戈.summer
             * @Date: 2023/11/16
             * @Param:
             * @Return:
             */
            @Component
            @RequiredArgsConstructor
            public class JwtUtils {
                private final JwtProperties jwtProperties;
                /**
                 * @Description: 生成令牌
                 * @Author: 翰戈.summer
                 * @Date: 2023/11/16
                 * @Param: Map
                 * @Return: String jwt
                 */
                public String getJwt(Map claims) {
                    String signingKey = jwtProperties.getSigningKey();
                    Long expire = jwtProperties.getExpire();
                    return Jwts.builder()
                            .setClaims(claims) //设置载荷内容
                            .signWith(SignatureAlgorithm.HS256, signingKey) //设置签名算法
                            .setExpiration(new Date(System.currentTimeMillis() + expire)) //设置有效时间
                            .compact();
                }
                /**
                 * @Description: 解析令牌
                 * @Author: 翰戈.summer
                 * @Date: 2023/11/16
                 * @Param: String jwt
                 * @Return: Claims claims
                 */
                public Claims parseJwt(String jwt) {
                    String signingKey = jwtProperties.getSigningKey();
                    return Jwts.parser()
                            .setSigningKey(signingKey) //指定签名密钥
                            .parseClaimsJws(jwt) //开始解析令牌
                            .getBody();
                }
            }
            • 登录接口

              用户登录成功并返回 jwt令牌,Result为统一响应的结果,UserLoginDTO用于封装用户登录信息,其中的UserDetails必须实现后才能获取到用户信息。

              import lombok.RequiredArgsConstructor;
              import org.springframework.security.authentication.AuthenticationManager;
              import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
              import org.springframework.security.core.Authentication;
              import org.springframework.security.core.GrantedAuthority;
              import org.springframework.security.core.context.SecurityContextHolder;
              import org.springframework.security.core.userdetails.UserDetails;
              import org.springframework.web.bind.annotation.PostMapping;
              import org.springframework.web.bind.annotation.RequestBody;
              import org.springframework.web.bind.annotation.RequestMapping;
              import org.springframework.web.bind.annotation.RestController;
              import java.util.Collection;
              import java.util.HashMap;
              import java.util.Map;
              /**
               * @Description: 用户登录操作相关接口
               * @Author: 翰戈.summer
               * @Date: 2023/11/20
               * @Param:
               * @Return:
               */
              @RestController
              @RequestMapping("/api/user/login")
              @RequiredArgsConstructor
              public class UserLoginController {
                  private final AuthenticationManager authenticationManager;
                  private final JwtUtils jwtUtils;
                  @PostMapping
                  public Result doLogin(@RequestBody UserLoginDTO userLoginDTO) {
                      try {
                          UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userLoginDTO.getUsername(), userLoginDTO.getPassword());
                          Authentication authentication = authenticationManager.authenticate(auth);
                          SecurityContextHolder.getContext().setAuthentication(authentication);
                          UserDetails userDetails = (UserDetails) authentication.getPrincipal();
                          //获取用户权限信息
                          String authorityString = "";
                          Collection authorities = userDetails.getAuthorities();
                          for (GrantedAuthority authority : authorities) {
                              authorityString = authority.getAuthority();
                          }
                          //用户身份验证成功,生成并返回jwt令牌
                          Map claims = new HashMap<>();
                          claims.put("username", userDetails.getUsername());
                          claims.put("authorityString", authorityString);
                          String jwtToken = jwtUtils.getJwt(claims);
                          return Result.success(jwtToken);
                      } catch (Exception ex) {
                          //用户身份验证失败,返回登陆失败提示
                          return Result.error("用户名或密码错误!");
                      }
                  }
              }
              • 自定义token过滤器

                过滤器中抛出的异常是不会被全局异常处理器捕获到的,直接返回错误结果。这里用到了SpringContextUtils通过上下文来获取Bean组件,下面会提供。

                过滤器属于Servlet(作用范围更大),拦截器属于SpringMVC(作用范围较小),全局异常处理器只能捕获到拦截器中的异常。在过滤器中无法初始化Bean组件,可以通过上下文来获取。

                import com.fasterxml.jackson.databind.ObjectMapper;
                import io.jsonwebtoken.Claims;
                import jakarta.servlet.FilterChain;
                import jakarta.servlet.http.HttpServletRequest;
                import jakarta.servlet.http.HttpServletResponse;
                import org.springframework.security.authentication.AuthenticationManager;
                import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
                import org.springframework.security.core.Authentication;
                import org.springframework.security.core.authority.SimpleGrantedAuthority;
                import org.springframework.security.core.context.SecurityContextHolder;
                import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
                import org.springframework.util.StringUtils;
                import java.io.IOException;
                import java.util.Collections;
                /**
                 * @Description: 自定义token验证过滤器,验证成功后将用户信息放入SecurityContext上下文
                 * @Author: 翰戈.summer
                 * @Date: 2023/11/18
                 * @Param:
                 * @Return:
                 */
                public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
                    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
                        super(authenticationManager);
                    }
                    @Override
                    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {
                        try {
                            //获取请求头中的token
                            String jwtToken = request.getHeader("token");
                            if (!StringUtils.hasLength(jwtToken)) {
                                //token不存在,交给其他过滤器处理
                                filterChain.doFilter(request, response);
                                return; //结束方法
                            }
                            //过滤器中无法初始化Bean组件,使用上下文获取
                            JwtUtils jwtUtils = SpringContextUtils.getBean("jwtUtils");
                            if (jwtUtils == null) {
                                throw new RuntimeException();
                            }
                            //解析jwt令牌
                            Claims claims;
                            try {
                                claims = jwtUtils.parseJwt(jwtToken);
                            } catch (Exception ex) {
                                throw new RuntimeException();
                            }
                            //获取用户信息
                            String username = (String) claims.get("username"); //用户名
                            String authorityString = (String) claims.get("authorityString"); //权限信息
                            Authentication authentication = new UsernamePasswordAuthenticationToken(
                                    username, null,
                                    Collections.singleton(new SimpleGrantedAuthority(authorityString))
                            );
                            //将用户信息放入SecurityContext上下文
                            SecurityContextHolder.getContext().setAuthentication(authentication);
                            filterChain.doFilter(request, response);
                        } catch (Exception ex) {
                            //过滤器中抛出的异常无法被全局异常处理器捕获,直接返回错误结果
                            response.setCharacterEncoding("utf-8");
                            response.setContentType("application/json; charset=utf-8");
                            String value = new ObjectMapper().writeValueAsString(Result.error("用户未登录!"));
                            response.getWriter().write(value);
                        }
                    }
                }
                • SpringContextUtils工具类

                  import jakarta.annotation.Nonnull;
                  import org.springframework.beans.BeansException;
                  import org.springframework.context.ApplicationContext;
                  import org.springframework.context.ApplicationContextAware;
                  import org.springframework.stereotype.Component;
                  /**
                   * @Description: 用于创建上下文,实现ApplicationContextAware接口
                   * @Author: 翰戈.summer
                   * @Date: 2023/11/17
                   * @Param:
                   * @Return:
                   */
                  @Component
                  public class SpringContextUtils implements ApplicationContextAware {
                      private static ApplicationContext applicationContext;
                      public static ApplicationContext getApplicationContext() {
                          return applicationContext;
                      }
                      @Override
                      public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
                          SpringContextUtils.applicationContext = applicationContext;
                      }
                      @SuppressWarnings("unchecked")
                      public static  T getBean(String name) throws BeansException {
                          if (applicationContext == null) {
                              return null;
                          }
                          return (T) applicationContext.getBean(name);
                      }
                  }
                  • 添加自定义token验证过滤器

                    将自定义token验证过滤器,添加到UsernamePasswordAuthenticationFilter前面。

                    UsernamePasswordAuthenticationFilter实现了基于用户名和密码的认证逻辑,我们利用token进行身份验证,所以用不到这个过滤器。

                        /**
                         * @Description: 配置SecurityFilterChain过滤器链
                         * @Author: 翰戈.summer
                         * @Date: 2023/11/17
                         * @Param: HttpSecurity
                         * @Return: SecurityFilterChain
                         */
                        @Bean
                        public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
                            httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                                    .requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() //登录放行
                                    .anyRequest().authenticated()
                            );
                            httpSecurity.authenticationProvider(authenticationProvider());
                            //禁用登录页面
                            httpSecurity.formLogin(AbstractHttpConfigurer::disable);
                            //禁用登出页面
                            httpSecurity.logout(AbstractHttpConfigurer::disable);
                            //禁用session
                            httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
                            //禁用httpBasic
                            httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
                            //禁用csrf保护
                            httpSecurity.csrf(AbstractHttpConfigurer::disable);
                            //通过上下文获取AuthenticationManager
                            AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager");
                            //添加自定义token验证过滤器
                            httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);
                            return httpSecurity.build();
                        }
                    • 自定义用户未登录的处理

                      用户请求未携带token的处理,替换AuthenticationEntryPoint

                      import com.fasterxml.jackson.databind.ObjectMapper;
                      import jakarta.servlet.http.HttpServletRequest;
                      import jakarta.servlet.http.HttpServletResponse;
                      import org.springframework.security.core.AuthenticationException;
                      import org.springframework.security.web.AuthenticationEntryPoint;
                      import org.springframework.stereotype.Component;
                      import java.io.IOException;
                      /**
                       * @Description: 自定义用户未登录的处理(未携带token)
                       * @Author: 翰戈.summer
                       * @Date: 2023/11/19
                       * @Param:
                       * @Return:
                       */
                      @Component
                      public class AuthEntryPointHandler implements AuthenticationEntryPoint {
                          @Override
                          public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
                              response.setCharacterEncoding("utf-8");
                              response.setContentType("application/json; charset=utf-8");
                              String value = new ObjectMapper().writeValueAsString(Result.error("未携带token!"));
                              response.getWriter().write(value);
                          }
                      }
                      • 自定义用户权限不足的处理

                        用户权限不足的处理,替换AccessDeniedHandler

                        import com.fasterxml.jackson.databind.ObjectMapper;
                        import jakarta.servlet.http.HttpServletRequest;
                        import jakarta.servlet.http.HttpServletResponse;
                        import org.springframework.security.access.AccessDeniedException;
                        import org.springframework.security.web.access.AccessDeniedHandler;
                        import org.springframework.stereotype.Component;
                        import java.io.IOException;
                        /**
                         * @Description: 自定义用户权限不足的处理
                         * @Author: 翰戈.summer
                         * @Date: 2023/11/19
                         * @Param:
                         * @Return:
                         */
                        @Component
                        public class AuthAccessDeniedHandler implements AccessDeniedHandler {
                            @Override
                            public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
                                response.setCharacterEncoding("utf-8");
                                response.setContentType("application/json; charset=utf-8");
                                String value = new ObjectMapper().writeValueAsString(Result.error("权限不足!"));
                                response.getWriter().write(value);
                            }
                        }
                        • 添加自定义处理器

                          修改 SecurityConfig 配置类,注入 AuthAccessDeniedHandler 和 AuthEntryPointHandler

                              /**
                               * @Description: 配置SecurityFilterChain过滤器链
                               * @Author: 翰戈.summer
                               * @Date: 2023/11/17
                               * @Param: HttpSecurity
                               * @Return: SecurityFilterChain
                               */
                              @Bean
                              public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
                                  httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                                          .requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() //登录放行
                                          .anyRequest().authenticated()
                                  );
                                  httpSecurity.authenticationProvider(authenticationProvider());
                                  //禁用登录页面
                                  httpSecurity.formLogin(AbstractHttpConfigurer::disable);
                                  //禁用登出页面
                                  httpSecurity.logout(AbstractHttpConfigurer::disable);
                                  //禁用session
                                  httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
                                  //禁用httpBasic
                                  httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
                                  //禁用csrf保护
                                  httpSecurity.csrf(AbstractHttpConfigurer::disable);
                                  //通过上下文获取AuthenticationManager
                                  AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager");
                                  //添加自定义token验证过滤器
                                  httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);
                                  //自定义处理器
                                  httpSecurity.exceptionHandling(exceptionHandling -> exceptionHandling
                                          .accessDeniedHandler(authAccessDeniedHandler) //处理用户权限不足
                                          .authenticationEntryPoint(authEntryPointHandler) //处理用户未登录(未携带token)
                                  );
                                  return httpSecurity.build();
                              }
                          • 静态资源放行

                            SpringBoot3 中使用 Swagger3 接口文档,在整合了 SpringSecurity 后会出现无法访问的情况,需要给静态资源放行。

                            在 SecurityConfig 中添加

                                /**
                                 * 静态资源放行
                                 */
                                @Bean
                                public WebSecurityCustomizer webSecurityCustomizer() {
                                    return (web) -> web.ignoring().requestMatchers(
                                            "/doc.html",
                                            "/doc.html/**",
                                            "/v3/api-docs",
                                            "/v3/api-docs/**",
                                            "/webjars/**",
                                            "/authenticate",
                                            "/swagger-ui.html/**",
                                            "/swagger-resources",
                                            "/swagger-resources/**"
                                    );
                                }