Springboot切换到3.3.4后,使用spring security一些小问题记录

这几天想试试Spring AI这个组件,看看ollama通过Spring AI怎么用。官方的介绍地址:https://spring.io/projects/spring-ai。
经过一番研究发现,发现这个组件需要Springboot 3.2+,之前一直在用Spring Boot2.6.13,同时为了自用,也自己组了一个基于Spring Security的脚手架。
最终经过一番选择,最终选了SpringBoot 3.3.4,JDK选择的是17,FastJson更新为2.0.53,JJWT更新为0.12.6,该版本调整为3个依赖:

<dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.12.6</version>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-api</artifactId>
      <version>0.12.6</version>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-impl</artifactId>
      <version>0.12.6</version>
    </dependency>

一番下来,发现如果仅仅是用chat接口来实现,还是挺简单的,不过要是用Stream模式的话,一般来讲还用Vue之类的框架,效果可能会更好些,所以干脆向着再弄一个新版的基于Spring Security的脚手架出来。
经过一番折腾,大部分问题都解决了,本文章主要是为了描述SpringBoot的一些基本使用问题,因为目前的经验及水平限制,这两天也弄不清楚到底是什么引起的,先把问题先记录下来,同时也找了算是一些解决方案的内容,供后面遇到类似问题的同学做一些启发。
1、问题一,这是遇到的第一个拦路虎,访问时提示:
拒绝访问 localhost,你没有查看此页面的用户权限,错误代码是HTTP ERROR 403。
在这里插入图片描述
后来费了一番手脚,才意识到是因为此接口不支持GET方式访问,通过apifox来访问,是没问题的:
在这里插入图片描述
估计是security拦截了错误提示,控制器里这样的:
在这里插入图片描述
再后来到一些内容,发现可能是因为@RequestBody要求强制验证了,这里改下:
在这里插入图片描述
重启服务,可以了。
在这里插入图片描述
浪费了好几个小时的时间o(╥﹏╥)o。
2、JWT的过滤器中,跟踪发现错误都是redis转换数据时的提示各种错误:
比如:

提示:java.lang.ClassCastException: class com.rainpet.springsecurity.entity.SysUser cannot be cast to class com.rainpet.springsecurity.entity.SysUser (com.rainpet.springsecurity.entity.SysUser is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @6171d624; com.rainpet.springsecurity.entity.SysUser is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @2b81ec2a)

后来禁用了spring-boot-devtool也不行。

提示:Name for argument of type [java.lang.String] not specified, and parameter name information not available via reflection. Ensure that the compiler uses the '-parameters' flag

后来发现把build段中的部分改为:

<plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>17</source>
          <target>17</target>
          <compilerArgs>
            <compilerArg>-parameters</compilerArg>
          </compilerArgs>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>

之前source及target配的是1.8,后来配置为17,同时增加了:

<compilerArgs>
            <compilerArg>-parameters</compilerArg>
          </compilerArgs>

貌似问题解决了。
3、SecurityConfig.java文件内容变动为,之前的版本和现在的版本差异较大:


import com.rainpet.springsecurity.filter.CustomAuthenticationProvider;
import com.rainpet.springsecurity.filter.JwtAuthenticationTokenFilter;
import com.rainpet.springsecurity.service.impl.UserDetailsServiceImpl;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
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.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;

