0


SpringBoot3.3.0 整合 SpringSecurity 的详细步骤

在Java企业级开发中,Spring Security 是一个广泛使用的安全框架,它提供了身份验证、授权以及防止攻击等安全性功能。SpringBoot3 与 SpringSecurity 的整合能够极大简化安全配置和管理的复杂性。

本片文章基于JDK17+springboot3.3.0

开发工具使用到hutool

以下将详细介绍如何在 SpringBoot3 项目中整合 SpringSecurity。

1. 引入依赖

首先,你需要在 SpringBoot 项目的

pom.xml

文件中引入 Spring Security 的依赖。对于 SpringBoot3,确保使用的是与 SpringBoot 版本兼容的 Spring Security 版本。

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath />
    </parent>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- JWT 相关 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

2. 配置 Spring Security

接下来配置 Spring Security 以满足安全需求。通常涉及到设置用户验证、请求授权以及配置各种过滤器等。

2.1 编写安全性配置类(核心类)

创建一个配置类来扩展

WebSecurityConfigurerAdapter

并覆盖其方法来配置安全性。

@Slf4j
@Configuration
@EnableWebSecurity //开启SpringSecurity的默认行为
@RequiredArgsConstructor//bean注解
// 新版不需要继承WebSecurityConfigurerAdapter
public class WebSecurityConfig {

