当前位置: 首页 > news >正文

若依前后端分离版学习笔记(五)——Spring Boot简介与Spring Security

一 Spring Boot 简介

1、介绍

Spring Boot是一款开箱即用框架,提供各种默认配置来简化项目配置。让我们的Spring应用变的更轻量化、更快的入门。 在主程序执行main函数就可以运行。你也可以打包你的应用为jar并通过使用java -jar来运行你的Web应用。它遵循"约定优先于配置"的原则, 使用SpringBoot只需很少的配置,大部分的时候直接使用默认的配置即可。同时可以与Spring Cloud的微服务无缝结合。

Spring Boot2.x版本环境要求必须是jdk8或以上版本,服务器Tomcat8或以上版本

2、优点

  • 使编码变得简单: 推荐使用注解
  • 使配置变得简单: 自动配置、快速集成新技术能力 没有冗余代码生成和XML配置的要求
  • 使部署变得简单: 内嵌Tomcat、Jetty、Undertow等web容器,无需以war包形式部署
  • 使监控变得简单: 提供运行时的应用监控
  • 使集成变得简单: 对主流开发框架的无配置集成
  • 使开发变得简单: 极大地提高了开发快速构建项目、部署效率

二 Spring Security安全控制

1、介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架

2、功能

Authentication 认证,就是用户登录
Authorization 授权,判断用户拥有什么权限,可以访问什么资源
安全防护,跨站脚本攻击,session攻击等
非常容易结合Spring进行使用

3、Spring Security与Shiro的区别

  • 相同点
    1.认证功能
    2.授权功能
    3.加密功能
    4.会话管理
    5.缓存支持
    6.rememberMe功能
  • 不同点
    优点:
    1.Spring Security基于Spring开发,项目如果使用Spring作为基础,配合Spring Security做权限更加方便。而Shiro需要和Spring进行整合开发
    2.Spring Security功能比Shiro更加丰富,例如安全防护方面
    3.Spring Security社区资源相对比Shiro更加丰富
    缺点:
    1.Shiro的配置和使用比较简单,Spring Security上手复杂些
    2.Shiro依赖性低,不需要依赖任何框架和容器,可以独立运行。Spring Security依赖Spring容器

上述所有内容来自ruoyi官方文档

4、Spring Security配置介绍

配置类为ruoyi-framework模块中com.ruoyi.framework.config下的SecurityConfig类

package com.ruoyi.framework.config;import org.springframework.beans.factory.annotation.Autowired;
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.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import com.ruoyi.framework.config.properties.PermitAllUrlProperties;
import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;/*** spring security配置* * @author ruoyi*/
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration
public class SecurityConfig
{/*** 自定义用户认证逻辑*/@Autowiredprivate UserDetailsService userDetailsService;/*** 认证失败处理类*/@Autowiredprivate AuthenticationEntryPointImpl unauthorizedHandler;/*** 退出处理类*/@Autowiredprivate LogoutSuccessHandlerImpl logoutSuccessHandler;/*** token认证过滤器*/@Autowiredprivate JwtAuthenticationTokenFilter authenticationTokenFilter;/*** 跨域过滤器*/@Autowiredprivate CorsFilter corsFilter;/*** 允许匿名访问的地址*/@Autowiredprivate PermitAllUrlProperties permitAllUrl;/*** 身份验证实现*/@Beanpublic AuthenticationManager authenticationManager(){DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setUserDetailsService(userDetailsService);daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());return new ProviderManager(daoAuthenticationProvider);}/*** anyRequest          |   匹配所有请求路径* access              |   SpringEl表达式结果为true时可以访问* anonymous           |   匿名可以访问* denyAll             |   用户不能访问* fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)* hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问* hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问* hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问* hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问* hasRole             |   如果有参数,参数表示角色,则其角色可以访问* permitAll           |   用户可以任意访问* rememberMe          |   允许通过remember-me登录的用户访问* authenticated       |   用户登录后可访问*/@Beanprotected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{return httpSecurity// CSRF禁用,因为不使用session.csrf(csrf -> csrf.disable())// 禁用HTTP响应标头.headers((headersCustomizer) -> {headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());})// 认证失败处理类.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))// 基于token,所以不需要session.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 注解标记允许匿名访问的url.authorizeHttpRequests((requests) -> {permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());// 对于登录login 注册register 验证码captchaImage 允许匿名访问requests.antMatchers("/login", "/register", "/captchaImage").permitAll()// 静态资源,可匿名访问.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll().antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();})// 添加Logout filter.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))// 添加JWT filter.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)// 添加CORS filter.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class).addFilterBefore(corsFilter, LogoutFilter.class).build();}/*** 强散列哈希加密实现*/@Beanpublic BCryptPasswordEncoder bCryptPasswordEncoder(){return new BCryptPasswordEncoder();}
}

