自己动手做后端(三)用户登录系统

2023-05-16

前言

用户登录系统,最简单的解释是将用户账号和密码传输到后端,后端将传过来的账号和密码信息与数据库进行比对,如果正确则登陆成功。这一简单的描述可以概况绝大部分用户登录系统,但是真正实现的时候,我们不仅要考虑登录信息传递的安全性和稳定性,还要时刻确认用户的登录状态,而且用户的权限分离也很重要。其中用户信息的安全是首要的,也是每一个网站应做到的最基本的事情。

Spring Boot Security

框架原理

Spring Boot集成了Spring Security框架,Spring Security的主要核心功能为认证(Authentication)和授权(Authorization),所有的架构也是基于这两个核心功能去实现的。Spring Security在我们进行用户认证以及授予权限的时候,通过各种各样的拦截器来控制权限的访问,从而实现安全。

框架核心组件

  1. SecurityContextHolder:提供对SecurityContext的访问
  2. SecurityContext,:持有Authentication对象和其他可能需要的信息
  3. AuthenticationManager 其中可以包含多个AuthenticationProvider
  4. ProviderManager对象为AuthenticationManager接口的实现类
  5. AuthenticationProvider 主要用来进行认证操作的类 调用其中的authenticate()方法去进行认证操作
  6. Authentication:Spring Security方式的认证主体
  7. GrantedAuthority:对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
  8. UserDetails:构建Authentication对象必须的信息,可以自定义,可能需要访问DB得到
  9. UserDetailsService:通过username构建UserDetails对象,通过loadUserByUsername根据userName获取UserDetail对象 (可以在这里基于自身业务进行自定义的实现 如通过数据库,xml,缓存获取等)

框架搭建

自定义了一个springSecurity安全框架的配置类继承WebSecurityConfigurerAdapter,重写其中的方法configure。实现该类后可以发现,在web容器启动的过程中该类实例对象会被WebSecurityConfiguration类处理。
SecurityConfig

