前言
本篇文章是基于Spring Security实现前后端分离登录认证及权限控制的实战,主要包括以下四方面内容:
-
Spring Seciruty简单介绍; -
通过Spring Seciruty实现的基于表单和Token认证的两种认证方式; -
自定义实现RBAC的权限控制; -
跨域问题处理;
Spring Seciruty简单介绍
Spring Security是基于Spring框架,提供了一套Web应用安全性的完整解决方案。关于安全方面的两个核心功能是认证和授权,Spring Security重要核心功能就是实现用户认证(Authentication)和用户授权(Authorization)。
认证(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
关于Maven部分细节这里不进行展示了,采用的父子工程,主要简单看下依赖的版本,
8
8
2.7.8
5.1.46
1.16.14
0.11.1
1.2.75
统一错误码
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 map = new HashMap();
private static Map 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 CommonResponseT> 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 泛型
* @return 返回结果
*/
public static CommonResponse ok() {
return ok(null);
}
/**
* 成功
*
* @param data 传入的对象
* @param 泛型
* @return 返回结果
*/
public static CommonResponse ok(T data) {
CommonResponse response = new CommonResponse();
response.code = ResultCode.SUCCESS.getCode();
response.data = data;
response.message = "返回成功";
response.state = true;
return response;
}
/**
* 错误
*
* @param code 自定义code
* @param message 自定义返回信息
* @param 泛型
* @return 返回信息
*/
public static CommonResponse error(Integer code, String message) {
return error(code, message, null);
}
/**
* 错误
*
* @param code 自定义code
* @param message 自定义返回信息
* @param detailMessage 错误详情信息
* @param 泛型
* @return 返回错误信息
*/
public static CommonResponse error(Integer code, String message,
String detailMessage) {
CommonResponse response = new CommonResponse();
response.code = code;
response.data = null;
response.message = message;
response.state = false;
response.detailMessage = detailMessage;
return response;
}
public Boolean getState() {
return state;
}
public CommonResponse setState(Boolean state) {
this.state = state;
return this;
}
public String getMessage() {
return message;
}
public CommonResponse setMessage(String message) {
this.message = message;
return this;
}
public T getData() {
return data;
}
public CommonResponse setData(T data) {
this.data = data;
return this;
}
public Integer getCode() {
return code;
}
public CommonResponse setCode(Integer code) {
this.code = code;
return this;
}
public String getDetailMessage() {
return detailMessage;
}
public CommonResponse 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 permitAllUrls = new ArrayList();
@Override
public void afterPropertiesSet() throws Exception {
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
Map 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 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 = Optional.ofNullable(sysUserRepository.findOptionalByUsername(username).orElseThrow(() ->
new UsernameNotFoundException("未找到用户名")));
List roles = sysRoleService.queryByUserName(sysUser.get().getUsername());
Set dbAuthsSet = new HashSet();
if (!CollectionUtils.isEmpty(roles)) {
//角色
roles.forEach(x -> {
dbAuthsSet.add("ROLE_" + x.getName());
});
List roleIds = roles.stream().map(SysRole::getId).collect(Collectors.toList());
List menus = sysMenuService.queryByRoleIds(roleIds);
//菜单
Set permissions= menus.stream().filter(x->x.getType().equals(3))
.map(SysMenu::getPermission).collect(Collectors.toSet());
dbAuthsSet.addAll(permissions);
}
Collection authorities = AuthorityUtils
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
return new AuthUser(username, sysUser.get().getPassword(), authorities);
}
}
基于Token认证
基于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();
}
}
Token创建
@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 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("认证失败");
}
}
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中的认证信息
Optional claimsOptional = validateToken(request)
.filter(claims -> claims.get("authorities") != null);
if (claimsOptional.isPresent()) {
List authoritiesList = castList(claimsOptional.get().get("authorities"), String.class);
List 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 List castList(Object obj, Class clazz) {
List result = new ArrayList();
if (obj instanceof List>) {
for (Object o : (List>) obj) {
result.add(clazz.cast(o));
}
return result;
}
return null;
}
private Optional 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-admin-template登录的简单探索感悟
这部分就是有些感悟(背景自身是没有接触过Vue相关的知识),具体的感悟就是不要畏惧一些自己不知道以及不会的东西,大胆的去尝试,因为自身的潜力是很大的。为什么要这么讲,通过自己折腾3个小时,自己完成整个登录过程,如果是前端可能会比较简单,针对我这种从没接触过的还是有些难度的,需要一些基础配置更改以及流程梳理,这里简单来让大家看下效果,后续我也会自己把剩下菜单动态加载以及一些简单表单交互来完成,做到简单的表单可以自己来实现。
结束
欢迎大家点点关注,点点赞! 具体代码可参考github
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.e1idc.net