核心配置都在这里

开启安全注解@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true),默认是禁用的。prePostEnabled方法级的控制访问权限

4.1 强散列哈希加密实现

方法bCryptPasswordEncoder()返回强散列哈希加密对象

4.2 身份认证实现

新建一个Spring Security内置的对象DaoAuthenticationProvider,set自定义的用户验证处理UserDetailsService以及密码加密方式
我们再来看一下用户验证处理UserDetailsService的实现

package com.ruoyi.framework.web.service;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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 com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;/*** 用户验证处理** @author ruoyi*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);@Autowiredprivate ISysUserService userService;@Autowiredprivate SysPasswordService passwordService;@Autowiredprivate SysPermissionService permissionService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{// 从数据库用户表中查询用户信息SysUser user = userService.selectUserByUserName(username);/** 根据查询结果抛出不同异常 */if (StringUtils.isNull(user)){log.info("登录用户:{} 不存在.", username);throw new ServiceException(MessageUtils.message("user.not.exists"));}// 根据当前代码,不会有该情况,从userService.selectUserByUserName(username)查询里已经将删除状态的用户过滤掉了else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())){log.info("登录用户:{} 已被删除.", username);throw new ServiceException(MessageUtils.message("user.password.delete"));}else if (UserStatus.DISABLE.getCode().equals(user.getStatus())){log.info("登录用户:{} 已被停用.", username);throw new ServiceException(MessageUtils.message("user.blocked"));}// 如果用户存在,则判断用户密码是否正确passwordService.validate(user);// 密码正确返回新建LoginUser()return createLoginUser(user);}public UserDetails createLoginUser(SysUser user){return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));}
}

4.3 Spring Security相关配置

返回一个配置好的HttpSecurity,其中配置了哪些内容,代码上都有注释。我们来看一下自定义的认证失败处理类AuthenticationEntryPointImpl

package com.ruoyi.framework.security.handle;import java.io.IOException;
import java.io.Serializable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;/*** 认证失败处理类 返回未授权* * @author ruoyi*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{private static final long serialVersionUID = -8970718410437077606L;@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)throws IOException{// 返回错误消息和状态码int code = HttpStatus.UNAUTHORIZED;String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));}
}

自定义退出处理类LogoutSuccessHandlerImpl

package com.ruoyi.framework.security.handle;import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.web.service.TokenService;/*** 自定义退出处理类 返回成功* * @author ruoyi*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{@Autowiredprivate TokenService tokenService;/*** 退出处理* * @return*/@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException, ServletException{// 查询登录用户信息LoginUser loginUser = tokenService.getLoginUser(request);if (StringUtils.isNotNull(loginUser)){String userName = loginUser.getUsername();// 删除用户缓存记录tokenService.delLoginUser(loginUser.getToken());// 记录用户退出日志AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));}// 返回退出登录成功信息ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success"))));}
}

JWT过滤,自定义的token认证过滤器 JwtAuthenticationTokenFilter