package com.manager.system.config.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Spring Security 安全性基本配置
 * 
 * @author cbigd
 *
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	SecurityUserService myUserService;
	@Autowired
	AuthenticationProviderCustom authenticationProviderCustom;

	@Bean
	public JwtAuthenticationFilter jwtAuthenticationFilter() {
		return new JwtAuthenticationFilter();
	}

	@Override
	public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
		authenticationManagerBuilder.authenticationProvider(authenticationProviderCustom);
	}

	@Bean
	@Override
	public AuthenticationManager authenticationManager() throws Exception {
		return super.authenticationManager();
	}

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Bean
	public SessionRegistry sessionRegistry() {
		return new SessionRegistryImpl();
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.cors().and().csrf().disable().authorizeRequests()
				.antMatchers("/admin/**").authenticated()
				.antMatchers("/user/**").hasAuthority("SUPER_ADMIN")  //访问该路径网站需要超级管理员权限
				.antMatchers("/static/**", "/img/**", "/picture/**", "/logo/**",  "/auth/**").permitAll()
				.and()
				.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); //访问受限网站时获取token
		http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(false).sessionRegistry(sessionRegistry());
	}

	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/auth/**", "/picture/**", "/services/**", "/logo/**");
	}
}

自定义一个SecurityUser类,继承自UserDetails。这个用户类在UserDetails的基础上添加我们数据库设计中用户表的元素,以满足登陆系统设计的需求。
SecurityUser

package com.manager.system.config.security;

import com.manager.system.model.ManagerUserView;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class SecurityUser implements UserDetails {
	/**
	 * 用户实体类
	 * @author cbigd
	 *
	 */
	private static final long serialVersionUID = 1L;
	private int pkid;  
    private String username;
    private String password;
    private int userStatus;
    private int groupStatus;
	private int groupPkid;
    private int type;
	private String groupName;
	private List<Integer> levelAuthorities;
	private List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
	
	SecurityUser(ManagerUserView userView, List<Integer> levelAuthorities) {
		super();  
        this.pkid = userView.getUserPkid(); 
        this.username = userView.getUsername();  
        this.password = userView.getPassword();
        this.setUserStatus(userView.getUserStatus());
        this.setGroupStatus(userView.getGroupStatus());
        this.setGroupPkid(userView.getGroupPkid());
		this.setGroupName(userView.getGroupName());
        this.type = userView.getType();
        this.levelAuthorities = levelAuthorities;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return AUTHORITIES;
	}
	
	public void setAuthorities(List<GrantedAuthority> AUTHORITIES) {
		 this.AUTHORITIES = AUTHORITIES;
	}


	@Override
	public String getPassword() {
		return password;
	}

	@Override
	public String getUsername() {
		return username;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	public void setUsername(String username) {
		this.username = username;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public int getPkid() {
		return pkid;
	}
	public void setPkid(int pkid) {
		this.pkid = pkid;
	}
	public static long getSerialversionuid() {
		return serialVersionUID;
	}
	public int getType() {
		return type;
	}
	public void setType(int type) {
		this.type = type;
	}
	public List<Integer> getLevelAuthorities() {
		return levelAuthorities;
	}
	public void setLevelAuthorities(List<Integer> levelAuthorities) {
		this.levelAuthorities = levelAuthorities;
	}

	public int getUserStatus() {
		return userStatus;
	}

	public void setUserStatus(int userStatus) {
		this.userStatus = userStatus;
	}

	public int getGroupStatus() {
		return groupStatus;
	}

	public void setGroupStatus(int groupStatus) {
		this.groupStatus = groupStatus;
	}

	public int getGroupPkid() {
		return groupPkid;
	}

	public void setGroupPkid(int groupPkid) {
		this.groupPkid = groupPkid;
	}

	public String getGroupName() {
		return groupName;
	}

	public void setGroupName(String groupName) {
		this.groupName = groupName;
	}
}

自定义一个SecurityUserService类,继承自UserDetailsService。这个Service类根据SecurityUser类自定义编写本网站的登录成功之后的权限识别。如果这个用户名是“Simon”则自动创建一个超级管理员Simon(这其实是用户登录系统初始化的后门,一般只会在测试阶段触发)。否则,检验这个成功登录的用户信息,识别它的用户权限并写入AUTHORITIES。

SecurityUserService

package com.manager.system.config.security;

import com.manager.system.dao.ManagerUserViewMapper;
import com.manager.system.model.ManagerUserView;
import com.manager.system.model.ManagerUserViewExample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 获取用户
 * 
 * @author cbigd
 *
 */
@Service
public class SecurityUserService implements UserDetailsService {

	@Autowired
	public ManagerUserViewMapper managerUserViewMapper;

	public SecurityUserService() {
	}
	@Override
	public SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException {
		ManagerUserViewExample userViewExample = new ManagerUserViewExample();
		List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
		if (username.equals("simon")) {
			AUTHORITIES.add(new SimpleGrantedAuthority("SUPER_ADMIN"));
			userViewExample.createCriteria();
		} else {
			userViewExample.createCriteria().andUsernameEqualTo(username);
		}
		List<ManagerUserView> userList = managerUserViewMapper.selectByExample(userViewExample);
		ManagerUserView userView = new ManagerUserView();
		if (!userList.isEmpty()) {
			userView = userList.get(0);
			if (userView.getGroupPkid() == null) {
				userView.setGroupPkid(-1);
			}
			if (userView.getGroupName() == null) {
				userView.setGroupName("");
			}
			if (userView.getGroupStatus() == null) {
				userView.setGroupStatus(-1);
			}
			if (userView.getType() == null) {
				userView.setType(-1);
			}
			List<Integer> levelAuthorities = new ArrayList<Integer>();
			for (int i = 0; i < userList.size(); i++) {
				if (userList.get(i).getAuthorityPkid() != null) {
					levelAuthorities.add(userList.get(i).getAuthorityPkid());
				}
			}
			SecurityUser myuser = null;
			if(username.equals("simon")){
				userView.setUsername("simon");
				userView.setPassword("zy");
				userView.setUserStatus(1);
				userView.setGroupStatus(1);
				userView.setType(0);
				myuser = new SecurityUser(userView, levelAuthorities);
			}else{
				if (userView.getUserStatus() == 1) {
					if (userView.getGroupStatus() == 1) {
						if (userView.getType() == 0) {
							AUTHORITIES.add(new SimpleGrantedAuthority("SUPER_ADMIN"));
						} else if (userView.getType() == 1) {
							AUTHORITIES.add(new SimpleGrantedAuthority("ADMIN"));
						} else {
							AUTHORITIES.add(new SimpleGrantedAuthority("USER"));
						}
					}
				}
				myuser = new SecurityUser(userView, levelAuthorities);
			}
			myuser.setAuthorities(AUTHORITIES);
			return myuser;
		}
		return null;
	}
}

如此一来,我们成功实现了Spring Boot Security初始化步骤。就目前来说,我们已经完成了一个可以登录和注销的登录系统。但是,这个登录系统没有状态保持检测,也没有信息加密,属于一个非常不安全的用户登录系统。

因此,我们要先实现它为其他网页提供的用户认证功能,即检验用户是否是已登录状态,在这里我们采用的是JWT 认证模式。

JWT用户认证

为什么使用JWT认证

前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容,因为 cookie 默认被发了出去。

为了解决这些问题,我们可以使用加密算法对token进行加密,因为对称加密算法很容易被破解,所以我们使用这个非对称加密的JWT。

JWT文件配置

首先写一个Filter拦截器,当用户访问需要权限的网站时,拦截器会将请求拦截,然后根据这个用户的token信息判断他是否拥有访问该网页的权限。这个token的读取方式写在getJwtFromRequest函数中。

JwtAuthenticationFilter

package com.manager.system.config.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {
	
	@Autowired
	private JwtTokenProvider tokenProvider;
	
	@Autowired
    private SecurityUserService securityUserService;
	
	private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
	
	
	@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                String username = tokenProvider.getUserNameFromJWT(jwt);
                UserDetails userDetails = securityUserService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } else {
            	//SecurityContextHolder.clearContext();
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        filterChain.doFilter(request, response);
    }
	
	private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null;
    }
}