@Configuration
public class SecurityConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl(); // Implement this service to load user details
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public CustomAuthenticationProvider customAuthenticationProvider(UserDetailsService userDetailsService) {
        return new CustomAuthenticationProvider((UserDetailsServiceImpl) userDetailsService);
    }

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//        若要让requestMatchers匹配所有地址,可使用"/**"
        http
                // 使用新的csrf配置方式
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/user/login","/user/register","/user/emailLogin","/user/telLogin","/error","/static/**").permitAll()
                        .anyRequest().authenticated()
                )
                .exceptionHandling(exceptions -> exceptions
                        .authenticationEntryPoint(this::handleAuthenticationException)
                        .accessDeniedHandler(new CustomAccessDeniedHandler())
                );

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


        // ... 其他配置保持不变 ...

        return http.build();
    }

    private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {

//            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
            response.getWriter().print(exception.getMessage());
            throw  exception;

    }

    static class CustomAccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, org.springframework.security.access.AccessDeniedException accessDeniedException) throws IOException {
//            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
//            response.setContentType("application/json;charset=UTF-8");
//            response.getWriter().write("{\"status\":\"error\",\"message\":\"Forbidden\"}");
            throw accessDeniedException;
        }
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

4、RedisConfig.java更改为:


import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.rainpet.springsecurity.utils.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.nio.charset.StandardCharsets;

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 使用 FastJSON2 作为值的序列化器
        RedisSerializer<Object> fastJson2RedisSerializer = new RedisSerializer<Object>() {
            @Override
            public byte[] serialize(Object o) throws SerializationException {
                if (o == null) {
                    return new byte[0];
                }
                return JSON.toJSONString(o, JSONWriter.Feature.WriteClassName).getBytes(StandardCharsets.UTF_8);
            }

            @Override
            public Object deserialize(byte[] bytes) throws SerializationException {
                if (bytes == null || bytes.length == 0) {
                    return null;
                }
                return JSON.parseObject(new String(bytes, StandardCharsets.UTF_8), Object.class, JSONReader.Feature.SupportAutoType);
            }
        };

        // 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(fastJson2RedisSerializer);

        // Hash 的 key 也采用 StringRedisSerializer 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        template.setHashValueSerializer(fastJson2RedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

5、mybatisplus版本更新为3.5.10.1,同时相关的配置文件也更新了,主要是牵涉分页组件的调整:
依赖:

<dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
      <version>3.5.10.1</version>
    </dependency>
    <!-- jdk 11+ 引入可选模块 -->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-jsqlparser</artifactId>
      <version>3.5.10.1</version>
    </dependency>

配置文件:MyBatisPlusConfig.java

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件,并指定数据库类型
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        // 设置最大单页限制数量,默认500条,-1不受限制
        paginationInnerInterceptor.setMaxLimit(1000L);
        // 设置请求的页面大于最大页后操作,true调回到首页,false继续请求,默认false
        paginationInnerInterceptor.setOverflow(true);
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
}

6、JwtAuthenticationTokenFilter.java更新为:

import com.rainpet.springsecurity.entity.LoginUser;
import com.rainpet.springsecurity.utils.JwtUtil;
import com.rainpet.springsecurity.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //如果没有token 则进行下次的filter 放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            // 设置响应状态码为 401,表示未授权
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            // 设置响应内容类型为 JSON
            response.setContentType("application/json;charset=UTF-8");
            // 构造异常信息的 JSON 字符串
            String errorJson = "{" +
                    "\"code\":401," +
                    "\"message\": \""+e.getMessage()+"\"}";
            // 获取响应输出流
            response.getWriter().write(errorJson);
            return;
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }

        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中  ****重点 第三个参数是获取权限数据,实际应用中这里必须设置,是从登陆时设置的权限列表中获取
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

7、JwtUtils.java更新为:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 *5000L;// 60 * 60 *1000  一个小时

    static String secret="123";//此处长度要调整为适当长度

    public static String getUUID() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, JWT_TTL, getUUID()); // 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID()); // 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .id(uuid)              // 唯一的ID
                .subject(subject)   // 主题  可以是JSON数据
                .issuer("sg")     // 签发者
                .issuedAt(now)      // 签发时间
                .signWith(secretKey, Jwts.SIG.HS512) // 使用HS256对称加密算法签名
                .expiration(expDate);
    }

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id); // 设置过期时间
        return builder.compact();
    }

    public static void main(String[] args) throws Exception {
        String token = "eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiI5ZGI3MzE5Y2RjNWU0ODhlYTA2ZTAyOTQ1MGNmYzYxMCIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTc0Mzc1NzE2OCwiZXhwIjoxNzQzNzc1MTY4fQ.ezfygdS3rMuSSD0Q6DDyGS2MLNOObna9QwaPfPwCd008jJ9mGPx3hDjb3jXZDBnTRO1nsgoOr662fImECRlu7g";
        Claims claims = parseJWT(token);
        System.out.println(claims);
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        // 使用 Jwts.SIG.HS256.key() 生成符合安全要求
//        return Jwts.SIG.HS512.key().build();
        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .verifyWith(secretKey)  // 使用 verifyWith 替代 setSigningKey
                .build()               // 需要调用 build() 方法
                .parseSignedClaims(jwt) // 使用 parseSignedClaims 替代 parseClaimsJws
                .getPayload();         // 使用 getPayload 替代 getBody
    }
}

8、application.yaml文件也更新了:
主要是springboot3中牵涉redis的部分有更新。

server:
  port: 8086
  tomcat:
    use-native: false

spring:
  application:
    name: spring-boot-application
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/mall?allowMultiQueries=true&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
  data:
    redis:
      # Redis服务器地址
      host: 127.0.0.1
      # Redis服务器连接端口
      port: 6379
      # Redis服务器连接密码
      password: 
      # Redis数据库索引
      database: 3
      # 连接超时时间(毫秒)
      timeout: 30000
      lettuce:
        pool:
          max-active: 50
          max-wait: -1
          max-idle: 50
          min-idle: 1

9、统一异常处理问题:
首先要先配置好:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.List;
import java.util.Properties;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
        }
}

其次,增加统一异常处理:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LogManager.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(NoResourceFoundException.class)
    public ResponseEntity<Map<String, Object>> handleResourceNotFoundException(Exception ex) {
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("message", ex.getMessage());
        errorResponse.put("status", HttpStatus.NOT_FOUND.value());
        return new ResponseEntity<>(errorResponse, HttpStatus.OK);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleGeneralException(Exception ex) {
        logger.error("An exception occurred: ", ex);
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("message", "An unexpected error occurred: " + ex.getMessage());
        errorResponse.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }



}
Spring Boot Starter Security是一个轻量级的框架,用于快速集成Spring SecuritySpring Boot应用程序中。版本3.3.4提供了一系列的安全功能,包括身份验证、授权、会话管理等。以下是使用Spring Boot Starter Security的一个简单教程: 1. **添加依赖**: 在你的`pom.xml`或`build.gradle`文件中添加Spring Security的相关依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` 2. **配置应用安全**: 创建一个`SecurityConfig.java`类并实现WebSecurityConfigurerAdapter,设置默认的登录页面和注销路径: ```java import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/login").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login") .logout().logoutUrl("/logout"); } // 如果需要,可以在此配置用户认证 @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("{noop}password").roles("USER"); } } ``` 3. **创建登录界面**: 创建一个简单的HTML登录表单,并将其放在`src/main/resources/static/login.html`文件中。 4. **启动应用**: 使用Spring Boot命令行工具运行应用,访问`http://localhost:8080/login`尝试登录。 5. **自定义授权规则**: 如果你需要更复杂的权限控制,可以在`configure()`方法中使用`accessDecisionManager()`,或者使用基于注解的声明式安全控制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lwprain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值