系列文章目录
若依框架学习(前后端分离)——(win11部署docker redis)
若依框架学习(前后端分离)——(redis 浅学)
若依框架学习(前后端分离)——(启动)
文章目录
前言
之前已经成功启动了若依项目,现在开始学习具体功能的代码实现部分。
能看到完成登陆操作时涉及到的几个方法。
重点关注和验证码相关的captchaimage,登陆相关的login,用户信息,用户路由相关的getInfo和getRouters。
一、验证码
思路:表达式@答案
前者生成图片发送给前端
后者作为答案存在redis(存储比较频繁的数据就缓存在redis)
1.前端实现
页面全在views里面
采用的elementUI
页面初始化:
其中的getCode方法就是获取验证码
如果成功回调了 就给前端url赋一个图片,uuid就是key值,也要存一下,这样多用户登录不会有问题。
继续点开方法getcodeimg()
@/api/login意思是我们要再去根目录里找api再找login这个脚本
可以右键网页检查一下
确实是get方法
继续追踪 发现封装了两次才看到axios
开发、生长、测试对应的配置文件
对应的网页检查页中的路径中为什么有/dev-api也就清晰了
http://localhost/dev-api/captchaImage
反向代理,解决跨域问题。url请求前端,进行代理,映射到后端。
把“/dev-api”替换成空再映射到8080后端:
http://localhost:8080/captchaImage
2.后端实现
用搜索功能定位captchaimage
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
AjaxResult ajax = AjaxResult.success();
boolean captchaEnabled = configService.selectCaptchaEnabled();//验证是否开启
ajax.put("captchaEnabled", captchaEnabled);
if (!captchaEnabled)
{
return ajax;
}//如果没开启 根本不需要保存验证码之类的操作 直接在这就结束
// 保存验证码信息
String uuid = IdUtils.simpleUUID();//就是我们之前说的key值
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;//这个就是字符串拼接 captchacode:+上面的答案
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType();
if ("math".equals(captchaType))
{
String capText = captchaProducerMath.createText();//表达式
capStr = capText.substring(0, capText.lastIndexOf("@"));//@之前的算式
code = capText.substring(capText.lastIndexOf("@") + 1);//@之后的答案
image = captchaProducerMath.createImage(capStr);//把算式变成图片
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os);//把数字写成图片
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
二、登陆的实现
1.登录流程 后台
校验验证码—校验用户名密码----生成token
关于token是什么 参考这篇文章:https://blog.csdn.net/Huozhiwu_11/article/details/107230718
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
public void validateCaptcha(String username, String code, String uuid)
{
boolean captchaEnabled = configService.selectCaptchaEnabled();
if (captchaEnabled)
{
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();
}
}
}
// 用户验证 说是用到spring security框架 另一个框架shiro
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();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));//登录成功/失败都用日志记录下来这个操作,这里记录的是操作信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());//记录用户最近的登录信息,ip地址,时间等等
/**
* 记录登录信息
*
* @param userId 用户ID
*/
public void recordLoginInfo(Long userId)
{
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(IpUtils.getIpAddr());
sysUser.setLoginDate(DateUtils.getNowDate());
userService.updateUserProfile(sysUser);
}
public String createToken(LoginUser loginUser)//生成令牌
{
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);//ip 各种信息
refreshToken(loginUser);//更新信息 设置有效期 将用户信息存入redis里面
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
private String createToken(Map<String, Object> claims)//最终用到的是jwt
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
关于jwt,参考这篇文章:
https://blog.csdn.net/weixin_45070175/article/details/118559272?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170428496116800185848791%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=170428496116800185848791&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-118559272-null-null.142v99pc_search_result_base5&utm_term=jwt&spm=1018.2226.3001.4187
2.前端实现
代码如下:
actions: {
// 登录
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)//把后台产生的token存到前端的cookie里面
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
三、用户相关
1.用户角色权限信息 getInfo
前端代码
//**前端** 在全局路由管理器这里出现getInfo,这就是方法入口,任意页面跳转,都会涉及到用户信息
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
if (store.getters.roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页
NProgress.done()
}
}
})
后端代码
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
SysUser user = SecurityUtils.getLoginUser().getUser();
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
return ajax;
}
public Set<String> getMenuPermission(SysUser user)
{
Set<String> perms = new HashSet<String>();
// 管理员拥有所有权限
if (user.isAdmin())
{
perms.add("*:*:*");//这里通配符表示拥有所有权限
}
else
{
List<SysRole> roles = user.getRoles();
if (!CollectionUtils.isEmpty(roles))
{
// 多角色设置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;
}
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_ID', user.userId)
commit('SET_NAME', user.userName)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
2.用户路由信息 getRouters
前端代码
actions: {
// 生成路由
GenerateRoutes({ commit }) {
return new Promise(resolve => {
// 向后端请求路由数据
getRouters().then(res => {
const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data))
const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, false, true)
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
router.addRoutes(asyncRoutes);
commit('SET_ROUTES', rewriteRoutes)
commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
commit('SET_DEFAULT_ROUTES', sidebarRoutes)
commit('SET_TOPBAR_ROUTES', sidebarRoutes)
resolve(rewriteRoutes)
})
})
}
}
不同用户根据角色不同 拿到不同的菜单栏 其实也就是动态路由
import request from '@/utils/request'
// 获取路由
export const getRouters = () => {
return request({
url: '/getRouters',
method: 'get'
})
}
后端代码:
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}//根据用户信息去查他权限下对应的菜单 然后生成动态路由
数据库里通过父id建立几级菜单的关系 遍历递归
public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId)
{
List<SysMenu> returnList = new ArrayList<SysMenu>();
for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();)
{
SysMenu t = (SysMenu) iterator.next();
// 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
if (t.getParentId() == parentId)
{
recursionFn(list, t);
returnList.add(t);
}
}
return returnList;
}
总结
简单介绍了在若依系统登录过程中是如何实现的验证码验证,用户验证登录,用户角色权限信息,用户获取动态菜单路由。