package com.ruoyi.framework.security.filter;import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.TokenService;/*** token过滤器 验证token有效性* * @author ruoyi*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{@Autowiredprivate TokenService tokenService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException{// 从请求中获取用户信息LoginUser loginUser = tokenService.getLoginUser(request);// 如果用户信息不为null且未进行认证(安全认证为null)if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())){// 验证token是否有效并重置token有效时间tokenService.verifyToken(loginUser);// 进行安全认证并set到Spring Security上下文中UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);}// 继续过滤器链chain.doFilter(request, response);}
}

关于CSRF、JWT以及CORS的一些概念

  1. 关于CSRF:
  • CSRF(跨站请求伪造)是一种攻击,攻击者诱导用户在当前已登录的Web应用上执行非本意的操作。这种攻击通常依赖于浏览器会自动携带与目标站点相关的Cookie(包括身份验证的Cookie)的特性。
  • 在传统的基于Session的认证中,服务器会为每个用户创建一个Session,并返回一个Session ID(通常通过Cookie存储)。当用户发起请求时,浏览器会自动携带这个Cookie,服务器通过Session ID来识别用户。因此,如果用户已经登录,攻击者可以构造一个恶意请求,诱使用户点击,浏览器会自动携带Cookie,从而以用户身份执行操作。
  • 为了防止CSRF攻击,常见的做法是使用CSRF Token。服务器生成一个Token,通常放在表单的隐藏字段中(或者作为请求头),然后服务器验证请求中的Token是否与Session中存储的Token一致。因为攻击者无法获取到Token(由于浏览器的同源策略),所以无法构造出合法的请求。
  1. 关于JWT:
  • JWT(JSON Web Token)是一种无状态的认证机制。服务器在用户登录后生成一个Token(包含用户信息、过期时间等),并返回给客户端。客户端在后续请求中需要携带这个Token(通常放在Authorization头中)。服务器验证Token的签名和有效性,从而识别用户身份。
  • 由于JWT不需要服务器保存Session,因此被称为无状态的。每次请求都携带Token,服务器通过验证Token来确认用户身份,而不是通过Cookie中的Session ID。因此,在JWT模式下,通常不需要担心CSRF攻击,因为:
    • 浏览器不会自动在跨域请求中携带Authorization头(除非使用Credentialed请求,但即使这样,攻击者也无法构造出合法的Authorization头,因为他们不知道Token)。
    • 但是,如果Token是存储在Cookie中(而不是使用LocalStorage然后通过JS手动添加到请求头),那么仍然可能受到CSRF攻击。因为攻击者可以诱导用户发起一个请求,浏览器会自动携带Cookie,从而将Token发送到服务器。所以,如果使用Cookie存储JWT,仍然需要CSRF保护。
  1. 关于CORS:
  • CORS(跨域资源共享)是一种机制,它允许在浏览器中运行的脚本从不同的源(域名、协议、端口)访问资源。浏览器出于安全考虑,会限制跨域请求(如同源策略)。CORS通过设置一些HTTP头(如Access-Control-Allow-Origin)来告诉浏览器该资源允许被哪些源访问。
  • 在前后端分离的架构中,前端和后端通常部署在不同的源(比如不同的端口或域名),因此需要配置CORS以允许跨域请求。否则,浏览器会阻止跨域请求的响应数据。
  • 在Spring Security中,可以通过CorsFilter或者配置http.cors()来启用CORS支持。这样,服务器会在响应中添加必要的CORS头。
    我的理解:
  • 在传统的基于Session认证的web项目中,用户的信息会存在浏览器cookie里,CSRF是一种通过诱导用户点击使浏览器发送请求从而从cookie中获取用户的信息。
  • JWT是每次请求都需要在发送请求的请求头中携带表明身份(如用户名,有效时间等)的请求头来进行身份验证,通常是Authorization: Bearer 。
  • CORS跨域过滤器是为了避免通过JWT方式验证身份时遭遇跨域限制,通过HTTP响应头声明允许的跨域规则,浏览器先发OPTIONS请求检查CORS规则,通过后才发真实请求。

5、Spring Security 密码加密

在Spring Security 配置类SecurityConfig中,有一个bCryptPasswordEncoder()方法。

/*** 强散列哈希加密实现*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{return new BCryptPasswordEncoder();
}

BCryptPasswordEncoder实现PasswordEncoder接口

package org.springframework.security.crypto.password;public interface PasswordEncoder {/*** 将原始密码加密为密文*/String encode(CharSequence rawPassword);/*** 验证原始密码与加密密文是否匹配*/boolean matches(CharSequence rawPassword, String encodedPassword);/*** 判断已加密密码是否需要重新加密(默认不需要)*/default boolean upgradeEncoding(String encodedPassword) {return false;}
}

