当前位置:   article > 正文

SpringBoot3.3.0 整合 SpringSecurity 的详细步骤_springboot3整合springsecurity

springboot3整合springsecurity

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

本片文章基于JDK17+springboot3.3.0
开发工具使用到hutool

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

1. 引入依赖

首先,你需要在 SpringBoot 项目的 pom.xml 文件中引入 Spring Security 的依赖。对于 SpringBoot3,确保使用的是与 SpringBoot 版本兼容的 Spring Security 版本。

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>3.3.0</version>
  5. <relativePath />
  6. </parent>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-security</artifactId>
  10. </dependency>
  11. <!-- JWT 相关 -->
  12. <dependency>
  13. <groupId>io.jsonwebtoken</groupId>
  14. <artifactId>jjwt-api</artifactId>
  15. <version>0.11.5</version>
  16. </dependency>
  17. <dependency>
  18. <groupId>io.jsonwebtoken</groupId>
  19. <artifactId>jjwt-impl</artifactId>
  20. <version>0.11.5</version>
  21. <scope>runtime</scope>
  22. </dependency>
  23. <dependency>
  24. <groupId>io.jsonwebtoken</groupId>
  25. <artifactId>jjwt-jackson</artifactId>
  26. <version>0.11.5</version>
  27. <scope>runtime</scope>
  28. </dependency>

2. 配置 Spring Security

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

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

创建一个配置类来扩展 WebSecurityConfigurerAdapter 并覆盖其方法来配置安全性。

  1. @Slf4j
  2. @Configuration
  3. @EnableWebSecurity //开启SpringSecurity的默认行为
  4. @RequiredArgsConstructor//bean注解
  5. // 新版不需要继承WebSecurityConfigurerAdapter
  6. public class WebSecurityConfig {
  7. // 这个类主要是获取库中的用户信息,交给security
  8. private final UserDetailServiceImpl userDetailsService;
  9. // 这个的类是认证失败处理(我在这里主要是把错误消息以json方式返回)
  10. private final JwtAuthenticationEntryPoint authenticationEntryPoint;
  11. // 鉴权失败的时候的处理类
  12. private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
  13. // 登录成功处理
  14. private final LoginSuccessHandler loginSuccessHandler;
  15. // 登录失败处理
  16. private final LoginFailureHandler loginFailureHandler;
  17. // 登出成功处理
  18. private final LoginLogoutSuccessHandler loginLogoutSuccessHandler;
  19. // token过滤器
  20. private final JwtTokenFilter jwtTokenFilter;
  21. @Bean
  22. public AuthenticationManager authenticationManager(
  23. AuthenticationConfiguration authenticationConfiguration
  24. ) throws Exception {
  25. return authenticationConfiguration.getAuthenticationManager();
  26. }
  27. // 加密方式
  28. @Bean
  29. public PasswordEncoder passwordEncoder() {
  30. return new BCryptPasswordEncoder();
  31. }
  32. /**
  33. * 核心配置
  34. */
  35. @Bean
  36. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  37. log.info("------------filterChain------------");
  38. http
  39. // 禁用basic明文验证
  40. .httpBasic(Customizer.withDefaults())
  41. // 基于 token ,不需要 csrf
  42. .csrf(AbstractHttpConfigurer::disable)
  43. // 禁用默认登录页
  44. .formLogin(fl ->
  45. fl.loginPage(PathMatcherUtil.FORM_LOGIN_URL)
  46. .loginProcessingUrl(PathMatcherUtil.TO_LOGIN_URL)
  47. .usernameParameter("username")
  48. .passwordParameter("password")
  49. .successHandler(loginSuccessHandler)
  50. .failureHandler(loginFailureHandler)
  51. .permitAll())
  52. // 禁用默认登出页
  53. .logout(lt -> lt.logoutSuccessHandler(loginLogoutSuccessHandler))
  54. // 基于 token , 不需要 session
  55. .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  56. // 设置 处理鉴权失败、认证失败
  57. .exceptionHandling(
  58. exceptions -> exceptions.authenticationEntryPoint(authenticationEntryPoint)
  59. .accessDeniedHandler(jwtAccessDeniedHandler)
  60. )
  61. // 下面开始设置权限
  62. .authorizeHttpRequests(authorizeHttpRequest -> authorizeHttpRequest
  63. // 允许所有 OPTIONS 请求
  64. .requestMatchers(PathMatcherUtil.AUTH_WHITE_LIST).permitAll()
  65. // 允许直接访问 授权登录接口
  66. // .requestMatchers(HttpMethod.POST, "/web/authenticate").permitAll()
  67. // 允许 SpringMVC 的默认错误地址匿名访问
  68. // .requestMatchers("/error").permitAll()
  69. // 其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailImpl对象中默认设置“ROLE_USER”
  70. //.requestMatchers("/**").hasAnyAuthority("ROLE_USER")
  71. // .requestMatchers("/heartBeat/**", "/main/**").permitAll()
  72. // 允许任意请求被已登录用户访问,不检查Authority
  73. .anyRequest().authenticated()
  74. )
  75. // 添加过滤器
  76. .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
  77. //可以加载fram嵌套页面
  78. http.headers( headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
  79. return http.build();
  80. }
  81. @Bean
  82. public UserDetailsService userDetailsService() {
  83. return userDetailsService::loadUserByUsername;
  84. }
  85. /**
  86. * 调用loadUserByUserName获取userDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
  87. *
  88. * @return
  89. */
  90. @Bean
  91. public AuthenticationProvider authenticationProvider() {
  92. DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
  93. authProvider.setUserDetailsService(userDetailsService);
  94. authProvider.setPasswordEncoder(passwordEncoder());
  95. return authProvider;
  96. }
  97. /**
  98. * 配置跨源访问(CORS)
  99. *
  100. * @return
  101. */
  102. @Bean
  103. CorsConfigurationSource corsConfigurationSource() {
  104. UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  105. source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
  106. return source;
  107. }
  108. }

