基于 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.loadUserByUsername authentication = 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()); // 生成token return tokenService.createToken(loginUser); }
2.2 SpringSecurity配置类
/** * spring security配置 * * @author Dftre */ @EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) @Configuration public class SecurityConfig { /** * 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 跨域过滤器 */ @Autowired private CorsFilter corsFilter; /** * 允许匿名访问的地址 */ @Autowired private PermitAllUrlProperties permitAllUrl; /** * @return * @throws Exception */ @Bean AuthenticationManager 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 | 用户登录后可访问 */ @Bean 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.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(); } /** * 强散列哈希加密实现 */ @Bean public 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 { @Autowired private TokenService tokenService; @Override protected 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)); } }