    // 这个类主要是获取库中的用户信息,交给security
    private final UserDetailServiceImpl userDetailsService;
    // 这个的类是认证失败处理(我在这里主要是把错误消息以json方式返回)
    private final JwtAuthenticationEntryPoint authenticationEntryPoint;
    // 鉴权失败的时候的处理类
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    //    登录成功处理
    private final LoginSuccessHandler loginSuccessHandler;
    //    登录失败处理
    private final LoginFailureHandler loginFailureHandler;
    //    登出成功处理
    private final LoginLogoutSuccessHandler loginLogoutSuccessHandler;
    //    token过滤器
    private final JwtTokenFilter jwtTokenFilter;
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authenticationConfiguration
    ) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // 加密方式
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 核心配置
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("------------filterChain------------");
        http
                //  禁用basic明文验证
                .httpBasic(Customizer.withDefaults())
                //  基于 token ,不需要 csrf
                .csrf(AbstractHttpConfigurer::disable)
                //  禁用默认登录页
                .formLogin(fl ->
                        fl.loginPage(PathMatcherUtil.FORM_LOGIN_URL)
                        .loginProcessingUrl(PathMatcherUtil.TO_LOGIN_URL)
                        .usernameParameter("username")
                        .passwordParameter("password")
                        .successHandler(loginSuccessHandler)
                        .failureHandler(loginFailureHandler)
                        .permitAll())
                //  禁用默认登出页
                .logout(lt -> lt.logoutSuccessHandler(loginLogoutSuccessHandler))
                //  基于 token , 不需要 session
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                //  设置 处理鉴权失败、认证失败
                .exceptionHandling(
                        exceptions -> exceptions.authenticationEntryPoint(authenticationEntryPoint)
                                .accessDeniedHandler(jwtAccessDeniedHandler)
                )
                //  下面开始设置权限
                .authorizeHttpRequests(authorizeHttpRequest -> authorizeHttpRequest
                        //  允许所有 OPTIONS 请求
                        .requestMatchers(PathMatcherUtil.AUTH_WHITE_LIST).permitAll()
                        //  允许直接访问 授权登录接口
//                        .requestMatchers(HttpMethod.POST, "/web/authenticate").permitAll()
                        //  允许 SpringMVC 的默认错误地址匿名访问
//                        .requestMatchers("/error").permitAll()
                        //  其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailImpl对象中默认设置“ROLE_USER”
                        //.requestMatchers("/**").hasAnyAuthority("ROLE_USER")
//                        .requestMatchers("/heartBeat/**", "/main/**").permitAll()
                        //  允许任意请求被已登录用户访问,不检查Authority
                        .anyRequest().authenticated()
                )
                //  添加过滤器
                .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //可以加载fram嵌套页面
        http.headers( headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
        return http.build();
    }
    @Bean
    public UserDetailsService userDetailsService() {
        return userDetailsService::loadUserByUsername;
    }

    /**
     * 调用loadUserByUserName获取userDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
     *
     * @return
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
    /**
     * 配置跨源访问(CORS)
     *
     * @return
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}
2.2 自定义用户登录认证

从数据库中查询用户信息,再进行用户验证逻辑,实现

UserDetailsService

接口,并在

AuthenticationManagerBuilder

中配置。

/**
 * 自定义登录接口服务类
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class UserDetailServiceImpl implements UserDetailsService {

    // 注入管理员信息service
    private final ManagerService managerService;
    // 注入角色信息service
    private final RoleService roleService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ManagerPo mng = managerService.getByUsername(username);
        if (mng == null) {
            log.info("用户名不存在!userName=" + username);
            throw new UsernameNotFoundException("用户名不存在" + username);
        }
        if (mng.getState() != 1) {
            log.info("用户已被冻结!userName=" + username);
            throw new LockedException("该用户已被冻结" + username);
        }
        // 角色集合
        Set<GrantedAuthority> authorities = new HashSet<>();
        // 查询用户角色
        List<RolePo> roleList = roleService.getByManager(mng.getId());
        for (RolePo role : roleList) {
            authorities.add(new SimpleGrantedAuthority(role.getRole()));
        }
        JwtMngBo jwtMng = new JwtMngBo(mng.getId(), mng.getUsername(), mng.getTrueName(), mng.getPassword(),
                mng.getGroupMark(), authorities);

        return jwtMng;
    }
}
2.3 认证失败处理
用户未登录处理类 自定义身份验证失败的handler,包括跳转页面并统计拦截次数
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    //统计用户错误登陆日志service
    @Autowired
    private MngLoginlogService mngLoginlogService;
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.error("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), authException);
        String requestUri = request.getRequestURI();
        String browser = request.getHeader("user-agent");
        // 请求login 或者 又admin字段都判断未登录 需要重新登录
        if (isAdminLogin(requestUri)) {
            JwtTokenGetUtil.deleteCookieToken(response);
            String token = JwtTokenGetUtil.getToken(request);
            log.info("认证失败后,后台不是登陆地址,则进入后台登录界面。requestURI={},token={}", requestUri, token);
            mngLoginlogService.recordLog(request, MngLoginLogDic.login_no, "", "browser=" + browser + ",token=" + token);
            response.sendRedirect("/xxx/login.html");
            return;
        }
        log.info("认证失败!--requestURI={},ip={},userAgent:{}", requestUri, IpUtil.getIp(request), browser);
        mngLoginlogService.recordLog(request, MngLoginLogDic.login_no, "", browser);
        LoginResultUtil.reJson(response, MsgCode.SYSTEM_TOKEN_AUTH_ERROR);
    }

    /**
     * 在用户身份认证失败后,判断为正确的后台地址,又不是登录页面,则返回true 防止用户身份过期后,无法跳转到登录页面处理
     *
     * @param url
     * @return
     */
    private boolean isAdminLogin(String url) {
        if (url.contains("admin") || url.contains("ADMIN")) {
            if (!(url.contains("login") || url.contains("LOGIN"))) {
                return true;
            }
        }
        return false;
    }
}
2.4 鉴权失败的时候的处理(暂无权限处理类)
@Slf4j
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Autowired
    private MngLoginlogService mngLoginlogService;
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        mngLoginlogService.recordLog(request, MngLoginLogDic.perm_no, "", accessDeniedException.getMessage());
        LoginResultUtil.reJson(response, 70001, MsgCode.PERMISSION_NO_ACCESS);
        log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
                "", accessDeniedException);
    }
}
2.5 鉴权成功后(token生成管理)
@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    
    @Autowired
    private  ManagerService managerService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        // 组装JWT
        JwtMngBo jwtMng = (JwtMngBo) authentication.getPrincipal();
        String token = MngJwtTokenUtil.generateToken(jwtMng);
        //存放token到cookie中,最好时直接返回json
        JwtTokenGetUtil.setCookieToken(response, token);

        //返回json
        //response.sendRedirect(request.getContextPath() + PathMatcherUtil.INDEX_URL);
    }
}

说明:jwt生成比较常见,这里忽略。生成token如何返回,根据自己场景而定,只要在随后得请求中带上即可。

用户安全模型(专供安全管理使用):
@Data
public class JwtMngBo implements UserDetails {
    private Integer id;
    private String password;
    private String username;
    private String trueName;

    /**
     * @Description 得到用户的角色列表
     */
    private Collection<? extends GrantedAuthority> authorities;
    /**
     * 判断用户是否为过期
     */
    private boolean accountNonExpired = true;
    /**
     * 判断用户是否为锁定
     */
    private boolean accountNonLocked = true;
    /**
     * 判断密码是否未过期
     */
    private boolean credentialsNonExpired = true;
    /**
     * 判断账户是否激活
     */
    private boolean enabled = true;

