基于 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));} }