SpringBoot3 + SpringSecurity6 前后端分离

目录

导入依赖

添加配置类

实现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/**"
        );
    }
  • 总结

SpringSecurity6 的用法和以前版本的有较大差别,比如WebSecurityConfigurerAdapter的废除,看到配置类继承了这个的都是过时的教程。因为不再继承,所以不能通过重写方法的方式去配置。另外很多配置的方式都变成使用Lambda表达式,或者是方法引用。

 创作不易,如果对你有帮助的话就点个赞鼓励一下吧 (人 •͈ᴗ•͈) (୨୧•͈ᴗ•͈)◞ᵗʱᵃᵑᵏઽ*♡

你可能感兴趣的:(其他内容,spring,security,spring,boot,java,后端)