赞
踩
在Java企业级开发中,Spring Security 是一个广泛使用的安全框架,它提供了身份验证、授权以及防止攻击等安全性功能。SpringBoot3 与 SpringSecurity 的整合能够极大简化安全配置和管理的复杂性。
本片文章基于JDK17+springboot3.3.0
开发工具使用到hutool
以下将详细介绍如何在 SpringBoot3 项目中整合 SpringSecurity。
首先,你需要在 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>
接下来配置 Spring Security 以满足安全需求。通常涉及到设置用户验证、请求授权以及配置各种过滤器等。
创建一个配置类来扩展 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;
- }
- }
从数据库中查询用户信息,再进行用户验证逻辑,实现 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;
- }
- }
用户未登录处理类 自定义身份验证失败的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;
- }
- }
- @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);
- }
- }
- @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;
- }
- }
- @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();
- }
- }
-
- }
- @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, "登出时");
- }
- }
此过滤器很重要,主要负责资源放过,拦截,以及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;
- }
- }
编写控制器来处理登录和注销请求。
- @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";
- }
- }
创建一个简单的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调用访问登陆地址即可。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。