这个是JWT的token生成文件,在build中,我们将用户名、系统时间、过期时间和签发者等信息生成token对象。同时,也提供了token有效性检验函数。

JwtTokenProvider

package com.manager.system.config.security;

import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtTokenProvider {

	private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

	@Value("${app.jwtSecret}")
	private String jwtSecret;

	@Value("${app.jwtExpirationInMs}")
	private int jwtExpirationInMs;

	public String generateToken(Authentication authentication) {

		SecurityUser userPrincipal = (SecurityUser) authentication.getPrincipal();
		Date now = new Date();
		Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
		return Jwts.builder()
				.setSubject(userPrincipal.getUsername())
				.setIssuedAt(new Date())
				.setExpiration(expiryDate)
				.signWith(SignatureAlgorithm.HS512, jwtSecret)
				.compact();
	}

	public String getUserNameFromJWT(String token) {
		Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
		return claims.getSubject();
	}

	public boolean validateToken(String authToken) {
		try {
			Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
			return true;
		} catch (SignatureException ex) {
			logger.error("Invalid JWT signature");
		} catch (MalformedJwtException ex) {
			logger.error("Invalid JWT token");
		} catch (ExpiredJwtException ex) {
			logger.error("Expired JWT token");
		} catch (UnsupportedJwtException ex) {
			logger.error("Unsupported JWT token");
		} catch (IllegalArgumentException ex) {
			logger.error("JWT claims string is empty.");
		}
		return false;
	}
}

根据JWT的标准,我们创建一个JwtAuthenticationResponse类,在里面生成标准的accessToken字符串。前端根据这个类的格式生成标准的Authentication对象,传递给后端用以检验用户信息。

