若依前后端分离项目源码解读笔记
若依前后端分离项目源码解读笔记 已于 2022-12-02 09:03:47 修改 收藏 2 分类专栏: spring boot 文章标签: java 开发语言 版权 spring boot 这里写目录标题 一、SpringSecurity 1.1 入口 二、JWT 2.1 登录生成token 2.2 请求解析token 三、前端 vue 3.1 验证码图片 3.2 登录 拓展1 Vuex.Store的使用 拓展2 vue.router组件 引言:借助 ruoyi-vue框架学习其对 SpringSecurity框架的运用。若依的前后端分离版本基于 SpringSecurity和 JWT配合 Redis来做用户状态记录. 一、SpringSecurity 1.1 入口 后台接收登录数据,基于用户名和密码封装一个(UsernamePasswordAuthenticationToken)认证对象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); 1 然后通过SpringScurity的安全管理器调用authenticate()方法,传入刚才创建的认证对象进行认授权认证 // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername authentication = authenticationManager.authenticate(authenticationToken); 1 2 在若依框架中,这个安全管理器是在配置类中手动注入容器的 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 解决 无法直接注入 AuthenticationManager * echoo mark:手动注入认证管理器 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } 省略... @EnableGlobalMethodSecurity: 开启方法级安全保护,包含了@Configuration 安全管理器调用authenticate()方法,会进入UserDetailsServiceImpl.loadUserByUsername()方法做登录校验操作,这个UserDetailsServiceImpl是若依实现的,loadUserByUsername()方法里就是若依自定义的登录逻辑。这里跳过了一些细节,就是如何保证authenticate()方法用的是若依自定义的登录逻辑?这个是通过重写WebSecurityConfigurerAdapter这个安全适配器里面的configure()方法来指定的。 首先可以看到配置类是继承了WebSecurityConfigurerAdapter这个父类的,然后通过重写configure(AuthenticationManagerBuilder auth)方法来指定用户详情业务对象userDetailsService,这个userDetailsService就是若依自定义的认证业务对象。 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; /** 身份认证接口 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } } 回到入口,也就是authenticate(authenticationToken)方法会去调用UserDetailsService.loadUserByUsername(String username)方法的具体实现UserDetailsServiceImpl.loadUserByUsername(String username)去做登录认证。完美! // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername authentication = authenticationManager.authenticate(authenticationToken); 1 2 最后看一下若依的登录逻辑:根据用户名找到用户→校验密码→创建登录用户数据、填充权限数据并缓存 // echoo mark:登录逻辑 @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); // 创建登录用户数据 } /** * 生成缓存用户对象,填充权限数据 */ public UserDetails createLoginUser(SysUser user) { return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); } 上面是用户认证登录的基本逻辑,后续还有登录状态相关的逻辑。 二、JWT 2.1 登录生成token 登录认证后就是关于登录状态的逻辑了。 首先是token令牌的创建 /** * 创建令牌 */ public String createToken(LoginUser loginUser) { String token = IdUtils.fastUUID(); loginUser.setToken(token); // 在登录对象里保存一份token数据 setUserAgent(loginUser); // 设置用户代理信息 refreshToken(loginUser); Map<String, Object> claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); return createToken(claims); } /** * 设置用户代理信息 * * @param loginUser 登录信息 */ 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()); } User-Agent是用户代理信息,包含了客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等信息 生成jwt令牌: /** * 从数据声明生成令牌 * @param claims 数据声明 * @return 令牌 */ private String createToken(Map<String, Object> claims) { String token = Jwts.builder() // Map<String, Object> claims = new HashMap<>(); // claims.put(Constants.LOGIN_USER_KEY, token); .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret).compact(); return token; } 其中signWith()方法用来配置jwt生成token时用的算法和密钥,然后调用compact()来打包压缩生成一个jwt专用token @Override public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) { Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty."); Assert.isTrue(alg.isHmac(), "Base64-encoded key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); byte[] bytes = TextCodec.BASE64.decode(base64EncodedSecretKey); // 把密钥解码成二进制数组 return signWith(alg, bytes); // 给 JwtBuilder 实例配置算法和密钥 } 经过加密算法加密后的token: eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjdlMzczNDFlLTJlNjQtNDRkZC1hMTU1LTkyMTE0NDQ2NzBjMyJ9.rBblkvEk81768K0tTj0FCaApvqIwFHGKoHxXiZXTJiGcdqhq8gbbzFMwdG-h4FCVFMsTjHSPZe3Dr5at0jqv6g 这个token包含了登录用户的登录参数,如登录用户唯一的uuid,在后续请求中将会用到。 2.2 请求解析token 三、前端 vue 3.1 验证码图片 页面元素,验证码以img元素展示。点击触发getCode()方法获取后台验证码。 <div class="login-code"> <img :src="codeUrl" @click="getCode" class="login-code-img"/> </div> 1 2 3 这里的getCode()方法调用来自login.js文件的getCodeImg()方法 import {getCodeImg} from "@/api/login"; methods: { getCode() { // 获取验证码 getCodeImg().then(res => { this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled; if (this.captchaEnabled) { this.codeUrl = "data:image/gif;base64," + res.img; this.loginForm.uuid = res.uuid; // 表单 token } }); }, ... } 从后台获取到的结果是验证码图片的base64编码数据,因此这里img元素指定图片url时加上了data:image/gif;base64,,其中data:image/gif表示数据类型,base64是数据的编码方式,,后面就是图片的编码数据。 3.2 登录 在login.js登录页面中可以看到登录处理函数 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.redirect = /index 登录成功后跳转到首页 this.$router.push({path: this.redirect || "/"}).catch(() => { }); }).catch(() => { this.loading = false; // 关闭等待蒙板 if (this.captchaEnabled) { this.getCode(); // 登录失败刷新验证码 } }); } }); } 表单校验后完了就是提交数据,这里因为不熟悉Vuex所以一开始都看不出来它那个地方发起了登录请求。 this.$store.dispatch("Login", this.loginForm) 1 上面这个this.$store.dispatch是Vuex用来做异步提交、发送数据的函数,像这里的有两个参数("Login", this.loginForm),其中Login是一个动作函数的名称,这个动作是在store组件定义的时候写好的,下面好好捋捋。 首先看创建store实例的时候,我们注册了很多个组件/模块,其中包含了user模块 const store = new Vuex.Store({ modules: { app, dict, user, tagsView, permission, settings }, getters }) 来看store组件的目录结构,可以看到每一个组件就是一个js文件,里面定义了各种各样的变量 进入user.js可以看到user对象中定义的actions中定义了很多动作函数,其中一个是Login函数,就是在这个函数里完成了登录表单的提交动作。 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) commit('SET_TOKEN', res.token) resolve() }).catch(error => { reject(error) }) }) }, // 获取用户信息 GetInfo({ commit, state }) {...}, // 退出系统 LogOut({ commit, state }) {...}, // 前端 登出 FedLogOut({ commit }){...} } 拓展1 Vuex.Store的使用 mutations和actions都是Vuex.Store里定义函数的属性 比如定义一个store对象的user模块:user对象里分别用了mutations actions两个属性来做函数定义 const user = { state: {...}, mutations: {...}, actions: {...} } 展开看 const user = { state: { name: '', }, // mutations 定义的函数使用 commit(state,...) 函数触发的第一个参数都是 state 对象,表示整个 state 对象,同步加载 mutations: { SET_NAME: (state, name) => { state.name = name // 因为 state 参数是整个 state 对象,所以可以调取到 name 属性进行操作 }, }, // actions 定义的函数使用 dispatch({commit, state},...) 函数触发,函数里的第一个参数是整个 store 对象,异步加载 // 因为 {} 为整个store 对象,所以对象里面包含了 commit函数,state属性等,都可以如{commit, state}这样传递调用。 actions: { Login({ commit }, userInfo) { // 除了代表 store 对象的{}参数,后面一样可以传递需要的其他参数 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) }) }) }, // 退出系统 LogOut({ commit, state }) { // 这里只有代表 store 对象的参数,没有其他参数,因此调用的时候不需要传参 return new Promise((resolve, reject) => { logout(state.token).then(() => { // 调用 state 对象里的属性 commit('SET_NAME', '') // 用 commit 函数调用 mutations 定义的 SET_NAME 函数 ... }).catch(error => { reject(error) }) }) }, } } mutations定义的函数SET_NAME如下调用this.$store.commit("SET_NAME",name) actions定义的函数LogOut如下调用this.$store.dispatch("LogOut") 拓展2 vue.router组件 vue.router组件是vue框架的基础组件之一,用来作为vue项目的独立路由。现在的主流前端框架都会实现自己的路由组件,在vue框架中这个组件就是vue.router。 在main.js中引入router import Vue from 'vue' import router from './router' new Vue({ el: '#app', router, store, render: h => h(App) }) router组件的具体内容是安装vue-router插件和定义路由映射 展开看路由映射是怎样定义的 // 公共路由 export const constantRoutes = [ // 路由映射列表 { path: '/redirect', // 自定义路径 component: Layout, // 路径关联的组件 hidden: true, // 这里属性指是否隐藏侧边栏 children: [ // 嵌套子路由 { path: '/redirect/:path(.*)', component: () => import('@/views/redirect') // 路由懒加载,在匹配到这个路径的时候再导入相关组件 } ] }, { path: '/login', component: () => import('@/views/login'), hidden: true }, // ... ] // 动态路由,基于用户权限动态去加载 export const dynamicRoutes = [ // 路由映射列表 { path: '/system/user-auth', // 自定义路径 component: Layout, // 路径关联的组件 hidden: true, permissions: ['system:user:edit'], // 权限字段列表 children: [ { path: 'role/:userId(\\d+)', // /system/role-auth/user/userId= component: () => import('@/views/system/user/authRole'), name: 'AuthRole', meta: { title: '分配角色', activeMenu: '/system/user' } // 元数据,用于拓展应用 } ] }, // ... ] 基本属性 path: 映射路径 component: 路径对应的组件,这里导入组件有两种方式,类似饿汉或者懒汉。 children: 嵌套子路由,定义父路由后面的路由映射 router的前置后置过滤器 若依在permission.js中对router设置了前置过滤和后置过滤逻辑。 前置过滤用来做登录和角色校验 router.beforeEach((to, from, next) => { NProgress.start(); // 开启加载进度条 if (getToken()) { // 有 token to.meta.title && store.dispatch('settings/setTitle', to.meta.title); // 目标页面标题 if (to.path === '/login') { // 如果请求的是登录页 next({path: '/'}); // 路由匹配路径 '/' NProgress.done() // 加载进度条完成 } 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=${to.fullPath}`); // 否则全部重定向到登录页 NProgress.done() // 完成进度条 } } }) 处理函数的三个参数: to:进入到哪个路由去 from:从哪个路由离开 next:路由的控制器,用来控制下一步操作,常用的有next(true)和next(false) ———————————————— 版权声明:本文为CSDN博主「Echoo华地」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_31856061/article/details/128001975
分类:
若依
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
2021-12-27 若依vue显示表格列配置,可配置操作权限
2021-12-27 用CSS实现一个抽奖转盘
2021-12-27 layui 表格中添加input表单效果且实现数据监听
2021-12-27 element-ui Dialog 对话框组件 :visible.sync 的作用
2021-12-27 git上传项目全部流程
2021-12-27 git----git提交项目的具体流程
2021-12-27 git上传项目