6.4 关于登录
最简单的登录:
1、web登录页填写登录信息,前端发送登录信息到后端;
2、后端接受登录信息,并校验。校验成功,返回成功结果。
这种登录会出现一个问题,用户1成功登录之后,获取到后台管理页,将这个页面的url分享给用户2,用户2没有进行登录就会直接进入到后台管理页,容易造成信息泄露;
解决方法:用户在进入后台管理页之前要进行校验;
几种常见的登录验证的方式:
方式1、cookie-session
1、用户带登录的时候,就会把用户信息发送到后端,后端根据用户信息创建一个sessionid,将用户登录的相关信息放入到session中,后续通过sessionid来进行查找校验,并将sessionid传送到前端;
2、前端接收到sieeionid之后将其存入到cookie中,后续在进行登陆之后会携带cookie,后端会根据前端发送过来的cookie解析到其中的sieeionid,通过sessionid来查找是否用这个id所对应的用户消息。如果存在用户信息,则表示之前这个用户登录过,就会放心该用户跳转到其他界面;如果该id没有查找到相关的用户信息,则表示该用户之前没有进行登录过,不会放行;
缺点:1、cookie只在web网页中生成,存在环境限制;
2、这种方式不能跨域;
3、cookie只能在本机保存,不能在集群环境中使用;
方式2: token认证
上图的token是存储在redis,由于redis是可以部署在集群环境中,所以解决了cookie-session的一下缺陷;
缺点:用户数量大会导致频繁的访问redis校验token,对于内存来说有很大的压力。
方式三:基于token的jwt令牌认证
1、前端的登录信息发送到后端之后,后端基于jwt服务产生一个string类型的字符串,成为jwt令牌,这个令牌是不需要存储在后端的,而是直接返回到前端;
2、前端使用本地的存储方式,将jwt存储起来,后续在进行操作的话会带着jwt令牌到后端,后端会对jwt令牌进行校验,如果能够正确使用jwt解密,说明校验通过了;
jwt:实际上是一个加解密的过程,这个过程是依靠jwt独立的工具包来完成和实现的。
jwt:结构是由负载,签名和头部组成的,由这三部分组成一个串;
6.5 jwt令牌工具类的实现
该工具类主要完成jwt的加密和解密操作:
首先引入相关的依赖:
新建jwtutil类:
public class JWTUtil {
private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
/**
* 密钥:Base64编码的密钥
*/
private static final String SECRET = "weS2l8Tp9wDFov9ic72l/9VRT3j9aYfhEfi8qwGMDgU=";
/**
* 生成安全密钥:将一个Base64编码的密钥解码并创建一个HMAC SHA密钥。
*/
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(
Decoders.BASE64.decode(SECRET));
/**
* 过期时间(单位: 毫秒)
*/
private static final long EXPIRATION = 60*60*1000*24*30;//一个月
/**
* 生成密钥
*
* @param claim {"id": 12, "name":"张山"}
* @return
*/
public static String genJwt(Map<String, Object> claim){
//签名算法
String jwt = Jwts.builder()
.setClaims(claim) // 自定义内容(载荷)
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 设置过期时间
.signWith(SECRET_KEY) // 签名算法,和加解密相关
.compact();
return jwt;
}
/**
* 验证密钥
*/
// Claims是jwt里面定义的对象
public static Claims parseJWT(String jwt){
if (!StringUtils.hasLength(jwt)){
return null;
}
// 创建解析器, 设置签名密钥
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);
Claims claims = null;
try {
//解析token
claims = jwtParserBuilder.build().parseClaimsJws(jwt).getBody();
}catch (ExpiredJwtException e){
// 签名验证失败
// logger.error("解析令牌错误,jwt:{}", jwt, e);
claims = e.getClaims();
}
return claims;
}
/**
* 从token中获取用户ID
*/
public static Integer getUserIdFromToken(String jwtToken) {
Claims claims = JWTUtil.parseJWT(jwtToken);
if (claims != null) {
Map<String, Object> userInfo = new HashMap<>(claims);
return (Integer) userInfo.get("userId");
}
return null;
}
}
controller层登录接口设计:
@RequestMapping("/password/login")
public CommonResult<UserLoginResult> userPasswordLogin(
@Validated @RequestBody UserPasswordLoginParam param) {
logger.info("userPasswordLogin UserPasswordLoginParam:{}",
JacksonUtil.writeValueAsString(param));
UserLoginDTO userLoginDTO = userService.login(param);
return CommonResult.success(convertToUserLoginResult(userLoginDTO));
}
service层登录接口实现:
@Override
public UserLoginDTO login(UserLoginParam param) {
UserLoginDTO userLoginDTO;
// 类型检查与类型转换,java 14及以上版本
if (param instanceof UserPasswordLoginParam loginParam) {
// 密码登录流程
userLoginDTO = loginByUserPassword(loginParam);
} else if (param instanceof ShortMessageLoginParam loginParam) {
// 短信验证码登录流程
userLoginDTO = loginByShortMessage(loginParam);
} else {
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_INFO_NOT_EXIST);
}
return userLoginDTO;
}
private UserLoginDTO loginByShortMessage(ShortMessageLoginParam loginParam) {
if (!RegexUtil.checkMobile(loginParam.getLoginMobile())) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
// 获取用户数据
UserDO userDO = userMapper.selectByPhoneNumber(
new Encrypt(loginParam.getLoginMobile()));
if (userDO == null) {
throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);
} else if (StringUtils.hasText(loginParam.getMandatoryIdentity())
&& !loginParam.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
// 校验验证码
String code = verificationCodeService.getVerificationCode(loginParam.getLoginMobile());
if (!loginParam.getVerificationCode().equals(code)) {
throw new ServiceException(ServiceErrorCodeConstants.VERIFICATION_CODE_ERROR);
}
// 塞入返回值(JWT)
Map<String, Object> claim = new HashMap<>();
claim.put("id", userDO.getId());
claim.put("identity", userDO.getIdentity());
String token = JWTUtil.genJwt(claim);
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userLoginDTO;
}
private UserLoginDTO loginByUserPassword(UserPasswordLoginParam loginParam) {
UserDO userDO = null;
// 判断手机登录还是邮箱登录
if (RegexUtil.checkMail(loginParam.getLoginName())) {
// 邮箱登录
// 根据邮箱查询用户表
userDO = userMapper.selectByMail(loginParam.getLoginName());
} else if (RegexUtil.checkMobile(loginParam.getLoginName())) {
// 手机号登录
// 根据手机号查询用户表
userDO = userMapper.selectByPhoneNumber(new Encrypt(loginParam.getLoginName()));
} else {
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_NOT_EXIST);
}
// 校验登录信息
if (null == userDO) {
throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);
} else if (StringUtils.hasText(loginParam.getMandatoryIdentity())
&& !loginParam.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {
// 强制身份登录,身份校验不通过
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
} else if (!DigestUtil.sha256Hex(loginParam.getPassword()).equals(userDO.getPassword())) {
// 校验密码不同
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
}
// 塞入返回值(JWT)
Map<String, Object> claim = new HashMap<>();
claim.put("id", userDO.getId());
claim.put("identity", userDO.getIdentity());
String token = JWTUtil.genJwt(claim);
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userLoginDTO;
}
使用postman对登录进行测试:
账号密码:
验证码登录:
开启redis缓存验证码:service redis-server start
发送验证码:
验证码登录:
6.6 前端登录完善
进行前端测试:
密码登录:
短信验证码登录:
登录成功之后进入新的界面:
在登录页面点击注册进入注册见面,注册成功之后返回登录界面:
6.7 登录拦截器
在设置好jwt令牌登陆之后,用户进行登录操作,会得到后端传过来的jwt令牌,其次用户会拿着这个令牌去访问其他界面的时候,后端会对这个jwt令牌进行校验,这里采用的是拦截器,当然不是所有的请求都需要校验,会进行相关配置;
登录拦截器:
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
/**登录拦截器
* 预处理,业务请求之前调用
* @param request
* @param response
* @param handler
* @return
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 获取请求头,jwt是在request的请求头中的
String token = request.getHeader("user_token");
log.info("获取token:{}", token);
log.info("获取路径:{}", request.getRequestURI());
// 令牌解析
Claims claims = JWTUtil.parseJWT(token);
if (claims == null) {
log.error("解析JWT令牌失败!");
response.setStatus(401);
return false;
}
log.info("解析JWT令牌成功!放行");
return true;
}
}
配置登录拦截资源:
@Configuration
public class AppConfig implements WebMvcConfigurer {
//配置项
@Autowired
private LoginInterceptor loginInterceptor;
private final List<String> excludes = Arrays.asList(
"/**/*.html",
"/css/**",
"/js/**",
"/pic/**",
"/*.jpg",
"/*.png",
"/favicon.ico",
"/**/login",
"/register",
"/verification-code/send"
);
@Override
//添加自定义的登录拦截器
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludes);
}
}
ps:关于登录的内容就到这里了,谢谢观看!!!