JwtAuthenticationResponse

package com.manager.system.config.security;
 
public class JwtAuthenticationResponse {
    private String accessToken;
    private String tokenType = "Bearer";
    private String currentAuthority;

    public JwtAuthenticationResponse(String accessToken, String authority) {
        this.accessToken = accessToken;
        this.currentAuthority = authority;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getTokenType() {
        return tokenType;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }
    
    public String getCurrentAuthority() {
        return currentAuthority;
    }

    public void setCurrentAuthority(String authority) {
        this.currentAuthority = authority;
    }
}

自定义一个JwtAuthenticationEntryPoint类继承自AuthenticationEntryPoint,当访问发生错误时,会发送对应类型的网络错误给前端网页。

JwtAuthenticationEntryPoint

package com.manager.system.config.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
	private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
    @Override
    public void commence(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse,
                         AuthenticationException e) throws IOException, ServletException {
        logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
                "Sorry, You're not authorized to access this resource.");
    }
}

对于前端传送过来的Header,我们自定义一个ExceptionHandler类来处理这个Header。

ExceptionHandler

package com.manager.system.config.security;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Spring security 用户认证
 * @author cbigd
 *
 */
@Component
public class ExceptionHandler implements AccessDeniedHandler {
	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException deniedexception)
			throws IOException, ServletException {
		response.sendRedirect(request.getContextPath()+"/login");
	}
}

上面我们检验的token是由后端在用户成功登录之后生成,并传送给前端的。在这里,我们需要自定义一个ApiResponse类对象,这个对象在用户成功登录之后生活,然后转化成json数据流传输给前端。

ApiResponse

package com.manager.system.config.security;

public class ApiResponse {
	private int status;
    private String statusText;
    private String currentAuthority;

    public ApiResponse(int code, String statusText, String currentAuthority) {
        this.status = code;
        this.statusText = statusText;
        this.currentAuthority = currentAuthority;
    }

    public int getStatus() {
        return status;
    }

    public void setSuccess(int status) {
        this.status = status;
    }

    public String getStatusText() {
        return statusText;
    }

    public void setStatusText(String statusText) {
        this.statusText = statusText;
    }
    
    public String getCurrentAuthority() {
        return currentAuthority;
    }

    public void setCurrentAuthority(String currentAuthority) {
        this.currentAuthority = currentAuthority;
    }
}

最后,我们要有一个用户登录检验接口,检测该用户的账号密码是否正确,状态是否为未注销,最后再根据用户组ID识别该用户的权限等级。出于用户信息安全考虑,我们在用户注册账号时,会对其密码使用BCrypt进行加密存储。于是,在检验用户账号密码时,要对密码进行BCrypt加密才能和数据库信息进行比对。

AuthenticationProviderCustom

package com.manager.system.config.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;

/**
 * Spring security 用户认证
 * 
 * @author cbigd
 *
 */
@Component
public class AuthenticationProviderCustom implements AuthenticationProvider {
	@Autowired
	private SecurityUserService myUserService;

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		String username = null;
		SecurityUser user = null;
		try {
			username = new String(authentication.getName().getBytes("iso8859-1"), "utf-8");
			System.out.println(username);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		if (username != null) {
			user = (SecurityUser) myUserService.loadUserByUsername(username);
		}
		BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
		if (user != null) {
			if (encoder.matches((CharSequence) authentication.getCredentials(), user.getPassword())) {
				if (user.getUserStatus() == 1) {
					if (user.getGroupStatus() == 1) {
						return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(), user.getAuthorities());
					}
					throw new BadCredentialsException("Group disabled");
				}
				throw new BadCredentialsException("Account disabled");
			}
			throw new BadCredentialsException("Password error");
		}
		throw new BadCredentialsException("Username error");

	}

	@Override
	public boolean supports(Class<?> authentication) {
		// 返回true后才会执行上面的authenticate方法,这步能确保authentication能正确转换类型
		return UsernamePasswordAuthenticationToken.class.equals(authentication);
	}
}

