标题
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 登陆的实现
- 一、登陆——前端
- 二、后端——登陆
登陆的实现
一、登陆——前端
我们在login.vue通过表单提交找到handleLogin方法
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
if (this.loginForm.rememberMe) {//是否记住密码
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
}
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => {
this.loading = false;
if (this.captchaEnabled) {
this.getCode();
}
});
}
});
}
它先检测了是否勾选了记住密码,是的话就存到cookies里边,没有勾选就移除,看到它通过Login方法进行跳转,我们在src\store\modules\user.js中找到对应的Login方法
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
Login方法将用户名,密码,验证码,uuid接收到,将它们传入Promise方法中,实现异步处理,其中的login方法,我们在src\api\login.js可以找到
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
return request({
url: '/login',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
使用data进行封装,然后向后端发送请求
二、后端——登陆
我们直接在后台搜索login找到若依的登陆方法
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());//其中进行了校验,日志的写入,使用jwt生成token
ajax.put(Constants.TOKEN, token);
return ajax;
}
首先创建了一个AjaxResult 返回类,我们点进login方法,进入SysLoginService中
boolean captchaEnabled = configService.selectCaptchaEnabled();
// 验证码开关
if (captchaEnabled)
{
validateCaptcha(username, code, uuid);
}
先判断验证码是否开启,点进进入validateCaptcha方法
public void validateCaptcha(String username, String code, String uuid)
{
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");//拼接固定前綴
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);//验证码拿出来后直接删除了
if (captcha == null)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();//过期异常
}
if (!code.equalsIgnoreCase(captcha))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException();//错误异常
}
}
定义了一个verifyKey来将验证码前缀和传递过来的uuid进行一个拼接,其中使用了StringUtils中的nvl判断uuid不为空,定义一个captcha 将redis中与verifyKey对应的value取出来进行判断,如果为空抛出过期异常,AsyncManager是进行异步写日志,其中的message中的内容是在messages.properties中进行配置的用于显示错误信息的,使用equalsIgnoreCase判断验证码是否正确
// 用户验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
}
finally
{
AuthenticationContextHolder.clearContext();
}
使用springSecurity安全框架进行用户验证,创建了一个authenticationToken来将用户账号密码进行保存,使用authenticate进行用户的认证,跳转到ruoyi-framework下的UserDetailsServiceImpl.java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("登录用户:" + username + " 不存在");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已停用");
}
//验证密码
passwordService.validate(user);
return createLoginUser(user);
}
通过查询操作将用户名传入,依据用户名和账号是否被删除进行查询,使用user将查询到的数据进行保存,将user进行数据判断,不通过便抛出异常,通过validate进行密码的验证
public void validate(SysUser user)
{
Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
String username = usernamePasswordAuthenticationToken.getName();
String password = usernamePasswordAuthenticationToken.getCredentials().toString();
Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
if (retryCount == null)
{
retryCount = 0;
}
if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime)));
throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
}
if (!matches(user, password))
{
retryCount = retryCount + 1;
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
MessageUtils.message("user.password.retry.limit.count", retryCount)));
redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
throw new UserPasswordNotMatchException();
}
else
{
clearLoginRecordCache(username);
}
}
validate中首先获取到了在redis中存入的账户名和密码,定义了一个retryCount 来获取缓存中用户密码错误次数,判断了用户错误的最大次数,超过最大次数需要对用户进行锁定默认一分钟,使用matches进行用户输入密码和数据库密码的判断
public boolean matches(SysUser user, String rawPassword)
{
//判断输入密码和数据库密码是否一致
return SecurityUtils.matchesPassword(rawPassword, user.getPassword());
}
点击matchesPassword,进入若依配置的安全服务工具类找到
public static boolean matchesPassword(String rawPassword, String encodedPassword)
{
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.matches(rawPassword, encodedPassword);
}
其中调用了BCryptPasswordEncoder 中的matches方法,用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较,如果两者相同,说明用户输入的密码正确。密码验证正确后我们回到UserDetailsServiceImpl调用createLoginUser方法
public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
createLoginUser将用户信息进行一个封装并通过getMenuPermission获取了菜单的权限数据
public Set<String> getMenuPermission(SysUser user)
{
Set<String> perms = new HashSet<String>();
// 管理员拥有所有权限
if (user.isAdmin())
{
perms.add("*:*:*");
}
else
{
List<SysRole> roles = user.getRoles();
if (!roles.isEmpty() && roles.size() > 1)
{
// 多角色设置permissions属性,以便数据权限匹配权限
for (SysRole role : roles)
{
Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());
role.setPermissions(rolePerms);
perms.addAll(rolePerms);
}
}
else
{
perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
}
}
return perms;
}
判断用户是否拥有多个角色,来获取用户对菜单所拥有的权限,我们返回到前边用户验证下边
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));//记录操作日志写入Logininfor数据库
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());//记录用户信息
// 生成token
return tokenService.createToken(loginUser);
若依又进行了异步处理将用户登陆成功写入的日志,使用recordLoginInfo来记录用户登陆信息
public void recordLoginInfo(Long userId)
{
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
sysUser.setLoginDate(DateUtils.getNowDate());
userService.updateUserProfile(sysUser);
}
recordLoginInfo将登陆的ip和地址写入数据库,若依使用了IpUtils来获取用户登陆的ip,通过Servlet请求里边获取ip,可以在user表中看到改变的字段
回到前边可以看到若依使用createToken生成token
public String createToken(LoginUser loginUser)
{
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
createToken中首先获取到了一个uuid,将uuid存到用户里边,调用setUserAgent和refreshToken
public void setUserAgent(LoginUser loginUser)
{
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
}
setUserAgent获取到用户的ip地址,浏览器,操作系统等
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
refreshToken是用来刷新token,登陆时间和有效期,有效期默认半个小时
return createToken(claims);方法中的createToken
private String createToken(Map<String, Object> claims)
{
String token = Jwts.builder()
//payload载荷
.setClaims(claims)
//signature签名
.signWith(SignatureAlgorithm.HS512, secret)
//compact()方法将header、payload、signature通过.进行一个拼接
.compact();
return token;
}
使用Jwt算法将信息生成一个字符串返回,最后返回到AjaxResult 里边传到前端
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
在前端呢,通过setToken方法进行保存,setToken方法将传递过来的数据保存到Cookies中