身份认证流程
在SecurityConfig中配置了AuthenticationManager bean,它使用了DaoAuthenticationProvider,并设置了UserDetailsService(即UserDetailsServiceImpl)和密码编码器bCryptPasswordEncoder()。

  • 在 SysLoginService 的login()方法中创建UsernamePasswordAuthenticationToken对象authenticationToken,把用户名,密码传入其中并set进AuthenticationContextHolder上下文,
  • 当SysLoginService的登录方法调用authenticationManager.authenticate(authenticationToken)时,实际调用的是ProviderManager(Spring Security默认的AuthenticationManager实现)
  • ProviderManager会遍历其配置的AuthenticationProvider列表(这里是DaoAuthenticationProvider)
  • DaoAuthenticationProvider会调用我们配置的UserDetailsService(即UserDetailsServiceImpl)的loadUserByUsername方法来加载用户信息
  • 加载到用户信息后,DaoAuthenticationProvider会使用配置的密码编码器(BCryptPasswordEncoder)来验证密码是否匹配
    将相关代码贴在这里
/**
* SysLoginService的login()方法中身份验证部分
*/
// 用户验证
Authentication authentication = null;
try
{UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);AuthenticationContextHolder.setContext(authenticationToken);// 该方法会去调用UserDetailsServiceImpl.loadUserByUsernameauthentication = authenticationManager.authenticate(authenticationToken);
}
catch (Exception e)
{if (e instanceof BadCredentialsException){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}else{AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}
}
finally
{AuthenticationContextHolder.clearContext();
}/**
* SecurityConfig中的身份验证配置
*/
@Bean
public AuthenticationManager authenticationManager()
{DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setUserDetailsService(userDetailsService);daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());return new ProviderManager(daoAuthenticationProvider);
}

6、Spring Security 退出配置

在SecurityConfig中有一行退出登录的配置,Spring Security提供了一个logout方法,这里设置一个拦截的url以及退出成功后的处理。

logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))

我们来看一下这个logoutSuccessHandler

package com.ruoyi.framework.security.handle;import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.web.service.TokenService;/*** 自定义退出处理类 返回成功* * @author ruoyi*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{@Autowiredprivate TokenService tokenService;/*** 退出处理* * @return*/@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException, ServletException{// 从request拿到用户信息LoginUser loginUser = tokenService.getLoginUser(request);if (StringUtils.isNotNull(loginUser)){String userName = loginUser.getUsername();// 删除用户缓存记录tokenService.delLoginUser(loginUser.getToken());// 记录用户退出日志AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));}ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success"))));}
}

LogoutSuccessHandlerImpl实现了LogoutSuccessHandler,LogoutSuccessHandler是Spring Security默认启动的,这里重写了onLogoutSuccess()方法。
前端api下的login.js中的退出方法即访问的/logout url进行退出操作

// 退出方法
export function logout() {return request({url: '/logout',method: 'post'})
}

7、Spring Security 登录配置

在SecurityConfig已经定义了身份认证实现,在4.3节我们已经查看了身份认证实现authenticationManager()方法以及自定义身份验证处理UserDetailsService。现在我们从登录接口来查看其登录配置。
首先查看SysLoginController中的login()方法