2.2 自定义用户登录认证

从数据库中查询用户信息,再进行用户验证逻辑,实现 UserDetailsService 接口,并在 AuthenticationManagerBuilder 中配置。

  1. /**
  2. * 自定义登录接口服务类
  3. */
  4. @Slf4j
  5. @Component
  6. @RequiredArgsConstructor
  7. public class UserDetailServiceImpl implements UserDetailsService {
  8. // 注入管理员信息service
  9. private final ManagerService managerService;
  10. // 注入角色信息service
  11. private final RoleService roleService;
  12. @Override
  13. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  14. ManagerPo mng = managerService.getByUsername(username);
  15. if (mng == null) {
  16. log.info("用户名不存在!userName=" + username);
  17. throw new UsernameNotFoundException("用户名不存在" + username);
  18. }
  19. if (mng.getState() != 1) {
  20. log.info("用户已被冻结!userName=" + username);
  21. throw new LockedException("该用户已被冻结" + username);
  22. }
  23. // 角色集合
  24. Set<GrantedAuthority> authorities = new HashSet<>();
  25. // 查询用户角色
  26. List<RolePo> roleList = roleService.getByManager(mng.getId());
  27. for (RolePo role : roleList) {
  28. authorities.add(new SimpleGrantedAuthority(role.getRole()));
  29. }
  30. JwtMngBo jwtMng = new JwtMngBo(mng.getId(), mng.getUsername(), mng.getTrueName(), mng.getPassword(),
  31. mng.getGroupMark(), authorities);
  32. return jwtMng;
  33. }
  34. }
2.3 认证失败处理
用户未登录处理类 自定义身份验证失败的handler,包括跳转页面并统计拦截次数
  1. @Slf4j
  2. @Component
  3. public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
  4. //统计用户错误登陆日志service
  5. @Autowired
  6. private MngLoginlogService mngLoginlogService;
  7. @Override
  8. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
  9. log.error("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), authException);
  10. String requestUri = request.getRequestURI();
  11. String browser = request.getHeader("user-agent");
  12. // 请求login 或者 又admin字段都判断未登录 需要重新登录
  13. if (isAdminLogin(requestUri)) {
  14. JwtTokenGetUtil.deleteCookieToken(response);
  15. String token = JwtTokenGetUtil.getToken(request);
  16. log.info("认证失败后,后台不是登陆地址,则进入后台登录界面。requestURI={},token={}", requestUri, token);
  17. mngLoginlogService.recordLog(request, MngLoginLogDic.login_no, "", "browser=" + browser + ",token=" + token);
  18. response.sendRedirect("/xxx/login.html");
  19. return;
  20. }
  21. log.info("认证失败!--requestURI={},ip={},userAgent:{}", requestUri, IpUtil.getIp(request), browser);
  22. mngLoginlogService.recordLog(request, MngLoginLogDic.login_no, "", browser);
  23. LoginResultUtil.reJson(response, MsgCode.SYSTEM_TOKEN_AUTH_ERROR);
  24. }
  25. /**
  26. * 在用户身份认证失败后,判断为正确的后台地址,又不是登录页面,则返回true 防止用户身份过期后,无法跳转到登录页面处理
  27. *
  28. * @param url
  29. * @return
  30. */
  31. private boolean isAdminLogin(String url) {
  32. if (url.contains("admin") || url.contains("ADMIN")) {
  33. if (!(url.contains("login") || url.contains("LOGIN"))) {
  34. return true;
  35. }
  36. }
  37. return false;
  38. }
  39. }