    public JwtMngBo(Integer id, String username, String trueName, String password, 
                    Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.username = username;
        this.trueName = trueName;
        this.password = password;
        this.authorities = authorities;
    }
}
2.6 登陆失败处理
@Slf4j
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    
    
    
    public LoginFailureHandler() {
        this.setDefaultFailureUrl("/xxx/login.html");
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) {
        if (exception instanceof UsernameNotFoundException) {
            log.info("【用户名不存在】" + exception.getMessage());
            //用户名不存在
            redirectLogin(response, request.getContextPath() + "/xxx/login.html?error=userNotExis");
            return;
        }
        if (exception instanceof LockedException) {
            log.info("【用户被冻结】" + exception.getMessage());
            //用户被冻结
            redirectLogin(response, request.getContextPath() + "/xxx/login.html?error=userFrozen");
            return;
        }
        if (exception instanceof BadCredentialsException) {
            log.info("【用户名密码不正确】" + exception.getMessage());
            //用户名密码不正确
            throw new BusinessException(MsgCode.USER_LOGIN_ERROR);
        }
        log.info("-----------登录验证失败,其他登录失败错误");
        //其他登录失败错误
        redirectLogin(response, request.getContextPath() + "/xxx/login.html?error=loginFailed");
        
    }

    private void  redirectLogin(HttpServletResponse res,String url){
        try {
            res.sendRedirect(url);
        } catch (IOException e1) {
            logger.error(e1.getMessage());
//            e1.printStackTrace();
        }
    }

}
2.7 登出处理
@Component
public class LoginLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        //清楚token储存
        JwtTokenGetUtil.deleteCookieToken(response);
        //跳转登陆页面
        LoginResultUtil.reLoginHtml(response, "登出时");
    }
}
2.8 Token过滤器

此过滤器很重要,主要负责资源放过,拦截,以及token验证

@Slf4j
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
    @Autowired
    private LoginFilterMng loginFilterMng;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String requestUri = request.getRequestURI();
        // Url白名单,正常放过
        if (PathMatcherUtil.passWhiteUrl(requestUri)) {
            chain.doFilter(request, response);
            return;
        }
        // Url黑名单,直接拦截返回
        if (PathMatcherUtil.passBlackUrl(requestUri)) {
            log.info("进入黑名单!requestUri={},ip={},userAgent={}", requestUri, IpUtil.getIp(request),
                    request.getHeader("user-agent"));
            response.sendRedirect("/xxx/404");
            return;
        }
        //验证码拦截验证
        if (PathMatcherUtil.TO_LOGIN_URL.equals(requestUri)) {
            // 验证前端传来的验证码
            if (loginFilterMng.verifyCode(request, response)) {
                log.info("验证码拦截!");
                chain.doFilter(request, response);
                return ;
            }else{
                chain.doFilter(request, response);
                return ;
            }
        }
        // 验证token是否有效
        String token = JwtTokenGetUtil.getToken(request);
        if (StringUtils.isEmpty(token)) {
            log.info("[后台token]为空!requestUri={},ip={},userAgent={}", requestUri, IpUtil.getIp(request),
                    request.getHeader("user-agent"));
            response.sendRedirect("/xxx/login.html");
//            chain.doFilter(request, response);
            return;
        }
        UsernamePasswordAuthenticationToken authentication = getAuthenticationToken(token, requestUri);
        if (authentication == null) {
            chain.doFilter(request, response);
            return;
        }
        SecurityContextHolder.getContext().setAuthentication(authentication);
        if (requestUri.contains("/xxx/login.html")) {
            response.sendRedirect("/xxx/index.html");
            return;
        }
        chain.doFilter(request, response);

    }
    private UsernamePasswordAuthenticationToken getAuthenticationToken(String token, String requestUri) {
        try {
            Claims claims = MngJwtTokenUtil.getClaimsFromToken(token);
            if (claims==null){
                log.error("token中过期,claims为空! requestUri={}", requestUri);
                return null;
            }
            String username = claims.getSubject();
            String userId = claims.getId();
            if (StringUtils.isEmpty(username) || StringUtils.isEmpty(userId)) {
                log.error("token中username或userId为空! requestUri={}", requestUri);
                return null;
            }
            // 获取角色
            List<GrantedAuthority> authorities = new ArrayList<>();
            String authority = claims.get("authorities").toString();
            if (!StringUtils.isEmpty(authority)) {
                @SuppressWarnings("unchecked")
                List<Map<String, String>> authorityMap = JSONObject.parseObject(authority, List.class);
                for (Map<String, String> role : authorityMap) {
                    if (!role.isEmpty()) {
                        authorities.add(new SimpleGrantedAuthority(role.get("authority")));
                    }
                }
            }
            String trueName = claims.get("trueName").toString();
            JwtMngBo jwtMng = new JwtMngBo(Integer.parseInt(userId), username, trueName, "",  authorities);

            return new UsernamePasswordAuthenticationToken(jwtMng, userId, authorities);
        } catch (ExpiredJwtException e) {
            logger.error("Token已过期: {} " + e);
            /* throw new TokenException("Token已过期"); */
        } catch (UnsupportedJwtException e) {
            logger.error("requestURI=" + requestUri + ",token=" + token + ",Token格式错误: {} " + e);
            /* throw new TokenException("Token格式错误"); */
        } catch (MalformedJwtException e) {
            logger.error("requestURI=" + requestUri + ",token=" + token + ",Token没有被正确构造: {} " + e);
            /* throw new TokenException("Token没有被正确构造"); */
        } catch (SignatureException e) {
            logger.error("requestURI=" + requestUri + ",token=" + token + ",签名失败: {} " + e);
            /* throw new TokenException("签名失败"); */
        } catch (IllegalArgumentException e) {
            logger.error("requestURI=" + requestUri + ",token=" + token + ",非法参数异常: {} " + e);
            /* throw new TokenException("非法参数异常"); */
        }
        return null;
    }
}