/*** 登录方法* * @param loginBody 登录信息* @return 结果*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{AjaxResult ajax = AjaxResult.success();// 生成令牌String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),loginBody.getUuid());ajax.put(Constants.TOKEN, token);return ajax;
}

这里调用自定义的loginService.login()登录逻辑,我们来查看其自定义的登录服务

package com.ruoyi.framework.web.service;import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.user.BlackListException;
import com.ruoyi.common.exception.user.CaptchaException;
import com.ruoyi.common.exception.user.CaptchaExpireException;
import com.ruoyi.common.exception.user.UserNotExistsException;
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.context.AuthenticationContextHolder;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserService;/*** 登录校验方法* * @author ruoyi*/
@Component
public class SysLoginService
{@Autowiredprivate TokenService tokenService;@Resourceprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Autowiredprivate ISysUserService userService;@Autowiredprivate ISysConfigService configService;/*** 登录验证* * @param username 用户名* @param password 密码* @param code 验证码* @param uuid 唯一标识* @return 结果*/public String login(String username, String password, String code, String uuid){// 验证码校验validateCaptcha(username, code, uuid);// 登录前置校验loginPreCheck(username, password);// 用户验证Authentication authentication = null;try{// 用户身份验证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);AuthenticationContextHolder.setContext(authenticationToken);// 该方法会去调用UserDetailsServiceImpl.loadUserByUsernameauthentication = authenticationManager.authenticate(authenticationToken);}catch (Exception e){// 如果是密码错误,抛出UserPasswordNotMatchException()异常,其它错误抛出具体异常信息if (e instanceof BadCredentialsException){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}else{AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}}// 清理AuthenticationContextHolder上下文finally{AuthenticationContextHolder.clearContext();}// 登录成功后的操作,更新ip,登录时间,记录登录日志AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));LoginUser loginUser = (LoginUser) authentication.getPrincipal();// 记录登录日志recordLoginInfo(loginUser.getUserId());// 生成tokenreturn tokenService.createToken(loginUser);}/*** 校验验证码* * @param username 用户名* @param code 验证码* @param uuid 唯一标识* @return 结果*/public void validateCaptcha(String username, String code, String uuid){// 检查系统是否开启了验证码功能(从redis中查询配置)boolean captchaEnabled = configService.selectCaptchaEnabled();if (captchaEnabled){// 从redis中获取对应uuid的验证码String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");String captcha = redisCache.getCacheObject(verifyKey);// redis中对应验证码为空,则抛出验证码过期异常if (captcha == null){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));throw new CaptchaExpireException();}// 从redis中删除过期验证码的keyredisCache.deleteObject(verifyKey);// 用户输入验证码与redis中对应验证码不一致,抛出对应异常if (!code.equalsIgnoreCase(captcha)){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));throw new CaptchaException();}}}/*** 登录前置校验* @param username 用户名* @param password 用户密码*/public void loginPreCheck(String username, String password){// 用户名或密码为空 错误if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));throw new UserNotExistsException();}// 密码如果不在指定范围内 错误if (password.length() < UserConstants.PASSWORD_MIN_LENGTH|| password.length() > UserConstants.PASSWORD_MAX_LENGTH){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}// 用户名不在指定范围内 错误if (username.length() < UserConstants.USERNAME_MIN_LENGTH|| username.length() > UserConstants.USERNAME_MAX_LENGTH){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}// IP黑名单校验String blackStr = configService.selectConfigByKey("sys.login.blackIPList");if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));throw new BlackListException();}}/*** 记录登录信息** @param userId 用户ID*/public void recordLoginInfo(Long userId){SysUser sysUser = new SysUser();sysUser.setUserId(userId);sysUser.setLoginIp(IpUtils.getIpAddr());sysUser.setLoginDate(DateUtils.getNowDate());userService.updateUserProfile(sysUser);}
}

我们再看一下这里的AsyncManager,在这里是进行异步执行,这里的核心逻辑是进行用户登录验证,为了快速响应这里异步执行日志记录等辅助功能操作。既保证了主流程的高效执行,又完成了必要的辅助操作。

package com.ruoyi.framework.manager;import java.util.TimerTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import com.ruoyi.common.utils.Threads;
import com.ruoyi.common.utils.spring.SpringUtils;/*** 异步任务管理器* * @author ruoyi*/
public class AsyncManager
{/*** 操作延迟10毫秒*/private final int OPERATE_DELAY_TIME = 10;/*** 异步操作任务调度线程池*/private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");/*** 单例模式*/private AsyncManager(){}private static AsyncManager me = new AsyncManager();public static AsyncManager me(){return me;}/*** 执行任务* * @param task 任务*/public void execute(TimerTask task){executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);}/*** 停止任务线程池*/public void shutdown(){Threads.shutdownAndAwaitTermination(executor);}
}

单例模式: AsyncManager.me() 返回 AsyncManager 的单例实例,保证线程安全。
延迟执行: execute() 方法将任务提交到调度线程池,延迟10毫秒执行
线程池管理:使用共享的ScheduledExecutorService线程池来控制并发执行的线程数量,避免创建过多线程导致资源耗尽,统一管理和监控异步任务

8、权限配置

在我们上述提到的UserDetailsServiceImpl实现类中有一个createLoginUser方法,当用户验证通过后来创建一个LoginUser对象,其中permissionService.getMenuPermission(user)即为获取该用户的权限。

public UserDetails createLoginUser(SysUser user)
{return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}

我们来看SysPermissionService的代码

package com.ruoyi.framework.web.service;import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.system.service.ISysRoleService;/*** 用户权限处理* * @author ruoyi*/
@Component
public class SysPermissionService
{@Autowiredprivate ISysRoleService roleService;@Autowiredprivate ISysMenuService menuService;/*** 获取角色数据权限* * @param user 用户信息* @return 角色权限信息*/public Set<String> getRolePermission(SysUser user){Set<String> roles = new HashSet<String>();// 管理员拥有所有权限if (user.isAdmin()){roles.add("admin");}else{roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));}return roles;}/*** 获取菜单数据权限* * @param user 用户信息* @return 菜单权限信息*/public Set<String> getMenuPermission(SysUser user){Set<String> perms = new HashSet<String>();// 管理员拥有所有权限if (user.isAdmin()){perms.add("*:*:*");}else{// 获取用户的角色列表List<SysRole> roles = user.getRoles();if (!CollectionUtils.isEmpty(roles)){// 多角色设置permissions属性,以便数据权限匹配权限for (SysRole role : roles){// 如果角色状态正常且不是admin角色if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && !role.isAdmin()){// 通过角色id从数据库中查询权限Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());role.setPermissions(rolePerms);perms.addAll(rolePerms);}}}// 如果没有角色则通过用户id从数据库中查询权限else{perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));}}return perms;}
}

这里我们看一下这两个查询权限的sql及结果

<select id="selectMenuPermsByRoleId" parameterType="Long" resultType="String">select distinct m.permsfrom sys_menu mleft join sys_role_menu rm on m.menu_id = rm.menu_idwhere m.status = '0' and rm.role_id = #{roleId}
</select><select id="selectMenuPermsByUserId" parameterType="Long" resultType="String">select distinct m.permsfrom sys_menu mleft join sys_role_menu rm on m.menu_id = rm.menu_idleft join sys_user_role ur on rm.role_id = ur.role_idleft join sys_role r on r.role_id = ur.role_idwhere m.status = '0' and r.status = '0' and ur.user_id = #{userId}
</select>

在这里插入图片描述在这里插入图片描述

9、权限注解

在ruoyi-framework模块中com.ruoyi.framework.web.service下有一个PermissionService,来判断是否具有权限等。

package com.ruoyi.framework.web.service;import java.util.Set;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.context.PermissionContextHolder;/*** RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母* * @author ruoyi*/
@Service("ss")
public class PermissionService
{/*** 验证用户是否具备某权限,有权限返回true,没权限返回false* * @param permission 权限字符串* @return 用户是否具备某权限*/public boolean hasPermi(String permission){if (StringUtils.isEmpty(permission)){return false;}// 获取用户信息LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}// 将当前检查权限set进上下文PermissionContextHolder.setContext(permission);// 返回是否具备该权限return hasPermissions(loginUser.getPermissions(), permission);}/*** 验证用户是否不具备某权限,与 hasPermi逻辑相反** @param permission 权限字符串* @return 用户是否不具备某权限*/public boolean lacksPermi(String permission){return hasPermi(permission) != true;}/*** 验证用户是否具有以下任意一个权限** @param permissions 以 PERMISSION_DELIMETER 为分隔符的权限列表* @return 用户是否具有以下任意一个权限*/public boolean hasAnyPermi(String permissions){if (StringUtils.isEmpty(permissions)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}PermissionContextHolder.setContext(permissions);Set<String> authorities = loginUser.getPermissions();// 通过“,”分割permissions权限字符串,循环判断是否有该权限for (String permission : permissions.split(Constants.PERMISSION_DELIMETER)){if (permission != null && hasPermissions(authorities, permission)){return true;}}return false;}/*** 判断用户是否拥有某个角色* * @param role 角色字符串* @return 用户是否具备某角色*/public boolean hasRole(String role){if (StringUtils.isEmpty(role)){return false;}// 获取用户信息LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}// 遍历该用户的角色列表,判断是否包括该角色for (SysRole sysRole : loginUser.getUser().getRoles()){String roleKey = sysRole.getRoleKey();// 是否是管理员角色或包括该角色if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))){return true;}}return false;}/*** 验证用户是否不具备某角色,与 isRole逻辑相反。** @param role 角色名称* @return 用户是否不具备某角色*/public boolean lacksRole(String role){return hasRole(role) != true;}/*** 验证用户是否具有以下任意一个角色** @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表* @return 用户是否具有以下任意一个角色*/public boolean hasAnyRoles(String roles){if (StringUtils.isEmpty(roles)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}for (String role : roles.split(Constants.ROLE_DELIMETER)){if (hasRole(role)){return true;}}return false;}/*** 判断是否包含权限* * @param permissions 权限列表* @param permission 权限字符串* @return 用户是否具备某权限*/private boolean hasPermissions(Set<String> permissions, String permission){// 返回用户的权限列表中是否包含所有权限标识符或该权限标识符return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));}
}

其中PermissionService通过@Service注册到容器中并命名“ss”,可以通过 @ss 来引用这个权限服务,用于进行权限判断和控制。set 为不允许重复的字符串集合,查询效率更高。

下面我们看一下如何应用PermissionService服务,查看ruoyi-admin模块com.ruoyi.web.controller.system下的SysUserController的部分代码。

/*** 获取用户列表*/// 直接@ss.hasPermi进行调用,传入权限字符串,判断是否有获取用户列表权限
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{startPage();List<SysUser> list = userService.selectUserList(user);return getDataTable(list);
}@Log(title = "用户管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:user:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysUser user)
{List<SysUser> list = userService.selectUserList(user);ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);util.exportExcel(response, list, "用户数据");
}

@PreAuthorize 是 Spring Security 框架提供的一个方法级安全注解,用于在方法执行前进行权限检查。