2.4 鉴权失败的时候的处理(暂无权限处理类)
  1. @Slf4j
  2. @Component
  3. public class JwtAccessDeniedHandler implements AccessDeniedHandler {
  4. @Autowired
  5. private MngLoginlogService mngLoginlogService;
  6. @Override
  7. public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
  8. mngLoginlogService.recordLog(request, MngLoginLogDic.perm_no, "", accessDeniedException.getMessage());
  9. LoginResultUtil.reJson(response, 70001, MsgCode.PERMISSION_NO_ACCESS);
  10. log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
  11. "", accessDeniedException);
  12. }
  13. }
2.5 鉴权成功后(token生成管理)
  1. @Slf4j
  2. @Component
  3. public class LoginSuccessHandler implements AuthenticationSuccessHandler {
  4. @Autowired
  5. private ManagerService managerService;
  6. @Override
  7. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
  8. Authentication authentication) throws IOException, ServletException {
  9. // 组装JWT
  10. JwtMngBo jwtMng = (JwtMngBo) authentication.getPrincipal();
  11. String token = MngJwtTokenUtil.generateToken(jwtMng);
  12. //存放token到cookie中,最好时直接返回json
  13. JwtTokenGetUtil.setCookieToken(response, token);
  14. //返回json
  15. //response.sendRedirect(request.getContextPath() + PathMatcherUtil.INDEX_URL);
  16. }
  17. }

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

用户安全模型(专供安全管理使用):
  1. @Data
  2. public class JwtMngBo implements UserDetails {
  3. private Integer id;
  4. private String password;
  5. private String username;
  6. private String trueName;
  7. /**
  8. * @Description 得到用户的角色列表
  9. */
  10. private Collection<? extends GrantedAuthority> authorities;
  11. /**
  12. * 判断用户是否为过期
  13. */
  14. private boolean accountNonExpired = true;
  15. /**
  16. * 判断用户是否为锁定
  17. */
  18. private boolean accountNonLocked = true;
  19. /**
  20. * 判断密码是否未过期
  21. */
  22. private boolean credentialsNonExpired = true;
  23. /**
  24. * 判断账户是否激活
  25. */
  26. private boolean enabled = true;
  27. public JwtMngBo(Integer id, String username, String trueName, String password,
  28. Collection<? extends GrantedAuthority> authorities) {
  29. this.id = id;
  30. this.username = username;
  31. this.trueName = trueName;
  32. this.password = password;
  33. this.authorities = authorities;
  34. }
  35. }
2.6 登陆失败处理
  1. @Slf4j
  2. @Component
  3. public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
  4. public LoginFailureHandler() {
  5. this.setDefaultFailureUrl("/xxx/login.html");
  6. }
  7. @Override
  8. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
  9. AuthenticationException exception) {
  10. if (exception instanceof UsernameNotFoundException) {
  11. log.info("【用户名不存在】" + exception.getMessage());
  12. //用户名不存在
  13. redirectLogin(response, request.getContextPath() + "/xxx/login.html?error=userNotExis");
  14. return;
  15. }
  16. if (exception instanceof LockedException) {
  17. log.info("【用户被冻结】" + exception.getMessage());
  18. //用户被冻结
  19. redirectLogin(response, request.getContextPath() + "/xxx/login.html?error=userFrozen");
  20. return;
  21. }
  22. if (exception instanceof BadCredentialsException) {
  23. log.info("【用户名密码不正确】" + exception.getMessage());
  24. //用户名密码不正确
  25. throw new BusinessException(MsgCode.USER_LOGIN_ERROR);
  26. }
  27. log.info("-----------登录验证失败,其他登录失败错误");
  28. //其他登录失败错误
  29. redirectLogin(response, request.getContextPath() + "/xxx/login.html?error=loginFailed");
  30. }
  31. private void redirectLogin(HttpServletResponse res,String url){
  32. try {
  33. res.sendRedirect(url);
  34. } catch (IOException e1) {
  35. logger.error(e1.getMessage());
  36. // e1.printStackTrace();
  37. }
  38. }
  39. }
