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

基于JWT+SpringSecurity整合一个单点认证授权机制

基于 JWT + Spring Security 的授权认证机制,在整体架构设计上体现了高度的安全性与灵活性。其在整合框架中的应用,充分展示了模块化、可扩展性和高效鉴权的设计理念,为开发者提供了一种值得借鉴的安全架构模式。

1.SpringSecurity概念理解

1.1 一般的Web应用需要进行认证和授权。

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。

授权:经过认证后判断当前用户是否有权限进行某些操作

1.2 RBAC权限模型的理解

可以理解为用户对应角色,角色又对应权限,在不考虑加入部门等参数的干预下,整个RBAC模型可以简化为5张表来描述:

1.3 SpringSecurity机制:

整个SpringSecurity机制可以理解为是一个过滤器链机制,前端发请求给后端,请求会经过整个过滤器链的认证和授权逻辑才能执行后续正常的系统接口逻辑。下方三个为核心过滤器

第一个负责处理在登录页面填写用户名和密码后登录请求

第二个负责处理过滤器链中抛出的授权和认证异常

第三个是负责权限校验的过滤器

认证点认证流程:

2.认证架构设计

2.1 登录的整体流程设计:

从login()逻辑开始执行

2.1 SpringSecurity的配置类

登录接口实现:

 /*** 登录方法* @param loginBody 登录信息* @return 结果*/
@Operation(summary = "登录方法")
@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;
}

/*** 登录验证* @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);String ip = IpUtils.getIpAddr();// 验证 IP 是否被封锁passwordService.validateIp(ip);// 用户验证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{passwordService.incrementIpFailCount(ip);AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}}finally{AuthenticationContextHolder.clearContext();}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);
}

2.2 SpringSecurity配置类

/*** spring security配置** @author Dftre*/
@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;/*** @return* @throws Exception*/@BeanAuthenticationManager authenticationManager() {DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(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 | 用户登录后可访问*/@BeanSecurityFilterChain 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.requestMatchers(url).permitAll());// 对于登录login 注册register 验证码captchaImage 允许匿名访问requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()// 静态资源,可匿名访问.requestMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js","/profile/**").permitAll().requestMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs","/druid/**", "/*/api-docs/**").permitAll().requestMatchers("/websocket/**").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();}
}

2.3 UserDetailsServiceImpl 重写 UserDetailsService接口的loadUserbyUsername方法

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser user = userService.selectUserByUserName(username);if (StringUtils.isNull(user)) {log.info("登录用户:{} 不存在.", username);throw new ServiceException("登录用户:" + username + " 不存在");} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {log.info("登录用户:{} 已被删除.", username);throw new ServiceException("对不起,您的账号:" + username + " 已被删除");} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {log.info("登录用户:{} 已被停用.", username);throw new ServiceException("对不起,您的账号:" + username + " 已停用");}passwordService.validate(user);return createLoginUser(user);
}public UserDetails createLoginUser(SysUser user) {return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}

2.4 保存LoginUser到Redis的逻辑在创建token的方法逻辑中实现

2.5 拦截器配置

/*** 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);if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())){tokenService.verifyToken(loginUser);UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);}chain.doFilter(request, response);}
}

3.授权架构设计

权限的验证最核心的是使用的Spring Security的提供的权限注解`@PreAuthorize `

当 @PreAuthorize 注解被应用于某个方法时,Spring Security 在该方法执行前会先对当前认证的用户进行权限检查。如果检查通过,方法调用得以继续;否则,框架会抛出相应的权限异常(如 AccessDeniedException),阻止方法执行。

  • @ss 引用了名为 "ss" 的 Spring Bean,即我们的 PermissionService
  • hasPermi('manage:order:list') 调用了 PermissionService 的 hasPermi 方法,检查用户是否拥有 "manage:order:list" 权限

授权相关校验操作实现:

@Service("ss")
public class PermissionService implements IPermissionService {/** 所有权限标识 */private static final String ALL_PERMISSION = "*:*:*";/** 管理员角色权限标识 */private static final String SUPER_ADMIN = "admin";private static final String ROLE_DELIMETER = ",";private static final String PERMISSION_DELIMETER = ",";/*** 验证用户是否具备某权限* * @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;}PermissionContextHolder.setContext(permission);return hasPermissions(loginUser.getPermissions(), permission);}/*** 验证用户是否具有以下任意一个权限** @param permissions 以 PERMISSION_NAMES_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();for (String permission : permissions.split(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 (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) {return true;}}return false;}/*** 验证用户是否具有以下任意一个角色** @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(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(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));}
}

相关文章:

  • 在UI界面内修改了对象名,在#include “ui_mainwindow.h“没更新
  • 探索Python融合地学:斗之气三段
  • 深度解构Vue3响应式内核:Proxy魔法与依赖追踪的极致艺术
  • 1.4 编译库:静态库、动态库
  • Java并发容器和原子类
  • caliper中的测试文件写法及其注意事项
  • 谷歌云代理商 | 游戏行业专属方案:谷歌云实时多人游戏服务器架构
  • 在Windows下利用LoongArch-toolchain交叉编译Qt
  • C++编程——关于比较器的使用
  • 五子棋网络对战游戏的设计与实现设计与实现【源码+文档】
  • 常见工具导出DDL语句
  • 图片切割工具:智能分割长图并控制文件大小
  • 三维GIS开发cesium智慧地铁教程(4)城市白模加载与样式控制
  • 数据任务调度解决离不开离线开发BatchWorks
  • 单周期cpu和多周期cpu、单周期数据通路和多周期数据通路与总线结构数据通路和专用数据通路的关系
  • AOP实现Restful接口操作日志入表方案
  • CC7利用链深度解析
  • 基于3D对象体积与直径特征的筛选
  • 【Linux】find 命令详解及使用示例:递归查找文件和目录
  • EtherNet/IP转DeviceNet协议网关详解
  • 孟村建设局网站/windows优化大师要会员
  • 失眠先生 wordpress/南宁百度seo排名价格
  • 个人定制网站怎么做/汕头网站建设方案优化
  • 免费做司考真题的网站/抖音广告
  • 棋牌游戏开发出售/单页网站怎么优化
  • 吕梁网站设计/seo网上培训课程