Sping?Security前后端分離兩種實戰(zhàn)方案
前言
本篇文章是基于Spring Security實現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的實戰(zhàn),主要包括以下四方面內(nèi)容:
Spring Seciruty簡單介紹;
通過Spring Seciruty實現(xiàn)的基于表單和Token認(rèn)證的兩種認(rèn)證方式;
自定義實現(xiàn)RBAC的權(quán)限控制;
跨域問題處理;
Spring Seciruty簡單介紹
Spring Security是基于Spring框架,提供了一套Web應(yīng)用安全性的完整解決方案。關(guān)于安全方面的兩個核心功能是認(rèn)證和授權(quán),Spring Security重要核心功能就是實現(xiàn)用戶認(rèn)證(Authentication)和用戶授權(quán)(Authorization)。
認(rèn)證(Authentication)
認(rèn)證是用來驗證某個用戶能否訪問該系統(tǒng)。用戶認(rèn)證一般要求用戶提供用戶名和密碼,系統(tǒng)通過校驗用戶名和密碼來完成認(rèn)證過程。
授權(quán)(Authorization)
授權(quán)發(fā)生在認(rèn)證之后,用來驗證某個用戶是否有權(quán)限執(zhí)行某個操作。在一個系統(tǒng)中,不同用戶所具有的權(quán)限是不同的。比如對一個文件來說,有的用戶只能進(jìn)行讀取,而有的用戶可以進(jìn)行修改。一般來說,系統(tǒng)會為不同的用戶分配不同的角色,而每個角色則對應(yīng)一系列的權(quán)限。
實現(xiàn)簡單介紹
Spring Security進(jìn)行認(rèn)證和鑒權(quán)的時候,采用的一系列的Filter來進(jìn)行攔截的。 下圖是Spring Security基于表單認(rèn)證授權(quán)的流程,
在Spring Security一個請求想要訪問到API就會從左到右經(jīng)過藍(lán)線框里的過濾器,其中綠色部分是負(fù)責(zé)認(rèn)證的過濾器,藍(lán)色部分是負(fù)責(zé)異常處理,橙色部分則是負(fù)責(zé)授權(quán)。進(jìn)過一系列攔截最終訪問到我們的API。
準(zhǔn)備階段
整個項目結(jié)構(gòu)如下,demo1部分是基于表單的認(rèn)證,demo2部分是基于Token的認(rèn)證,數(shù)據(jù)庫采用是Mysql,訪問數(shù)據(jù)庫使用的JPA,Spring Boot版本是2.7.8,Spring Security版本是比較新的5.7.6,這里需要注意的是Spring Security5.7以后版本和前面的版本有一些差異,未來該Demo的版本的問題一直會持續(xù)保持升級。 后續(xù)也會引用前端項目,前端后臺管理部分我個人感覺后端程序員也要進(jìn)行簡單的掌握一些,便于工作中遇到形形色色問題更好的去處理。
Maven
關(guān)于Maven部分細(xì)節(jié)這里不進(jìn)行展示了,采用的父子工程,主要簡單看下依賴的版本,
????<properties> ????????<maven.compiler.source>8</maven.compiler.source> ????????<maven.compiler.target>8</maven.compiler.target> ????????<springboot.vetsion>2.7.8</springboot.vetsion> ????????<mysql-connector-java.version>5.1.46</mysql-connector-java.version> ????????<org.projectlombok.version>1.16.14</org.projectlombok.version> ????????<jjwt.version>0.11.1</jjwt.version> ????????<fastjson.version>1.2.75</fastjson.version> ????</properties>
統(tǒng)一錯誤碼
public?enum?ResultCode?{ ????/*?成功?*/ ????SUCCESS(200,?"成功"), ????/*?默認(rèn)失敗?*/ ????COMMON_FAIL(999,?"失敗"), ????/*?參數(shù)錯誤:1000~1999?*/ ????PARAM_NOT_VALID(1001,?"參數(shù)無效"), ????PARAM_IS_BLANK(1002,?"參數(shù)為空"), ????PARAM_TYPE_ERROR(1003,?"參數(shù)類型錯誤"), ????PARAM_NOT_COMPLETE(1004,?"參數(shù)缺失"), ????/*?用戶錯誤?*/ ????USER_NOT_LOGIN(2001,?"用戶未登錄"), ????USER_ACCOUNT_EXPIRED(2002,?"賬號已過期"), ????USER_CREDENTIALS_ERROR(2003,?"密碼錯誤"), ????USER_CREDENTIALS_EXPIRED(2004,?"密碼過期"), ????USER_ACCOUNT_DISABLE(2005,?"賬號不可用"), ????USER_ACCOUNT_LOCKED(2006,?"賬號被鎖定"), ????USER_ACCOUNT_NOT_EXIST(2007,?"賬號不存在"), ????USER_ACCOUNT_ALREADY_EXIST(2008,?"賬號已存在"), ????USER_ACCOUNT_USE_BY_OTHERS(2009,?"賬號下線"), ????/*?業(yè)務(wù)錯誤?*/ ????NO_PERMISSION(3001,?"沒有權(quán)限"); ????private?Integer?code; ????private?String?message; ????ResultCode(Integer?code,?String?message)?{ ????????this.code?=?code; ????????this.message?=?message; ????} ????public?Integer?getCode()?{ ????????return?code; ????} ????public?void?setCode(Integer?code)?{ ????????this.code?=?code; ????} ????public?String?getMessage()?{ ????????return?message; ????} ????public?void?setMessage(String?message)?{ ????????this.message?=?message; ????} ????private?static?Map<Integer,?ResultCode>?map?=?new?HashMap<>(); ????private?static?Map<String,?ResultCode>?descMap?=?new?HashMap<>(); ????static?{ ????????for?(ResultCode?value?:?ResultCode.values())?{ ????????????map.put(value.getCode(),?value); ????????????descMap.put(value.getMessage(),?value); ????????} ????} ????public?static?ResultCode?getByCode(Integer?code)?{ ????????return?map.get(code); ????} ????public?static?ResultCode?getByMessage(String?desc)?{ ????????return?descMap.get(desc); ????} }
統(tǒng)一返回定義
public?class?CommonResponse<T>?implements?Serializable?{ ????/** ?????*?成功狀態(tài)碼 ?????*/ ????private?final?static?String?SUCCESS_CODE?=?"SUCCESS"; ????/** ?????*?提示信息 ?????*/ ????private?String?message; ????/** ?????*?返回數(shù)據(jù) ?????*/ ????private?T?data; ????/** ?????*?狀態(tài)碼 ?????*/ ????private?Integer?code; ????/** ?????*?狀態(tài) ?????*/ ????private?Boolean?state; ????/** ?????*?錯誤明細(xì) ?????*/ ????private?String?detailMessage; ????/** ?????*?成功 ?????* ?????*?@param?<T>?泛型 ?????*?@return?返回結(jié)果 ?????*/ ????public?static?<T>?CommonResponse<T>?ok()?{ ????????return?ok(null); ????} ????/** ?????*?成功 ?????* ?????*?@param?data?傳入的對象 ?????*?@param?<T>??泛型 ?????*?@return?返回結(jié)果 ?????*/ ????public?static?<T>?CommonResponse<T>?ok(T?data)?{ ????????CommonResponse<T>?response?=?new?CommonResponse<T>(); ????????response.code?=?ResultCode.SUCCESS.getCode(); ????????response.data?=?data; ????????response.message?=?"返回成功"; ????????response.state?=?true; ????????return?response; ????} ????/** ?????*?錯誤 ?????* ?????*?@param?code????自定義code ?????*?@param?message?自定義返回信息 ?????*?@param?<T>?????泛型 ?????*?@return?返回信息 ?????*/ ????public?static?<T>?CommonResponse<T>?error(Integer?code,?String?message)?{ ????????return?error(code,?message,?null); ????} ????/** ?????*?錯誤 ?????* ?????*?@param?code??????????自定義code ?????*?@param?message???????自定義返回信息 ?????*?@param?detailMessage?錯誤詳情信息 ?????*?@param?<T>???????????泛型 ?????*?@return?返回錯誤信息 ?????*/ ????public?static?<T>?CommonResponse<T>?error(Integer?code,?String?message, ??????????????????????????????????????????????String?detailMessage)?{ ????????CommonResponse<T>?response?=?new?CommonResponse<T>(); ????????response.code?=?code; ????????response.data?=?null; ????????response.message?=?message; ????????response.state?=?false; ????????response.detailMessage?=?detailMessage; ????????return?response; ????} ????public?Boolean?getState()?{ ????????return?state; ????} ????public?CommonResponse<T>?setState(Boolean?state)?{ ????????this.state?=?state; ????????return?this; ????} ????public?String?getMessage()?{ ????????return?message; ????} ????public?CommonResponse<T>?setMessage(String?message)?{ ????????this.message?=?message; ????????return?this; ????} ????public?T?getData()?{ ????????return?data; ????} ????public?CommonResponse<T>?setData(T?data)?{ ????????this.data?=?data; ????????return?this; ????} ????public?Integer?getCode()?{ ????????return?code; ????} ????public?CommonResponse<T>?setCode(Integer?code)?{ ????????this.code?=?code; ????????return?this; ????} ????public?String?getDetailMessage()?{ ????????return?detailMessage; ????} ????public?CommonResponse<T>?setDetailMessage(String?detailMessage)?{ ????????this.detailMessage?=?detailMessage; ????????return?this; ????} }
數(shù)據(jù)庫設(shè)計
基于RBAC模型最簡單奔版本的數(shù)據(jù)庫設(shè)計,用戶、角色、權(quán)限表;
基于表單認(rèn)證
對于表單認(rèn)證整體過程可以參考下圖,
核心配置
核心配置包含了框架開啟以及權(quán)限配置,這部分內(nèi)容是重點要關(guān)注的,這里可以看到所有重寫的內(nèi)容,主要包含以下七方面內(nèi)容:
定義哪些資源不需要認(rèn)證,哪些需要認(rèn)證,這里我采用注解形式;
實現(xiàn)自定義認(rèn)證以及授權(quán)異常的接口;
實現(xiàn)自定義登錄成功以及失敗的接口;
實現(xiàn)自定義登出以后的接口;
實現(xiàn)自定義重數(shù)據(jù)查詢對應(yīng)的賬號權(quán)限的接口;
自定義加密的Bean;
自定義授權(quán)認(rèn)證Bean;
當(dāng)然Spring Security還支持更多內(nèi)容,比如限制用戶登錄個數(shù)等等,這里部分內(nèi)容使用不是太多,后續(xù)大家如果有需要我也可以進(jìn)行補充。
//Spring?Security框架開啟 @EnableWebSecurity //授權(quán)全局配置 @EnableGlobalMethodSecurity(prePostEnabled?=?true) @Configuration public?class?SecurityConfig?{ ????@Autowired ????private?SysUserService?sysUserService; ????@Autowired ????private?NotAuthenticationConfig?notAuthenticationConfig; ????@Bean ????SecurityFilterChain?filterChain(HttpSecurity?http)?throws?Exception?{ ????????//支持跨域 ????????http.cors().and() ????????????????//csrf關(guān)閉 ????????????????.csrf().disable() ????????????????//配置哪些需要認(rèn)證?哪些不需要認(rèn)證 ????????????????.authorizeRequests(rep?->?rep.antMatchers(notAuthenticationConfig.getPermitAllUrls().toArray(new?String[0])) ????????????????????????.permitAll().anyRequest().authenticated()) ????????????????.exceptionHandling() ????????????????//認(rèn)證異常處理 ????????????????.authenticationEntryPoint(new?ResourceAuthExceptionEntryPoint()) ????????????????//授權(quán)異常處理 ????????????????.accessDeniedHandler(new?CustomizeAccessDeniedHandler()) ????????????????//登錄認(rèn)證處理 ????????????????.and().formLogin() ????????????????.successHandler(new?CustomizeAuthenticationSuccessHandler()) ????????????????.failureHandler(new?CustomizeAuthenticationFailureHandler()) ????????????????//登出 ????????????????.and().logout().permitAll().addLogoutHandler(new?CustomizeLogoutHandler()) ????????????????.logoutSuccessHandler(new?CustomizeLogoutSuccessHandler()) ????????????????.deleteCookies("JSESSIONID") ????????????????//自定義認(rèn)證 ????????????????.and().userDetailsService(sysUserService); ????????return?http.build(); ????} ????@Bean ????public?PasswordEncoder?passwordEncoder()?{ ????????BCryptPasswordEncoder?bCryptPasswordEncoder?=?new?BCryptPasswordEncoder(); ????????return?bCryptPasswordEncoder; ????} ????@Bean("ssc") ????public?SecuritySecurityCheckService?permissionService()?{ ????????return?new?SecuritySecurityCheckService(); ????} ???? }
通過注解形式實現(xiàn)哪些需要資源不需要認(rèn)證
通過自定義注解@NotAuthentication,然后通過實現(xiàn)InitializingBean接口,實現(xiàn)加載不需要認(rèn)證的資源,支持類和方法,使用就是通過在方法或者類打上對應(yīng)的注解。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({?ElementType.METHOD,?ElementType.TYPE?}) public?@interface?NotAuthentication?{ } @Service public?class?NotAuthenticationConfig?implements?InitializingBean,?ApplicationContextAware?{ ????private?static?final?String?PATTERN?=?"\\{(.*?)}"; ????public?static?final?String?ASTERISK?=?"*"; ????private?ApplicationContext?applicationContext; ????@Getter ????@Setter ????private?List<String>?permitAllUrls?=?new?ArrayList<>(); ????@Override ????public?void?afterPropertiesSet()?throws?Exception?{ ????????RequestMappingHandlerMapping?mapping?=?applicationContext.getBean(RequestMappingHandlerMapping.class); ????????Map<RequestMappingInfo,?HandlerMethod>?map?=?mapping.getHandlerMethods(); ????????map.keySet().forEach(x?->?{ ????????????HandlerMethod?handlerMethod?=?map.get(x); ????????????//?獲取方法上邊的注解?替代path?variable?為?* ????????????NotAuthentication?method?=?AnnotationUtils.findAnnotation(handlerMethod.getMethod(),?NotAuthentication.class); ????????????Optional.ofNullable(method).ifPresent(inner?->?Objects.requireNonNull(x.getPathPatternsCondition()) ????????????????????.getPatternValues().forEach(url?->?permitAllUrls.add(url.replaceAll(PATTERN,?ASTERISK)))); ????????????//?獲取類上邊的注解,?替代path?variable?為?* ????????????NotAuthentication?controller?=?AnnotationUtils.findAnnotation(handlerMethod.getBeanType(),?NotAuthentication.class); ????????????Optional.ofNullable(controller).ifPresent(inner?->?Objects.requireNonNull(x.getPathPatternsCondition()) ????????????????????.getPatternValues().forEach(url?->?permitAllUrls.add(url.replaceAll(PATTERN,?ASTERISK)))); ????????}); ????} ????@Override ????public?void?setApplicationContext(ApplicationContext?applicationContext)?throws?BeansException?{ ????????this.applicationContext?=?applicationContext; ????} }
自定義認(rèn)證異常實現(xiàn)
AuthenticationEntryPoint?用來解決匿名用戶訪問無權(quán)限資源時的異常。
public?class?ResourceAuthExceptionEntryPoint?implements?AuthenticationEntryPoint?{ ????@Override ????public?void?commence(HttpServletRequest?request,?HttpServletResponse?response,?AuthenticationException?authException)?throws?IOException,?ServletException?{ ????????CommonResponse?result=?CommonResponse.error(ResultCode.USER_NOT_LOGIN.getCode(), ????????????????ResultCode.USER_NOT_LOGIN.getMessage()); ????????response.setCharacterEncoding("UTF-8"); ????????response.setContentType("application/json;?charset=utf-8"); ????????response.getWriter().write(JSON.toJSONString(result)); ????} }
自定義授權(quán)異常實現(xiàn)
AccessDeniedHandler用來解決認(rèn)證過的用戶訪問無權(quán)限資源時的異常。
public?class?CustomizeAccessDeniedHandler?implements?AccessDeniedHandler?{ ????@Override ????public?void?handle(HttpServletRequest?request,?HttpServletResponse?response,?AccessDeniedException?accessDeniedException)?throws?IOException,?ServletException?{ ????????CommonResponse?result?=?CommonResponse.error(ResultCode.NO_PERMISSION.getCode(), ????????????????????????ResultCode.NO_PERMISSION.getMessage()); ????????//處理編碼方式,防止中文亂碼的情況 ????????response.setContentType("text/json;charset=utf-8"); ????????//塞到HttpServletResponse中返回給前臺 ????????response.getWriter().write(JSON.toJSONString(result)); ????} }
自定義登錄成功、失敗
AuthenticationSuccessHandler和AuthenticationFailureHandler這兩個接口用于登錄成功失敗以后的處理。
public?class?CustomizeAuthenticationSuccessHandler?implements?AuthenticationSuccessHandler?{ ????@Override ????public?void?onAuthenticationSuccess(HttpServletRequest?request,?HttpServletResponse?response,?Authentication?authentication)?throws?IOException,?ServletException?{ ????????AuthUser?authUser?=?(AuthUser)?SecurityContextHolder.getContext().getAuthentication().getPrincipal(); ????????//返回json數(shù)據(jù) ????????CommonResponse<AuthUser>?result?=?CommonResponse.ok(authUser); ????????//處理編碼方式,防止中文亂碼的情況 ????????response.setContentType("text/json;charset=utf-8"); ????????//塞到HttpServletResponse中返回給前臺 ????????response.getWriter().write(JSON.toJSONString(result)); ????} } public?class?CustomizeAuthenticationFailureHandler?implements?AuthenticationFailureHandler?{ ????@Override ????public?void?onAuthenticationFailure(HttpServletRequest?request,?HttpServletResponse?response,?AuthenticationException?exception)?throws?IOException,?ServletException?{ ????????//返回json數(shù)據(jù) ????????CommonResponse?result?=?null; ????????if?(exception?instanceof?AccountExpiredException)?{ ????????????//賬號過期 ????????????result?=?CommonResponse.error(ResultCode.USER_ACCOUNT_EXPIRED.getCode(),?ResultCode.USER_ACCOUNT_EXPIRED.getMessage()); ????????}?else?if?(exception?instanceof?BadCredentialsException)?{ ????????????//密碼錯誤 ????????????result?=?CommonResponse.error(ResultCode.USER_CREDENTIALS_ERROR.getCode(),?ResultCode.USER_CREDENTIALS_ERROR.getMessage()); //????????}?else?if?(exception?instanceof?CredentialsExpiredException)?{ //????????????//密碼過期 //????????????result?=?CommonResponse.error(ResultCode.USER_CREDENTIALS_EXPIRED); //????????}?else?if?(exception?instanceof?DisabledException)?{ //????????????//賬號不可用 //????????????result?=?CommonResponse.error(ResultCode.USER_ACCOUNT_DISABLE); //????????}?else?if?(exception?instanceof?LockedException)?{ //????????????//賬號鎖定 //????????????result?=?CommonResponse.error(ResultCode.USER_ACCOUNT_LOCKED); //????????}?else?if?(exception?instanceof?InternalAuthenticationServiceException)?{ //????????????//用戶不存在 //????????????result?=?CommonResponse.error(ResultCode.USER_ACCOUNT_NOT_EXIST); ????????}?else?{ ????????????//其他錯誤 ????????????result?=?CommonResponse.error(ResultCode.COMMON_FAIL.getCode(),?ResultCode.COMMON_FAIL.getMessage()); ????????} ????????//處理編碼方式,防止中文亂碼的情況 ????????response.setContentType("text/json;charset=utf-8"); ????????//塞到HttpServletResponse中返回給前臺 ????????response.getWriter().write(JSON.toJSONString(result)); ????} }
自定義登出
LogoutHandler自定義登出以后處理邏輯,比如記錄在線時長等等;LogoutSuccessHandler登出成功以后邏輯處理。
public?class?CustomizeLogoutSuccessHandler?implements?LogoutSuccessHandler?{ ????@Override ????public?void?onLogoutSuccess(HttpServletRequest?request,?HttpServletResponse?response,?Authentication?authentication)?throws?IOException,?ServletException?{ ????????CommonResponse?result?=?CommonResponse.ok(); ????????response.setContentType("text/json;charset=utf-8"); ????????response.getWriter().write(JSON.toJSONString(result)); ????} } public?class?CustomizeLogoutHandler?implements?LogoutHandler?{ ????@Override ????public?void?logout(HttpServletRequest?request,?HttpServletResponse?response,?Authentication?authentication)?{ ????} }
自定義認(rèn)證
自定義認(rèn)證涉及三個對象UserDetialsService、UserDetails以及PasswordEncoder,整個流程首先根據(jù)用戶名查詢出用戶對象交由UserDetialsService接口處理,該接口只有一個方法loadUserByUsername,通過用戶名查詢用戶對象。查詢出來的用戶對象需要通過Spring Security中的用戶數(shù)據(jù)UserDetails實體類來體現(xiàn),這里使用AuthUser繼承User,User本質(zhì)上就是繼承與UserDetails,UserDetails該類中提供了賬號、密碼等通用屬性。對密碼進(jìn)行校驗使用PasswordEncoder組件,負(fù)責(zé)密碼加密與校驗。
public?class?AuthUser?extends?User?{ ????public?AuthUser(String?username,?String?password,?Collection<??extends?GrantedAuthority>?authorities)?{ ????????super(username,?password,?authorities); ????} } @Service public?class?SysUserService?implements?UserDetailsService?{ ????@Autowired ????private?SysUserRepository?sysUserRepository; ????@Autowired ????private?SysRoleService?sysRoleService; ????@Autowired ????private?SysMenuService?sysMenuService; ????@Override ????public?UserDetails?loadUserByUsername(String?username)?throws?UsernameNotFoundException?{ ????????Optional<SysUser>?sysUser?=?Optional.ofNullable(sysUserRepository.findOptionalByUsername(username).orElseThrow(()?-> ????????????????new?UsernameNotFoundException("未找到用戶名"))); ????????List<SysRole>?roles?=?sysRoleService.queryByUserName(sysUser.get().getUsername()); ????????Set<String>?dbAuthsSet?=?new?HashSet<>(); ????????if?(!CollectionUtils.isEmpty(roles))?{ ????????????//角色 ????????????roles.forEach(x?->?{ ????????????????dbAuthsSet.add("ROLE_"?+?x.getName()); ????????????}); ????????????List<Long>?roleIds?=?roles.stream().map(SysRole::getId).collect(Collectors.toList()); ????????????List<SysMenu>?menus?=?sysMenuService.queryByRoleIds(roleIds); ????????????//菜單 ????????????Set<String>?permissions=?menus.stream().filter(x->x.getType().equals(3)) ????????????????????.map(SysMenu::getPermission).collect(Collectors.toSet()); ????????????dbAuthsSet.addAll(permissions); ????????} ????????Collection<GrantedAuthority>?authorities?=?AuthorityUtils ????????????????.createAuthorityList(dbAuthsSet.toArray(new?String[0])); ????????return?new?AuthUser(username,?sysUser.get().getPassword(),?authorities); ????} }
基于Token認(rèn)證
基于Token認(rèn)證這里我采用JWT方式,下圖是整個處理的流程,通過自定義的登錄以及JwtAuthenticationTokenFilter來完成整個任務(wù)的實現(xiàn),需要注意的是這里我沒有使用緩存。
核心配置
與表單認(rèn)證不同的是這里關(guān)閉和FormLogin表單認(rèn)證以及不使用Session方式,增加了JwtAuthenticationTokenFilter,此外ResourceAuthExceptionEntryPoint兼職處理之前登錄失敗以后的異常處理。
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled?=?true) @Configuration public?class?SecurityConfig?{ ????@Autowired ????private?SysUserService?sysUserService; ????@Autowired ????private?NotAuthenticationConfig?notAuthenticationConfig; ????@Bean ????SecurityFilterChain?filterChain(HttpSecurity?http)?throws?Exception?{ ????????//支持跨域 ????????http.cors().and() ????????????????//csrf關(guān)閉 ????????????????.csrf().disable() ????????????????//不使用session ????????????????.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) ????????????????.and().authorizeRequests(rep?->?rep.antMatchers(notAuthenticationConfig.getPermitAllUrls().toArray(new?String[0])) ????????????????????????.permitAll().anyRequest().authenticated()) ????????????????.exceptionHandling() ????????????????//異常認(rèn)證 ????????????????.authenticationEntryPoint(new?ResourceAuthExceptionEntryPoint()) ????????????????.accessDeniedHandler(new?CustomizeAccessDeniedHandler()) ????????????????.and() ????????????????//token過濾 ????????????????.addFilterBefore(new?JwtAuthenticationTokenFilter(),?UsernamePasswordAuthenticationFilter.class) ????????????????.userDetailsService(sysUserService); ????????return?http.build(); ????} ????/** ?????*?獲取AuthenticationManager ?????* ?????*?@param?configuration ?????*?@return ?????*?@throws?Exception ?????*/ ????@Bean ????public?AuthenticationManager?authenticationManager(AuthenticationConfiguration?configuration)?throws?Exception?{ ????????return?configuration.getAuthenticationManager(); ????} ????/** ?????*?密碼 ?????* ?????*?@return ?????*/ ????@Bean ????public?PasswordEncoder?passwordEncoder()?{ ????????BCryptPasswordEncoder?bCryptPasswordEncoder?=?new?BCryptPasswordEncoder(); ????????return?bCryptPasswordEncoder; ????} ????@Bean("ssc") ????public?SecuritySecurityCheckService?permissionService()?{ ????????return?new?SecuritySecurityCheckService(); ????} }
Token創(chuàng)建
@Service public?class?LoginService?{ ????@Autowired ????private?AuthenticationManager?authenticationManager?; ????@Autowired ????private?SysUserService?sysUserService; ????public?LoginVO?login(LoginDTO?loginDTO)?{ ????????//創(chuàng)建Authentication對象 ????????UsernamePasswordAuthenticationToken?authenticationToken?= ????????????????new?UsernamePasswordAuthenticationToken(loginDTO.getUsername(), ????????????????loginDTO.getPassword()); ????????//調(diào)用AuthenticationManager的authenticate方法進(jìn)行認(rèn)證 ????????Authentication?authentication?=?authenticationManager.authenticate(authenticationToken); ????????if(authentication?==?null)?{ ????????????throw?new?RuntimeException("用戶名或密碼錯誤"); ????????} ????????//登錄成功以后用戶信息、 ????????AuthUser?authUser?=(AuthUser)authentication.getPrincipal(); ????????LoginVO?loginVO=new?LoginVO(); ????????loginVO.setUserName(authUser.getUsername()); ????????loginVO.setAccessToken(JwtUtils.createAccessToken(authUser)); ????????loginVO.setRefreshToken(JwtUtils.createRefreshToken(authUser)); ????????return?loginVO; ????} ????public?LoginVO?refreshToken(String?accessToken,?String?refreshToken){ ????????if?(!JwtUtils.validateRefreshToken(refreshToken)?&&?!JwtUtils.validateWithoutExpiration(accessToken))?{ ????????????throw?new?RuntimeException("認(rèn)證失敗"); ????????} ????????Optional<String>?userName?=?JwtUtils.parseRefreshTokenClaims(refreshToken).map(Claims::getSubject); ????????if?(userName.isPresent()){ ????????????AuthUser?authUser?=?sysUserService.loadUserByUsername(userName.get()); ????????????if?(Objects.nonNull(authUser))?{ ????????????????LoginVO?loginVO=new?LoginVO(); ????????????????loginVO.setUserName(authUser.getUsername()); ????????????????loginVO.setAccessToken(JwtUtils.createAccessToken(authUser)); ????????????????loginVO.setRefreshToken(JwtUtils.createRefreshToken(authUser)); ????????????????return?loginVO; ????????????} ????????????throw?new?InternalAuthenticationServiceException("用戶不存在"); ????????} ????????throw?new?RuntimeException("認(rèn)證失敗"); ????} }
Token過濾
public?class?JwtAuthenticationTokenFilter?extends?OncePerRequestFilter?{ ????@Override ????protected?void?doFilterInternal(HttpServletRequest?request,?HttpServletResponse?response,?FilterChain?chain)?throws?ServletException,?IOException?{ ????????//check?Token ????????if?(checkJWTToken(request))?{ ????????????//解析token中的認(rèn)證信息 ????????????Optional<Claims>?claimsOptional?=?validateToken(request) ????????????????????.filter(claims?->?claims.get("authorities")?!=?null); ????????????if?(claimsOptional.isPresent())?{ ????????????????List<String>?authoritiesList?=?castList(claimsOptional.get().get("authorities"),?String.class); ????????????????List<SimpleGrantedAuthority>?authorities?=?authoritiesList ????????????????????????.stream().map(String::valueOf) ????????????????????????.map(SimpleGrantedAuthority::new).collect(Collectors.toList()); ????????????????UsernamePasswordAuthenticationToken?usernamePasswordAuthenticationToken?= ????????????????????????new?UsernamePasswordAuthenticationToken(claimsOptional.get().getSubject(),?null,?authorities); ????????????????SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); ????????????}?else?{ ????????????????SecurityContextHolder.clearContext(); ????????????} ????????} ????????chain.doFilter(request,?response); ????} ????public?static?<T>?List<T>?castList(Object?obj,?Class<T>?clazz)?{ ????????List<T>?result?=?new?ArrayList<T>(); ????????if?(obj?instanceof?List<?>)?{ ????????????for?(Object?o?:?(List<?>)?obj)?{ ????????????????result.add(clazz.cast(o)); ????????????} ????????????return?result; ????????} ????????return?null; ????} ????private?Optional<Claims>?validateToken(HttpServletRequest?req)?{ ????????String?jwtToken?=?req.getHeader("token"); ????????try?{ ????????????return?JwtUtils.parseAccessTokenClaims(jwtToken); ????????}?catch?(ExpiredJwtException?|?SignatureException?|?MalformedJwtException?|?UnsupportedJwtException?|?IllegalArgumentException?e)?{ ????????????//輸出日志 ????????????return?Optional.empty(); ????????} ????} ????private?boolean?checkJWTToken(HttpServletRequest?request)?{ ????????String?authenticationHeader?=?request.getHeader("token"); ????????return?authenticationHeader?!=?null; ????} }
授權(quán)處理
全局授權(quán)的配置已經(jīng)在核心配置中開啟,核心思路是通過SecurityContextHolder獲取當(dāng)前用戶權(quán)限,判斷當(dāng)前用戶的權(quán)限是否包含該方法的權(quán)限,此部分設(shè)計后續(xù)如果存在性能問題,可以設(shè)計緩存來解決。
授權(quán)檢查
public?class?SecuritySecurityCheckService?{ ????public?boolean?hasPermission(String?permission)?{ ????????return?hasAnyPermissions(permission); ????} ????public?boolean?hasAnyPermissions(String...?permissions)?{ ????????if?(CollectionUtils.isEmpty(Arrays.asList(permissions)))?{ ????????????return?false; ????????} ????????Authentication?authentication?=?SecurityContextHolder.getContext().getAuthentication(); ????????if?(authentication?==?null)?{ ????????????return?false; ????????} ????????Collection<??extends?GrantedAuthority>?authorities?=?authentication.getAuthorities(); ????????return?authorities.stream().map(GrantedAuthority::getAuthority).filter(x?->?!x.contains("ROLE_")) ????????????????.anyMatch(x?->?PatternMatchUtils.simpleMatch(permissions,?x)); ????} ????public?boolean?hasRole(String?role)?{ ????????return?hasAnyRoles(role); ????} ????public?boolean?hasAnyRoles(String...?roles)?{ ????????if?(CollectionUtils.isEmpty(Arrays.asList(roles)))?{ ????????????return?false; ????????} ????????Authentication?authentication?=?SecurityContextHolder.getContext().getAuthentication(); ????????if?(authentication?==?null)?{ ????????????return?false; ????????} ????????Collection<??extends?GrantedAuthority>?authorities?=?authentication.getAuthorities(); ????????return?authorities.stream().map(GrantedAuthority::getAuthority).filter(x?->?x.contains("ROLE_")) ????????????????.anyMatch(x?->?PatternMatchUtils.simpleMatch(roles,?x)); ????} }
如何使用
@PreAuthorize("@ssc.hasPermission('sys:user:query')") @PostMapping("/helloWord") public?String?hellWord(){ ??return?"hello?word"; }
跨域問題處理
關(guān)于這部分跨域部分的配置還可以更加細(xì)化一點。
@Configuration public?class?CorsConfig?implements?WebMvcConfigurer?{ ????@Override ????public?void?addCorsMappings(CorsRegistry?registry)?{ ????????//?設(shè)置允許跨域的路徑 ????????registry.addMapping("/**") ????????????????//?設(shè)置允許跨域請求的域名 ????????????????.allowedOriginPatterns("*") ????????????????//?是否允許cookie ????????????????.allowCredentials(true) ????????????????//?設(shè)置允許的請求方式 ????????????????.allowedMethods("GET",?"POST",?"DELETE",?"PUT") ????????????????//?設(shè)置允許的header屬性 ????????????????.allowedHeaders("*") ????????????????//?跨域允許時間 ????????????????.maxAge(3600); ????} }
vue-admin-template登錄的簡單探索感悟
這部分就是有些感悟(背景自身是沒有接觸過Vue相關(guān)的知識),具體的感悟就是不要畏懼一些自己不知道以及不會的東西,大膽的去嘗試,因為自身的潛力是很大的。為什么要這么講,通過自己折騰3個小時,自己完成整個登錄過程,如果是前端可能會比較簡單,針對我這種從沒接觸過的還是有些難度的,需要一些基礎(chǔ)配置更改以及流程梳理,這里簡單來讓大家看下效果,后續(xù)我也會自己把剩下菜單動態(tài)加載以及一些簡單表單交互來完成,做到簡單的表單可以自己來實現(xiàn)。
到此這篇關(guān)于Sping Security前后端分離兩種方案的文章就介紹到這了,更多相關(guān)Sping Security前后端分離內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 調(diào)用天氣Webservice詳解及實例代碼
這篇文章主要介紹了Java 調(diào)用天氣Webservice詳解及實例代碼的相關(guān)資料,這里附實例代碼,使用java 調(diào)用webservice 的小應(yīng)用,需要的朋友可以參考下2016-11-11ExecutorService Callable Future多線程返回結(jié)果原理解析
這篇文章主要為大家介紹了ExecutorService Callable Future多線程返回結(jié)果,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09CommonMark 使用教程:將 Markdown 語法轉(zhuǎn)成 Html
這篇文章主要介紹了CommonMark 使用教程:將 Markdown 語法轉(zhuǎn)成 Html,這個技巧我們做任何網(wǎng)站都可以用到,而且非常好用。,需要的朋友可以參考下2019-06-06淺談Spring Cloud下微服務(wù)權(quán)限方案
這篇文章主要介紹了淺談Spring Cloud下微服務(wù)權(quán)限方案,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06spring boot實現(xiàn)過濾器和攔截器demo
本篇文章主要介紹了spring boot實現(xiàn)過濾器和攔截器demo ,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02JAVA后端學(xué)習(xí)精華之網(wǎng)絡(luò)通信項目進(jìn)階
不同項目之間的通信方式分為,http、socket、webservice;其中socket通信的效率最高,youtube就采用的是原始的socket通信,他們信奉的原則是簡單有效2021-09-09Spring Cloud Eureka 服務(wù)上下線監(jiān)控的實現(xiàn)
這篇文章主要介紹了Spring Cloud Eureka 服務(wù)上下線監(jiān)控的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09