结语

经过上面复杂的步骤,我们终于完成了JWT认证和BCrypt加密的用户登录系统,在用户登录经过层层加密和检验之后,他也终于可以访问我们的数据库啦!虽然无论后端多么复杂,用户还是只用输入账号和密码,但为了用户信息安全,再复杂的检验系统也做下去。信息安全永远是第一要义!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

自己动手做后端(三)用户登录系统 的相关文章

  • 【CentOS 7】命令行安装GNOME、KDE图形界面(转载)

    目录 正文 一 进入 root 模式 二 安装 X 窗口系统 三 安装图形界面软件 GNOME 四 更新系统的默认运行级别 正文 CentOS 7 默认是没有图形化界面的 xff0c 但我们很多人在习惯了 Windows 的图形化界面之后
  • Git子模块使用教程

    Git子模块 1 问题背景 随着产品的日益增多 xff0c 各个产品之间的业务功能会出现高度的相同性 xff0c 比如产品A有串口的接收功能 xff0c 产品B也有相同的串口功能 xff0c 这类功能我们可以写成一个通用的串口接收模块 这样
  • K8S Flannel

    1 简介 Flannel 由CoreOS开发 xff0c 用于解决docker集群跨主机通讯的覆盖网络 overlay network xff0c 它的主要思路是 xff1a 预先留出一个网段 xff0c 每个主机使用其中一部分 xff0c
  • 阿里云服务器VNC使用步骤

    1 控制台设置 2 VNC桌面连接设置 yum安装太难 xff0c 不建议 分两步 xff1a 1 安装yum 2 安装VNC ubuntu 16 04中安装yum 在Ubuntu系统中按住 xff1a ctrl 43 alt 43 T 打
  • vscode 配置git

    下载git https git scm com 安装时 xff0c 直接默认所有选项安装 然后打开git安装目录 找到如下路径 打开vscode 点击文件 找到 首选项 点击设置 在搜索框搜索 git path 编辑settings jso
  • Intel D405 运行环境——Realsense-viewer

    第一章 Intel D405 运行环境 Realsense viewer 文章目录 第一章 Intel D405 运行环境 Realsense viewer一 开盲盒二 ubuntu环境下的realsense viewer安装 一 开盲盒
  • linux arm64 中断处理流程完整分析 (一)—— 中断向量表、中断处理流程汇编部分

    中断流程老生常谈 xff0c 但我一直以来也只是知道中断过来之后 xff0c 会保护现场 xff0c 跳到中断向量表 xff0c 执行中断 xff0c 恢复现场 xff0c 然后返回 至于更多细节 xff0c 就不得而知了 这篇文章旨在把更
  • ubuntu apt-get update 失败 server certificate verification failed

    报错提示解决方法step 1step 2step 3 报错提示 执行sudo apt get update时 xff0c 报错如下 Ign 188 https mirrors tuna tsinghua edu cn ubuntu xeni
  • mySQL创建数据库和数据表

    SQL 的主要功能是和数据库建立连接 xff0c 进行增删改查的操作 SQL是关系型数据库管理系统的标准语言 SQL 语言的作用 xff1a 数据定义语言 DDL Data Definition Language 用于创建数据库 xff0c
  • C++刷过的笔试题知识点

    函数若无返回值 xff0c 则它一定无形参 X 析构函数可以有参数 xff0c 但没有返回值 某32位系统下 C 43 43 程序void p 61 malloc 100 sizeof xff08 p xff09 61 4 xff1f 指针
  • 5-字符串

    1 字符串基础 1 1 定义字符串 通过String构造函数构造的字符串与字符串直接量的类型不同 前者为引用对象 xff0c 后者为值类型的字符串 span class token keyword var span s1 span clas
  • 没有Android SDK选项的解决办法+修改Android Studio中的Sdk路径

    安装教程 安装Android Studio时没有Android SDK选项 xff0c 可以先不管 xff0c 继续安装 注意在安装的过程中 xff0c 应该在最后一步install时 xff0c 会出现一个sdk的位置 比如我的在C Us
  • Android Studio一直在Download https://services.gradle.org/distributions/gradle-5.4.1-all.zip的解决方法

    Android Studio的新建工程下面一直出现Download https services gradle org distributions gradle 5 4 1 all zip 解决方法 xff1a 去https service
  • TDEngine 集群安装 (K8S)

    1 构建镜像 1 1 entrypoint sh span class token shebang important bin bash span span class token builtin class name set span 4
  • 设置Android Studio中的模拟器

    怎么设置Android Studio中的模拟器 xff0c 下面记录一下大概流程 然后自己选择设备 xff0c next 下好了之后next 建立后可能会出现以下图片所示问题 位于 的ADB二进制文件已过时 xff0c 并且在Android
  • 算法题算法题!!!!

    0223 思路 xff1a 先计算出老板没控制自己的情绪时的满意数量sum xff0c 再根据X的值 xff0c 维护一个滑动窗口 xff0c 遍历grumpy数组 xff0c 计算增加的满意数量add xff0c 选取最大的一个 xff0
  • MongoDB使用教程

    1 下载 xff1a https www mongodb com try download community 2 安装 解压下载包后正常步骤安装 创建服务 e Application develop MongoDB bin为路径 data
  • 动态规划几个例题!!

    动态规划法 xff01 xff01 xff01 dp i j 61 true表示字符串从下标 i 到下标 j 的位置是一个回文子串 xff08 所谓的状态转移 xff09 span class token keyword var span
  • 小白自学PIX飞控学习笔记

    小白自学飞控学习笔记 xff08 三 xff09 飞控开发准备工作准备阶段Misson Planner 高端操作 飞控开发准备工作 准备阶段 地面站电脑上安装mission planner 校准你的飞行器 Pixhawk指示灯的含义 红灯和
  • 线程同步的四种方式

    原文链接 xff1a https blog csdn net qq 40261882 article details 100538711 1 临界区 xff1a 通过对多线程的串行化来访问公共资源或一段代码 xff0c 速度快 xff0c

