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

本文介绍如何使用SpringBoot Security和JWT实现用户登录系统的安全认证,包括框架原理、核心组件、配置类编写、自定义用户类和服务,以及JWT认证的实现过程。

前言

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

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加密的用户登录系统,在用户登录经过层层加密和检验之后,他也终于可以访问我们的数据库啦!虽然无论后端多么复杂,用户还是只用输入账号和密码,但为了用户信息安全,再复杂的检验系统也做下去。信息安全永远是第一要义!

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值