2.7 登出处理
  1. @Component
  2. public class LoginLogoutSuccessHandler implements LogoutSuccessHandler {
  3. @Override
  4. public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
  5. SecurityContextHolder.clearContext();
  6. //清楚token储存
  7. JwtTokenGetUtil.deleteCookieToken(response);
  8. //跳转登陆页面
  9. LoginResultUtil.reLoginHtml(response, "登出时");
  10. }
  11. }
 2.8 Token过滤器

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

  1. @Slf4j
  2. @Component
  3. public class JwtTokenFilter extends OncePerRequestFilter {
  4. @Autowired
  5. private LoginFilterMng loginFilterMng;
  6. @Override
  7. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
  8. String requestUri = request.getRequestURI();
  9. // Url白名单,正常放过
  10. if (PathMatcherUtil.passWhiteUrl(requestUri)) {
  11. chain.doFilter(request, response);
  12. return;
  13. }
  14. // Url黑名单,直接拦截返回
  15. if (PathMatcherUtil.passBlackUrl(requestUri)) {
  16. log.info("进入黑名单!requestUri={},ip={},userAgent={}", requestUri, IpUtil.getIp(request),
  17. request.getHeader("user-agent"));
  18. response.sendRedirect("/xxx/404");
  19. return;
  20. }
  21. //验证码拦截验证
  22. if (PathMatcherUtil.TO_LOGIN_URL.equals(requestUri)) {
  23. // 验证前端传来的验证码
  24. if (loginFilterMng.verifyCode(request, response)) {
  25. log.info("验证码拦截!");
  26. chain.doFilter(request, response);
  27. return ;
  28. }else{
  29. chain.doFilter(request, response);
  30. return ;
  31. }
  32. }
  33. // 验证token是否有效
  34. String token = JwtTokenGetUtil.getToken(request);
  35. if (StringUtils.isEmpty(token)) {
  36. log.info("[后台token]为空!requestUri={},ip={},userAgent={}", requestUri, IpUtil.getIp(request),
  37. request.getHeader("user-agent"));
  38. response.sendRedirect("/xxx/login.html");
  39. // chain.doFilter(request, response);
  40. return;
  41. }
  42. UsernamePasswordAuthenticationToken authentication = getAuthenticationToken(token, requestUri);
  43. if (authentication == null) {
  44. chain.doFilter(request, response);
  45. return;
  46. }
  47. SecurityContextHolder.getContext().setAuthentication(authentication);
  48. if (requestUri.contains("/xxx/login.html")) {
  49. response.sendRedirect("/xxx/index.html");
  50. return;
  51. }
  52. chain.doFilter(request, response);
  53. }
  54. private UsernamePasswordAuthenticationToken getAuthenticationToken(String token, String requestUri) {
  55. try {
  56. Claims claims = MngJwtTokenUtil.getClaimsFromToken(token);
  57. if (claims==null){
  58. log.error("token中过期,claims为空! requestUri={}", requestUri);
  59. return null;
  60. }
  61. String username = claims.getSubject();
  62. String userId = claims.getId();
  63. if (StringUtils.isEmpty(username) || StringUtils.isEmpty(userId)) {
  64. log.error("token中username或userId为空! requestUri={}", requestUri);
  65. return null;
  66. }
  67. // 获取角色
  68. List<GrantedAuthority> authorities = new ArrayList<>();
  69. String authority = claims.get("authorities").toString();
  70. if (!StringUtils.isEmpty(authority)) {
  71. @SuppressWarnings("unchecked")
  72. List<Map<String, String>> authorityMap = JSONObject.parseObject(authority, List.class);
  73. for (Map<String, String> role : authorityMap) {
  74. if (!role.isEmpty()) {
  75. authorities.add(new SimpleGrantedAuthority(role.get("authority")));
  76. }
  77. }
  78. }
  79. String trueName = claims.get("trueName").toString();
  80. JwtMngBo jwtMng = new JwtMngBo(Integer.parseInt(userId), username, trueName, "", authorities);
  81. return new UsernamePasswordAuthenticationToken(jwtMng, userId, authorities);
  82. } catch (ExpiredJwtException e) {
  83. logger.error("Token已过期: {} " + e);
  84. /* throw new TokenException("Token已过期"); */
  85. } catch (UnsupportedJwtException e) {
  86. logger.error("requestURI=" + requestUri + ",token=" + token + ",Token格式错误: {} " + e);
  87. /* throw new TokenException("Token格式错误"); */
  88. } catch (MalformedJwtException e) {
  89. logger.error("requestURI=" + requestUri + ",token=" + token + ",Token没有被正确构造: {} " + e);
  90. /* throw new TokenException("Token没有被正确构造"); */
  91. } catch (SignatureException e) {
  92. logger.error("requestURI=" + requestUri + ",token=" + token + ",签名失败: {} " + e);
  93. /* throw new TokenException("签名失败"); */
  94. } catch (IllegalArgumentException e) {
  95. logger.error("requestURI=" + requestUri + ",token=" + token + ",非法参数异常: {} " + e);
  96. /* throw new TokenException("非法参数异常"); */
  97. }
  98. return null;
  99. }
  100. }