  • 作用:在方法执行前验证用户是否具有指定权限,如果没有相应权限则拒绝访问
  • 位置:可以放在类或方法上,方法上的注解优先级更高

在这里的具体作用为

  • @ss 引用之前注册的名为"ss"的PermissionService Bean
  • .hasPermi(‘system:user:list’) 调用该服务的hasPermi方法检查用户是否具有"system:user:list"权限
  • 只有当权限检查通过时,才会执行被注解的方法list(SysUser user)
http://www.dtcms.com/a/315925.html

相关文章:

  • [驱动开发篇] Can通信进阶 --- CanFD 的三次采样
  • Chisel芯片开发入门系列 -- 18. CPU芯片开发和解释8(流水线架构的代码级理解)
  • 深度学习-卷积神经网络CNN-填充与步幅
  • AR文旅新纪元:从黄姚古镇到秦始皇陵,虚实共生的沉浸式体验革命
  • 华为云云产品的发展趋势:技术创新驱动数字化未来
  • 基于Docker的RabbitMQ运行参数设置
  • 基于华为开发者空间的Open WebUI数据分析与可视化实战
  • 光伏清洗机器人是什么?艾利特协作机器人如何重塑新能源运维效率
  • 【18】C实战篇——C语言 文件读写【fputc、fgetc、fputs、fgets】
  • FPGA学习笔记——简易的DDS信号发生器
  • 力扣106:从中序与后序遍历序列构造二叉树
  • Android 之 Kotlin 和 MVVM 架构的 Android 登录示例
  • 宝塔(免费版9.2.0)的docker拉取仓库失败的加速方法
  • 数据挖掘,到底是在挖掘什么?
  • 27-数据仓库与Apache Hive-2
  • 26-数据仓库与Apache Hive
  • LTR-308ALS-01 LiteOn光宝高精度光耦隔离器 5000Vrms持续隔离电压
  • Python-初学openCV——图像预处理(七)——亮度变换、形态学变换
  • 基于Flask的微博话题多标签情感分析系统设计
  • 李宏毅深度学习教程 第12-13章 对抗攻击 + 迁移学习transfer learning
  • NLP——BERT模型全面解析:从基础架构到优化演进
  • Idea无法识别Maven项目处理
  • 机器翻译的局限性:歧义、文化差异、专业术语翻译难题
  • Python-深度学习--1交叉熵损失函数
  • aab文件直接安装到手机
  • 西门子PLC基础指令4:置位指令 S、复位指令 R
  • 超越注意力机制
  • 【基础】第二篇 Java 类的定义、构造器、成员变量与方法详解
  • hyper-v实战系列:显卡虚拟化(GPU分区)--windows篇详解
  • SpringCloud学习-------Eureka详解