3. 控制器

编写控制器来处理登录和注销请求。

@Controller
@RequestMapping("/xxx")
public class AdminLoginAction {

    //登录入口
    @RequestMapping(value = "/login.html")
    public String login(HttpServletRequest request, HttpServletResponse response) {
        return "/xxx/loginForm";
    }

    /**
     * 生成验证码
     */
    @RequestMapping(value = "/getVerify")
    public void getVerify(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.setContentType("image/jpeg");// 设置相应类型,告诉浏览器输出的内容为图片
            response.setHeader("Pragma", "No-cache");// 设置响应头信息,告诉浏览器不要缓存此内容
            response.setHeader("Cache-Control", "no-cache");
            response.setDateHeader("Expire", 0);
            RandomValidateCodeUtil randomValidateCode = new RandomValidateCodeUtil();
            randomValidateCode.getRandcode(request, response);// 输出验证码图片方法
        } catch (Exception e) {
            log.error("获取验证码失败>>>> ", e);
        }
    }

    @GetMapping(value = "/404.html")
    public String error404(HttpServletRequest request, HttpServletResponse response) {
        return "/error/404";
    }
}

4. 创建登录页面

创建一个简单的HTML页面loginForm作为登录页面,通常放在

src/main/resources/templates

目录下。

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
<meta name="renderer" content="webkit|ie-comp|ie-stand">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport"
    content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi" />
<meta http-equiv="Cache-Control" content="no-siteapp" />
</head>
<body>
    <div class="login layui-anim layui-anim-up">
        <div class="message">登录</div>
        <div id="darkbannerwrap"></div>

        <form method="post" class="layui-form" id="login-form" action="">
            <input name="username" placeholder="用户名" type="text" lay-verify="required" class="layui-input">
            <input name="password" lay-verify="required" placeholder="密码" type="password" class="layui-input">
            <input class="form-inline"   name="verifyCode" autocomplete="off"
                 style="width: 50%" type="text" id="verify_input" placeholder="请输入验证码" maxlength="4">
            <a href="javascript:void(0);" rel="external nofollow" title="点击更换验证码"> <img id="imgVerify" src="" alt="更换验证码"
                style="vertical-align: bottom; float: right" height="46" width="40%" onclick="getVerify(this);">
            </a>
            <hr class="hr15">
            <input value="登录"  style="width: 100%;" type="button" onclick="login(this);">
            <span id="info" style="color: red"></span>
        </form>
    </div>
</body>
</html>

说明:以上HTML代码只做主要功能展示,基于安全考虑,提出了静态资源文件,所以不能直接使用,可以根据自己得界面设计参照使用。

其中,οnclick="getVerify(this);"主要作用时更新验证码,若无验证码需求,可去除。

οnclick="login(this);"为js登陆方法,里面主要时使用ajax调用访问登陆地址即可。

本文转载自: https://blog.csdn.net/u011291844/article/details/140594118
版权归原作者 冬山兄 所有, 如有侵权,请联系我们删除。

“SpringBoot3.3.0 整合 SpringSecurity 的详细步骤”的评论:

还没有评论