3. 控制器

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

  1. @Controller
  2. @RequestMapping("/xxx")
  3. public class AdminLoginAction {
  4. //登录入口
  5. @RequestMapping(value = "/login.html")
  6. public String login(HttpServletRequest request, HttpServletResponse response) {
  7. return "/xxx/loginForm";
  8. }
  9. /**
  10. * 生成验证码
  11. */
  12. @RequestMapping(value = "/getVerify")
  13. public void getVerify(HttpServletRequest request, HttpServletResponse response) {
  14. try {
  15. response.setContentType("image/jpeg");// 设置相应类型,告诉浏览器输出的内容为图片
  16. response.setHeader("Pragma", "No-cache");// 设置响应头信息,告诉浏览器不要缓存此内容
  17. response.setHeader("Cache-Control", "no-cache");
  18. response.setDateHeader("Expire", 0);
  19. RandomValidateCodeUtil randomValidateCode = new RandomValidateCodeUtil();
  20. randomValidateCode.getRandcode(request, response);// 输出验证码图片方法
  21. } catch (Exception e) {
  22. log.error("获取验证码失败>>>> ", e);
  23. }
  24. }
  25. @GetMapping(value = "/404.html")
  26. public String error404(HttpServletRequest request, HttpServletResponse response) {
  27. return "/error/404";
  28. }
  29. }


4. 创建登录页面

创建一个简单的HTML页面loginForm作为登录页面,通常放在 src/main/resources/templates 目录下。

  1. <!doctype html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录</title>
  6. <meta name="renderer" content="webkit|ie-comp|ie-stand">
  7. <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  8. <meta name="viewport"
  9. content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi" />
  10. <meta http-equiv="Cache-Control" content="no-siteapp" />
  11. </head>
  12. <body>
  13. <div class="login layui-anim layui-anim-up">
  14. <div class="message">登录</div>
  15. <div id="darkbannerwrap"></div>
  16. <form method="post" class="layui-form" id="login-form" action="">
  17. <input name="username" placeholder="用户名" type="text" lay-verify="required" class="layui-input">
  18. <input name="password" lay-verify="required" placeholder="密码" type="password" class="layui-input">
  19. <input class="form-inline" name="verifyCode" autocomplete="off"
  20. style="width: 50%" type="text" id="verify_input" placeholder="请输入验证码" maxlength="4">
  21. <a href="javascript:void(0);" rel="external nofollow" title="点击更换验证码"> <img id="imgVerify" src="" alt="更换验证码"
  22. style="vertical-align: bottom; float: right" height="46" width="40%" onclick="getVerify(this);">
  23. </a>
  24. <hr class="hr15">
  25. <input value="登录" style="width: 100%;" type="button" onclick="login(this);">
  26. <span id="info" style="color: red"></span>
  27. </form>
  28. </div>
  29. </body>
  30. </html>


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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/921518
推荐阅读
相关标签
  

闽ICP备14008679号