您好,登錄后才能下訂單哦!
本篇內容主要講解“Sping Security前后端分離怎么實現”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Sping Security前后端分離怎么實現”吧!
Spring Security是基于Spring框架,提供了一套Web應用安全性的完整解決方案。關于安全方面的兩個核心功能是認證和授權,Spring Security重要核心功能就是實現用戶認證(Authentication)和用戶授權(Authorization)。
認證是用來驗證某個用戶能否訪問該系統。用戶認證一般要求用戶提供用戶名和密碼,系統通過校驗用戶名和密碼來完成認證過程。
授權發生在認證之后,用來驗證某個用戶是否有權限執行某個操作。在一個系統中,不同用戶所具有的權限是不同的。比如對一個文件來說,有的用戶只能進行讀取,而有的用戶可以進行修改。一般來說,系統會為不同的用戶分配不同的角色,而每個角色則對應一系列的權限。
Spring Security進行認證和鑒權的時候,采用的一系列的Filter來進行攔截的。 下圖是Spring Security基于表單認證授權的流程,
在Spring Security一個請求想要訪問到API就會從左到右經過藍線框里的過濾器,其中綠色部分是負責認證的過濾器,藍色部分是負責異常處理,橙色部分則是負責授權。進過一系列攔截最終訪問到我們的API。
整個項目結構如下,demo1部分是基于表單的認證,demo2部分是基于Token的認證,數據庫采用是Mysql,訪問數據庫使用的JPA,Spring Boot版本是2.7.8,Spring Security版本是比較新的5.7.6,這里需要注意的是Spring Security5.7以后版本和前面的版本有一些差異,未來該Demo的版本的問題一直會持續保持升級。 后續也會引用前端項目,前端后臺管理部分我個人感覺后端程序員也要進行簡單的掌握一些,便于工作中遇到形形色色問題更好的去處理。
關于Maven部分細節這里不進行展示了,采用的父子工程,主要簡單看下依賴的版本,
<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>
public enum ResultCode { /* 成功 */ SUCCESS(200, "成功"), /* 默認失敗 */ COMMON_FAIL(999, "失敗"), /* 參數錯誤:1000~1999 */ PARAM_NOT_VALID(1001, "參數無效"), PARAM_IS_BLANK(1002, "參數為空"), PARAM_TYPE_ERROR(1003, "參數類型錯誤"), PARAM_NOT_COMPLETE(1004, "參數缺失"), /* 用戶錯誤 */ 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, "賬號下線"), /* 業務錯誤 */ NO_PERMISSION(3001, "沒有權限"); 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); } }
public class CommonResponse<T> implements Serializable { /** * 成功狀態碼 */ private final static String SUCCESS_CODE = "SUCCESS"; /** * 提示信息 */ private String message; /** * 返回數據 */ private T data; /** * 狀態碼 */ private Integer code; /** * 狀態 */ private Boolean state; /** * 錯誤明細 */ private String detailMessage; /** * 成功 * * @param <T> 泛型 * @return 返回結果 */ public static <T> CommonResponse<T> ok() { return ok(null); } /** * 成功 * * @param data 傳入的對象 * @param <T> 泛型 * @return 返回結果 */ 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; } }
基于RBAC模型最簡單奔版本的數據庫設計,用戶、角色、權限表;
對于表單認證整體過程可以參考下圖,
核心配置包含了框架開啟以及權限配置,這部分內容是重點要關注的,這里可以看到所有重寫的內容,主要包含以下七方面內容:
定義哪些資源不需要認證,哪些需要認證,這里我采用注解形式;
實現自定義認證以及授權異常的接口;
實現自定義登錄成功以及失敗的接口;
實現自定義登出以后的接口;
實現自定義重數據查詢對應的賬號權限的接口;
自定義加密的Bean;
自定義授權認證Bean;
當然Spring Security還支持更多內容,比如限制用戶登錄個數等等,這里部分內容使用不是太多,后續大家如果有需要我也可以進行補充。
//Spring Security框架開啟 @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關閉 .csrf().disable() //配置哪些需要認證 哪些不需要認證 .authorizeRequests(rep -> rep.antMatchers(notAuthenticationConfig.getPermitAllUrls().toArray(new String[0])) .permitAll().anyRequest().authenticated()) .exceptionHandling() //認證異常處理 .authenticationEntryPoint(new ResourceAuthExceptionEntryPoint()) //授權異常處理 .accessDeniedHandler(new CustomizeAccessDeniedHandler()) //登錄認證處理 .and().formLogin() .successHandler(new CustomizeAuthenticationSuccessHandler()) .failureHandler(new CustomizeAuthenticationFailureHandler()) //登出 .and().logout().permitAll().addLogoutHandler(new CustomizeLogoutHandler()) .logoutSuccessHandler(new CustomizeLogoutSuccessHandler()) .deleteCookies("JSESSIONID") //自定義認證 .and().userDetailsService(sysUserService); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); return bCryptPasswordEncoder; } @Bean("ssc") public SecuritySecurityCheckService permissionService() { return new SecuritySecurityCheckService(); } }
通過自定義注解@NotAuthentication,然后通過實現InitializingBean接口,實現加載不需要認證的資源,支持類和方法,使用就是通過在方法或者類打上對應的注解。
@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; } }
AuthenticationEntryPoint?用來解決匿名用戶訪問無權限資源時的異常。
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)); } }
AccessDeniedHandler用來解決認證過的用戶訪問無權限資源時的異常。
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數據 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數據 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) { } }
自定義認證涉及三個對象UserDetialsService、UserDetails以及PasswordEncoder,整個流程首先根據用戶名查詢出用戶對象交由UserDetialsService接口處理,該接口只有一個方法loadUserByUsername,通過用戶名查詢用戶對象。查詢出來的用戶對象需要通過Spring Security中的用戶數據UserDetails實體類來體現,這里使用AuthUser繼承User,User本質上就是繼承與UserDetails,UserDetails該類中提供了賬號、密碼等通用屬性。對密碼進行校驗使用PasswordEncoder組件,負責密碼加密與校驗。
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認證這里我采用JWT方式,下圖是整個處理的流程,通過自定義的登錄以及JwtAuthenticationTokenFilter來完成整個任務的實現,需要注意的是這里我沒有使用緩存。
與表單認證不同的是這里關閉和FormLogin表單認證以及不使用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關閉 .csrf().disable() //不使用session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and().authorizeRequests(rep -> rep.antMatchers(notAuthenticationConfig.getPermitAllUrls().toArray(new String[0])) .permitAll().anyRequest().authenticated()) .exceptionHandling() //異常認證 .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(); } }
@Service public class LoginService { @Autowired private AuthenticationManager authenticationManager ; @Autowired private SysUserService sysUserService; public LoginVO login(LoginDTO loginDTO) { //創建Authentication對象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword()); //調用AuthenticationManager的authenticate方法進行認證 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("認證失敗"); } 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("認證失敗"); } }
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { //check Token if (checkJWTToken(request)) { //解析token中的認證信息 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; } }
全局授權的配置已經在核心配置中開啟,核心思路是通過SecurityContextHolder獲取當前用戶權限,判斷當前用戶的權限是否包含該方法的權限,此部分設計后續如果存在性能問題,可以設計緩存來解決。
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"; }
關于這部分跨域部分的配置還可以更加細化一點。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 設置允許跨域的路徑 registry.addMapping("/**") // 設置允許跨域請求的域名 .allowedOriginPatterns("*") // 是否允許cookie .allowCredentials(true) // 設置允許的請求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 設置允許的header屬性 .allowedHeaders("*") // 跨域允許時間 .maxAge(3600); } }
這部分就是有些感悟(背景自身是沒有接觸過Vue相關的知識),具體的感悟就是不要畏懼一些自己不知道以及不會的東西,大膽的去嘗試,因為自身的潛力是很大的。為什么要這么講,通過自己折騰3個小時,自己完成整個登錄過程,如果是前端可能會比較簡單,針對我這種從沒接觸過的還是有些難度的,需要一些基礎配置更改以及流程梳理,這里簡單來讓大家看下效果,后續我也會自己把剩下菜單動態加載以及一些簡單表單交互來完成,做到簡單的表單可以自己來實現。
到此,相信大家對“Sping Security前后端分離怎么實現”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。