随机推荐

  • 分卷压缩与解压分卷

    分卷压缩与解压分卷 分卷压缩 名词解释 分卷压缩 分卷压缩操作 应用场景 解压分卷 解压踩坑 解压操作 nbsp nbsp nbsp nbsp 之前有写过一篇关于 Cesium 加载OSGB倾斜摄影三维模型 的文章 对OSGB模型的特点和文
  • C++输出到.txt文档,并被python读取

    C 43 43 中 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 in
  • 主流消息中间件及选型

    应用最为广泛的三大消息中间件 xff1a RabbitMQ RocketMQ kafka 在传统金融机构 银行 政府机构等有一些老系统还在使用IBM等厂商提供的商用MQ产品 选取原则 1 首先 xff0c 产品应该是开源的 开源意味着如果队
  • K8S Core-DNS

    1 Kube dns 1 1 概述 KubeDNS 由三部分构成 xff1a kube dns xff1a 核心组件 KubeDNS xff1a 依赖 client go 中的 informer 机制 xff0c 监听 Service 和
  • 什么是Batch,什么是Epoch?在训练模型的时候经常看到的参数,自己的见解。

    1 首先我们要大概了解一下什么是梯度下降法 xff1a 梯度下降法的基本思想可以类比为一个下山的过程 假设这样一个场景 xff1a 一个人被困在山上 xff0c 需要从山上下来 找到山的最低点 xff0c 也就是山谷 但此时山上的浓雾很大
  • mavlink协议发送与接收--串口版

    mavlink官网 MAVLINK现分为两个版本V1和V2 xff0c 区别是V2的MsgId扩展到24位 xff0c V1只有8位 xff08 0 255 xff09 原理都是差不多的 xff0c 这里以V1为例 xff0c V2也实际测
  • 转载-关于VDDA、VSSA 、参考电压的问题

    在小于等于64Pin的芯片中 xff0c 在芯片的内部Vref 43 是和VDDA连接在一起的 xff0c 也就是说ADC的是以VDDA为参考电压的 那么还有一点需要注意的就是VDDA和VDD的压差必须小于300mV xff0c 否则可能由
  • wsl,Ubuntu,关于解决 mysql-server : 依赖: mysql-server-5.7 但是它将不会被安装 问题

    出现问题 xff1a 安装mysql时 xff0c sudo apt span class token operator span get install mysql span class token operator span serve
  • jQuery-获取/设置 属性(标准属性,自定义属性)和内容

    一 获取 设置内容 text 设置或返回元素的文本内容 xff1b html 设置或返回元素的内容 xff08 包括html标记 xff09 xff1b val 设置或返回表单字段的值 具体例子如下 xff1a 控制台调试 34 Dcoun
  • idea--java开发最常用快捷键

    复制行 xff1a ctrl 43 d 删除行 xff1a ctrl 43 y 将某行代码向下移动 xff1a ctrl 43 shift 43 将某行代码向上移动 xff1a ctrl 43 shift 43 向下插入新行 xff08 e
  • 数据同步之初识Canal

    git地址 xff1a 阿里巴巴Canal的Git地址 Canal基于日志增量订阅和消费的业务包括 xff1a 数据库镜像 数据库实时备份索引构建和实时维护 拆分异构索引 倒排索引 业务cache刷新 带业务逻辑的增量数据处理 Mysql
  • SpringBoot整合Shiro登录认证和授权(附demo)

    SpringBoot整合Shiro登录认证和授权 废话不多说 xff0c 直接上代码 xff1a 代码有点多 xff0c 想直接拿demo的直接拉到底 开发环境 xff1a JDK1 8Maven 3 5 3IntelliJ IDEAMyS
  • QGroundControl 4.0 地面站使用

    QGroundControl 4 0 地面站使用 飞行界面 总览 xff1a 飞行控制栏 xff1a 在飞行界面与任务规划界面间进行切换控制飞机起飞和降落暂停或者重启当前的操作 xff08 比如降落或者规划的任务 xff09 控制飞机安全返
  • 改变PX4飞控通过MAVLink发送IMU数据的频率

    改变PX4飞控通过MAVLink发送IMU数据的频率 参考 xff1a https docs px4 io master en middleware mavlink html 在QGC的MAVLink Console中执行命令 xff1a
  • K8S Calico

    1 概述 Calico是一个基于 BGP 的纯三层网络方案 它在每个计算节点都利用 Linux kernel 实现了一个高效的虚拟路由器 vRouter 来进行数据转发 每个 vRouter 都通过 BGP 协议将本节点上运行容器的路由信息
  • QT读取XML

    读取XML 1 读取根节点 xff1a QDomElement root 61 doc documentElement 2 读取第一个子节点 xff1a QDomNode node 61 root firstChild 3 读取下一个子节点
  • C++基础入门学习笔记

    C 43 43 基础入门 1 1 hello world include lt iostream gt using namespace std int main cout lt lt 34 Hello world 34 lt lt endl
  • Android的Handler的简单理解和使用

    简单来说 xff0c Handler就是用来传递消息的 Handler可以当成子线程与主线程的消息传送的纽带 在安卓开发中 xff0c 在子线程中无法刷新UI xff0c 是因为UI在子线程中刷新的话 xff0c 是不安全的 xff0c 就
  • 使用datax-web把oracle数据库中的数据导入到mysql

    一 所需环境 Windows系统电脑 Python2 7 18 xff08 需要配置环境变量 xff09 oracle环境 mysql环境 jdk1 8 navicat git python安装下载 https www python org
  • 自己动手做后端(三)用户登录系统

    前言 用户登录系统 xff0c 最简单的解释是将用户账号和密码传输到后端 xff0c 后端将传过来的账号和密码信息与数据库进行比对 xff0c 如果正确则登陆成功 这一简单的描述可以概况绝大部分用户登录系统 xff0c 但是真正实现的时候