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调用访问登陆地址即可。

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冬山兄

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

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

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

打赏作者

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

抵扣说明:

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

余额充值