Sping?Security前后端分離兩種實(shí)戰(zhàn)方案
前言
本篇文章是基于Spring Security實(shí)現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的實(shí)戰(zhàn),主要包括以下四方面內(nèi)容:
Spring Seciruty簡(jiǎn)單介紹;
通過(guò)Spring Seciruty實(shí)現(xiàn)的基于表單和Token認(rèn)證的兩種認(rèn)證方式;
自定義實(shí)現(xiàn)RBAC的權(quán)限控制;
跨域問(wèn)題處理;
Spring Seciruty簡(jiǎn)單介紹
Spring Security是基于Spring框架,提供了一套Web應(yīng)用安全性的完整解決方案。關(guān)于安全方面的兩個(gè)核心功能是認(rèn)證和授權(quán),Spring Security重要核心功能就是實(shí)現(xiàn)用戶認(rèn)證(Authentication)和用戶授權(quán)(Authorization)。
認(rèn)證(Authentication)
認(rèn)證是用來(lái)驗(yàn)證某個(gè)用戶能否訪問(wèn)該系統(tǒng)。用戶認(rèn)證一般要求用戶提供用戶名和密碼,系統(tǒng)通過(guò)校驗(yàn)用戶名和密碼來(lái)完成認(rèn)證過(guò)程。
授權(quán)(Authorization)
授權(quán)發(fā)生在認(rèn)證之后,用來(lái)驗(yàn)證某個(gè)用戶是否有權(quán)限執(zhí)行某個(gè)操作。在一個(gè)系統(tǒng)中,不同用戶所具有的權(quán)限是不同的。比如對(duì)一個(gè)文件來(lái)說(shuō),有的用戶只能進(jìn)行讀取,而有的用戶可以進(jìn)行修改。一般來(lái)說(shuō),系統(tǒng)會(huì)為不同的用戶分配不同的角色,而每個(gè)角色則對(duì)應(yīng)一系列的權(quán)限。
實(shí)現(xiàn)簡(jiǎn)單介紹
Spring Security進(jìn)行認(rèn)證和鑒權(quán)的時(shí)候,采用的一系列的Filter來(lái)進(jìn)行攔截的。 下圖是Spring Security基于表單認(rèn)證授權(quán)的流程,
在Spring Security一個(gè)請(qǐng)求想要訪問(wèn)到API就會(huì)從左到右經(jīng)過(guò)藍(lán)線框里的過(guò)濾器,其中綠色部分是負(fù)責(zé)認(rèn)證的過(guò)濾器,藍(lán)色部分是負(fù)責(zé)異常處理,橙色部分則是負(fù)責(zé)授權(quán)。進(jìn)過(guò)一系列攔截最終訪問(wèn)到我們的API。
準(zhǔn)備階段
整個(gè)項(xiàng)目結(jié)構(gòu)如下,demo1部分是基于表單的認(rèn)證,demo2部分是基于Token的認(rèn)證,數(shù)據(jù)庫(kù)采用是Mysql,訪問(wèn)數(shù)據(jù)庫(kù)使用的JPA,Spring Boot版本是2.7.8,Spring Security版本是比較新的5.7.6,這里需要注意的是Spring Security5.7以后版本和前面的版本有一些差異,未來(lái)該Demo的版本的問(wèn)題一直會(huì)持續(xù)保持升級(jí)。 后續(xù)也會(huì)引用前端項(xiàng)目,前端后臺(tái)管理部分我個(gè)人感覺(jué)后端程序員也要進(jìn)行簡(jiǎn)單的掌握一些,便于工作中遇到形形色色問(wèn)題更好的去處理。
Maven
關(guān)于Maven部分細(xì)節(jié)這里不進(jìn)行展示了,采用的父子工程,主要簡(jiǎ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)一錯(cuò)誤碼
public?enum?ResultCode?{ ????/*?成功?*/ ????SUCCESS(200,?"成功"), ????/*?默認(rèn)失敗?*/ ????COMMON_FAIL(999,?"失敗"), ????/*?參數(shù)錯(cuò)誤:1000~1999?*/ ????PARAM_NOT_VALID(1001,?"參數(shù)無(wú)效"), ????PARAM_IS_BLANK(1002,?"參數(shù)為空"), ????PARAM_TYPE_ERROR(1003,?"參數(shù)類(lèi)型錯(cuò)誤"), ????PARAM_NOT_COMPLETE(1004,?"參數(shù)缺失"), ????/*?用戶錯(cuò)誤?*/ ????USER_NOT_LOGIN(2001,?"用戶未登錄"), ????USER_ACCOUNT_EXPIRED(2002,?"賬號(hào)已過(guò)期"), ????USER_CREDENTIALS_ERROR(2003,?"密碼錯(cuò)誤"), ????USER_CREDENTIALS_EXPIRED(2004,?"密碼過(guò)期"), ????USER_ACCOUNT_DISABLE(2005,?"賬號(hào)不可用"), ????USER_ACCOUNT_LOCKED(2006,?"賬號(hào)被鎖定"), ????USER_ACCOUNT_NOT_EXIST(2007,?"賬號(hào)不存在"), ????USER_ACCOUNT_ALREADY_EXIST(2008,?"賬號(hào)已存在"), ????USER_ACCOUNT_USE_BY_OTHERS(2009,?"賬號(hào)下線"), ????/*?業(yè)務(wù)錯(cuò)誤?*/ ????NO_PERMISSION(3001,?"沒(méi)有權(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; ????/** ?????*?返回?cái)?shù)據(jù) ?????*/ ????private?T?data; ????/** ?????*?狀態(tài)碼 ?????*/ ????private?Integer?code; ????/** ?????*?狀態(tài) ?????*/ ????private?Boolean?state; ????/** ?????*?錯(cuò)誤明細(xì) ?????*/ ????private?String?detailMessage; ????/** ?????*?成功 ?????* ?????*?@param?<T>?泛型 ?????*?@return?返回結(jié)果 ?????*/ ????public?static?<T>?CommonResponse<T>?ok()?{ ????????return?ok(null); ????} ????/** ?????*?成功 ?????* ?????*?@param?data?傳入的對(duì)象 ?????*?@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; ????} ????/** ?????*?錯(cuò)誤 ?????* ?????*?@param?code????自定義code ?????*?@param?message?自定義返回信息 ?????*?@param?<T>?????泛型 ?????*?@return?返回信息 ?????*/ ????public?static?<T>?CommonResponse<T>?error(Integer?code,?String?message)?{ ????????return?error(code,?message,?null); ????} ????/** ?????*?錯(cuò)誤 ?????* ?????*?@param?code??????????自定義code ?????*?@param?message???????自定義返回信息 ?????*?@param?detailMessage?錯(cuò)誤詳情信息 ?????*?@param?<T>???????????泛型 ?????*?@return?返回錯(cuò)誤信息 ?????*/ ????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ù)庫(kù)設(shè)計(jì)
基于RBAC模型最簡(jiǎn)單奔版本的數(shù)據(jù)庫(kù)設(shè)計(jì),用戶、角色、權(quán)限表;
基于表單認(rèn)證
對(duì)于表單認(rèn)證整體過(guò)程可以參考下圖,
核心配置
核心配置包含了框架開(kāi)啟以及權(quán)限配置,這部分內(nèi)容是重點(diǎn)要關(guān)注的,這里可以看到所有重寫(xiě)的內(nèi)容,主要包含以下七方面內(nèi)容:
定義哪些資源不需要認(rèn)證,哪些需要認(rèn)證,這里我采用注解形式;
實(shí)現(xiàn)自定義認(rèn)證以及授權(quán)異常的接口;
實(shí)現(xiàn)自定義登錄成功以及失敗的接口;
實(shí)現(xiàn)自定義登出以后的接口;
實(shí)現(xiàn)自定義重?cái)?shù)據(jù)查詢對(duì)應(yīng)的賬號(hào)權(quán)限的接口;
自定義加密的Bean;
自定義授權(quán)認(rèn)證Bean;
當(dāng)然Spring Security還支持更多內(nèi)容,比如限制用戶登錄個(gè)數(shù)等等,這里部分內(nèi)容使用不是太多,后續(xù)大家如果有需要我也可以進(jìn)行補(bǔ)充。
//Spring?Security框架開(kāi)啟 @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(); ????} ???? }
通過(guò)注解形式實(shí)現(xiàn)哪些需要資源不需要認(rèn)證
通過(guò)自定義注解@NotAuthentication,然后通過(guò)實(shí)現(xiàn)InitializingBean接口,實(shí)現(xiàn)加載不需要認(rèn)證的資源,支持類(lèi)和方法,使用就是通過(guò)在方法或者類(lèi)打上對(duì)應(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)))); ????????????//?獲取類(lèi)上邊的注解,?替代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)證異常實(shí)現(xiàn)
AuthenticationEntryPoint?用來(lái)解決匿名用戶訪問(wèn)無(wú)權(quán)限資源時(shí)的異常。
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)異常實(shí)現(xiàn)
AccessDeniedHandler用來(lái)解決認(rèn)證過(guò)的用戶訪問(wèn)無(wú)權(quán)限資源時(shí)的異常。
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中返回給前臺(tái) ????????response.getWriter().write(JSON.toJSONString(result)); ????} }
自定義登錄成功、失敗
AuthenticationSuccessHandler和AuthenticationFailureHandler這兩個(gè)接口用于登錄成功失敗以后的處理。
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中返回給前臺(tái) ????????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)?{ ????????????//賬號(hào)過(guò)期 ????????????result?=?CommonResponse.error(ResultCode.USER_ACCOUNT_EXPIRED.getCode(),?ResultCode.USER_ACCOUNT_EXPIRED.getMessage()); ????????}?else?if?(exception?instanceof?BadCredentialsException)?{ ????????????//密碼錯(cuò)誤 ????????????result?=?CommonResponse.error(ResultCode.USER_CREDENTIALS_ERROR.getCode(),?ResultCode.USER_CREDENTIALS_ERROR.getMessage()); //????????}?else?if?(exception?instanceof?CredentialsExpiredException)?{ //????????????//密碼過(guò)期 //????????????result?=?CommonResponse.error(ResultCode.USER_CREDENTIALS_EXPIRED); //????????}?else?if?(exception?instanceof?DisabledException)?{ //????????????//賬號(hào)不可用 //????????????result?=?CommonResponse.error(ResultCode.USER_ACCOUNT_DISABLE); //????????}?else?if?(exception?instanceof?LockedException)?{ //????????????//賬號(hào)鎖定 //????????????result?=?CommonResponse.error(ResultCode.USER_ACCOUNT_LOCKED); //????????}?else?if?(exception?instanceof?InternalAuthenticationServiceException)?{ //????????????//用戶不存在 //????????????result?=?CommonResponse.error(ResultCode.USER_ACCOUNT_NOT_EXIST); ????????}?else?{ ????????????//其他錯(cuò)誤 ????????????result?=?CommonResponse.error(ResultCode.COMMON_FAIL.getCode(),?ResultCode.COMMON_FAIL.getMessage()); ????????} ????????//處理編碼方式,防止中文亂碼的情況 ????????response.setContentType("text/json;charset=utf-8"); ????????//塞到HttpServletResponse中返回給前臺(tái) ????????response.getWriter().write(JSON.toJSONString(result)); ????} }
自定義登出
LogoutHandler自定義登出以后處理邏輯,比如記錄在線時(shí)長(zhǎng)等等;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)證涉及三個(gè)對(duì)象UserDetialsService、UserDetails以及PasswordEncoder,整個(gè)流程首先根據(jù)用戶名查詢出用戶對(duì)象交由UserDetialsService接口處理,該接口只有一個(gè)方法loadUserByUsername,通過(guò)用戶名查詢用戶對(duì)象。查詢出來(lái)的用戶對(duì)象需要通過(guò)Spring Security中的用戶數(shù)據(jù)UserDetails實(shí)體類(lèi)來(lái)體現(xiàn),這里使用AuthUser繼承User,User本質(zhì)上就是繼承與UserDetails,UserDetails該類(lèi)中提供了賬號(hào)、密碼等通用屬性。對(duì)密碼進(jìn)行校驗(yàn)使用PasswordEncoder組件,負(fù)責(zé)密碼加密與校驗(yàn)。
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方式,下圖是整個(gè)處理的流程,通過(guò)自定義的登錄以及JwtAuthenticationTokenFilter來(lái)完成整個(gè)任務(wù)的實(shí)現(xiàn),需要注意的是這里我沒(méi)有使用緩存。
核心配置
與表單認(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過(guò)濾 ????????????????.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對(duì)象 ????????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("用戶名或密碼錯(cuò)誤"); ????????} ????????//登錄成功以后用戶信息、 ????????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過(guò)濾
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)在核心配置中開(kāi)啟,核心思路是通過(guò)SecurityContextHolder獲取當(dāng)前用戶權(quán)限,判斷當(dāng)前用戶的權(quán)限是否包含該方法的權(quán)限,此部分設(shè)計(jì)后續(xù)如果存在性能問(wèn)題,可以設(shè)計(jì)緩存來(lái)解決。
授權(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"; }
跨域問(wèn)題處理
關(guān)于這部分跨域部分的配置還可以更加細(xì)化一點(diǎn)。
@Configuration public?class?CorsConfig?implements?WebMvcConfigurer?{ ????@Override ????public?void?addCorsMappings(CorsRegistry?registry)?{ ????????//?設(shè)置允許跨域的路徑 ????????registry.addMapping("/**") ????????????????//?設(shè)置允許跨域請(qǐng)求的域名 ????????????????.allowedOriginPatterns("*") ????????????????//?是否允許cookie ????????????????.allowCredentials(true) ????????????????//?設(shè)置允許的請(qǐng)求方式 ????????????????.allowedMethods("GET",?"POST",?"DELETE",?"PUT") ????????????????//?設(shè)置允許的header屬性 ????????????????.allowedHeaders("*") ????????????????//?跨域允許時(shí)間 ????????????????.maxAge(3600); ????} }
vue-admin-template登錄的簡(jiǎn)單探索感悟
這部分就是有些感悟(背景自身是沒(méi)有接觸過(guò)Vue相關(guān)的知識(shí)),具體的感悟就是不要畏懼一些自己不知道以及不會(huì)的東西,大膽的去嘗試,因?yàn)樽陨淼臐摿κ呛艽蟮?。為什么要這么講,通過(guò)自己折騰3個(gè)小時(shí),自己完成整個(gè)登錄過(guò)程,如果是前端可能會(huì)比較簡(jiǎn)單,針對(duì)我這種從沒(méi)接觸過(guò)的還是有些難度的,需要一些基礎(chǔ)配置更改以及流程梳理,這里簡(jiǎn)單來(lái)讓大家看下效果,后續(xù)我也會(huì)自己把剩下菜單動(dòng)態(tài)加載以及一些簡(jiǎn)單表單交互來(lái)完成,做到簡(jiǎn)單的表單可以自己來(lái)實(shí)現(xiàn)。
到此這篇關(guān)于Sping Security前后端分離兩種方案的文章就介紹到這了,更多相關(guān)Sping Security前后端分離內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity實(shí)現(xiàn)前后端分離的示例詳解
- Springboot+Spring Security實(shí)現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的示例代碼
- 基于Spring Security前后端分離的權(quán)限控制系統(tǒng)問(wèn)題
- SpringSecurity+JWT實(shí)現(xiàn)前后端分離的使用詳解
- SpringBoot Security前后端分離登錄驗(yàn)證的實(shí)現(xiàn)
- SpringBoot+Vue前后端分離,使用SpringSecurity完美處理權(quán)限問(wèn)題的解決方法
相關(guān)文章
Java 調(diào)用天氣Webservice詳解及實(shí)例代碼
這篇文章主要介紹了Java 調(diào)用天氣Webservice詳解及實(shí)例代碼的相關(guān)資料,這里附實(shí)例代碼,使用java 調(diào)用webservice 的小應(yīng)用,需要的朋友可以參考下2016-11-11ExecutorService Callable Future多線程返回結(jié)果原理解析
這篇文章主要為大家介紹了ExecutorService Callable Future多線程返回結(jié)果,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09CommonMark 使用教程:將 Markdown 語(yǔ)法轉(zhuǎn)成 Html
這篇文章主要介紹了CommonMark 使用教程:將 Markdown 語(yǔ)法轉(zhuǎn)成 Html,這個(gè)技巧我們做任何網(wǎng)站都可以用到,而且非常好用。,需要的朋友可以參考下2019-06-06淺談Spring Cloud下微服務(wù)權(quán)限方案
這篇文章主要介紹了淺談Spring Cloud下微服務(wù)權(quán)限方案,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06spring boot實(shí)現(xiàn)過(guò)濾器和攔截器demo
本篇文章主要介紹了spring boot實(shí)現(xiàn)過(guò)濾器和攔截器demo ,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02JAVA后端學(xué)習(xí)精華之網(wǎng)絡(luò)通信項(xiàng)目進(jìn)階
不同項(xiàng)目之間的通信方式分為,http、socket、webservice;其中socket通信的效率最高,youtube就采用的是原始的socket通信,他們信奉的原則是簡(jiǎn)單有效2021-09-09Spring Cloud Eureka 服務(wù)上下線監(jiān)控的實(shí)現(xiàn)
這篇文章主要介紹了Spring Cloud Eureka 服務(wù)上下線監(jiān)控的實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-09-09