若依对SpringSecurity框架的运用

news2024/10/6 18:30:42

引言:借助ruoyi-vue框架学习其对SpringSecurity框架的运用。若依的前后端分离版本基于SpringSecurityJWT配合Redis来做用户状态记录.

1 SpringSecurity

1.1 入口

  1. 后台接收登录数据,基于用户名和密码封装一个(UsernamePasswordAuthenticationToken)认证对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
  1. 然后通过SpringScurity的安全管理器调用authenticate()方法,传入刚才创建的认证对象进行认授权认证
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);

在若依框架中,这个安全管理器是在配置类中手动注入容器的

@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

  1. 安全管理器调用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());
    }
}
  1. 再来看这个若依自定义的业务,这个业务实现了springframework.securityUserDetailsService接口,并通过实现它的loadUserByUsername(String username)方法自定义认证逻辑。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
	
	// 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);
    }
}
  1. 回到入口,也就是authenticate(authenticationToken)方法会去调用UserDetailsService.loadUserByUsername(String username)方法的具体实现UserDetailsServiceImpl.loadUserByUsername(String username)去做登录认证。完美!
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
  1. 最后看一下若依的登录逻辑:根据用户名找到用户→校验密码→创建登录用户数据、填充权限数据并缓存
    // 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

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,在后续请求中将会用到。

请求解析token

vue

验证码图片

页面元素,验证码以img元素展示。点击触发getCode()方法获取后台验证码。

<div class="login-code">
	<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>

这里的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是数据的编码方式,,后面就是图片的编码数据。

登录

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)

上面这个this.$store.dispatchVuex用来做异步提交、发送数据的函数,像这里的有两个参数("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 }){...}
  }

拓展 Vuex.Store的使用

mutationsactions都是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")

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/47085.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

线程安全和synchronized关键字

一&#xff0c;线程安全的引入 1.示例 多线程在多进程的基础上更好解决了并发问题&#xff0c;但由于一个进程内的多个线程是资源共享的&#xff0c;就会出现多个线程在并发执行的时候造成内存中数据的混乱。 举一个例子&#xff1a; class Counter {public int count;publi…

hypervision理解的记录

目录 一、hypervision介绍 Type 1 Hypervisor Type 2 Hypervisor 二、QNX hypervision是TYPE1的虚拟机 三、QNX hypervision架构 1、VMM (虚拟机管理器) 2、virtual-net 3、qnx官网 network 九、其他 一、hypervision介绍 首先&#xff0c;hypervision分为Type1和Type2…

SpringBoot添加外部jar包及打包(亲测有效) - 第452篇

历史文章&#xff08;文章累计450&#xff09; 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 深入Feign源码吃透Spring扩展点「扩展点…

亿级异构任务调度框架设计与实践

背景 阿里云日志服务作为云原生可观测与分析平台。提供了一站式的数据采集、加工、查询分析、可视化、告警、消费与投递等功能。全面提升用户的研发、运维、运营、安全场景的数字化能力。 日志服务平台作为可观测性平台提供了数据导入、数据加工、聚集加工、告警、智能巡检、…

“200万天价床垫”引发的思考:普通床垫越卖越贵是推测还是事实

定制床垫价格高达200万元&#xff1f;近段时间&#xff0c;一场娱乐圈的闹剧让大家把目光转向了床垫市场。在天价床垫的话题下&#xff0c;除了大部分猜测床垫品牌的讨论以外&#xff0c;也有不少人认为指出了“社会现状”&#xff1a;健康品质化的消费追求正在让市面上的床垫价…

Python_数据容器_字典

一、字典&#xff08;映射&#xff09;的定义 生活中的字典&#xff1a; 【字】&#xff1a;【含义】 可以按【字】找出对应的【含义】 Python中的字典&#xff1a; key : value 可以按照[key]找出对应的[value] 1、Python字典使用场景&#xff1a; 通过使用字典&#…

uni-app 之 web-view 与h5 通讯

官网文档&#xff1a;https://uniapp.dcloud.net.cn/component/web-view.html#getenv web-view 是一个 web 浏览器组件&#xff0c;可以用来承载网页的容器&#xff0c;会自动铺满整个页面&#xff08;nvue 使用需要手动指定宽高&#xff09;。 各小程序平台&#xff0c;web-v…

图像下采样再上采样维度不匹配

图像在下采样后再上采样&#xff0c;维度会发生不匹配&#xff0c;假设一幅图像的维度为(b,c,h,w)&#xff0c;那么当h和w是偶数的时候&#xff0c;下采样和上采样是匹配的&#xff0c;当且仅当他是偶数的时候才匹配&#xff0c;然而图像的h和w往往不一定是偶数。当然有许多种方…

【Shell 脚本速成】06、Shell 数组详解

目录 一、数组介绍 二、数组定义 三、数组赋值方式 四、数组取值 案例演示 五、关联数组 5.1 定义管理数组 5.2 关联数组赋值 5.3 管理数组取值 5.4 综合案例 有这样一个现实问题&#xff1a;一个班级学员信息系统&#xff0c;要求存储学员ID、NAME、SCORE、AGE、GE…

关于订单功能的处理和分析

这两天看了一下RABC的权限管理处理&#xff0c;梳理了一下订单功能的表创建&#xff0c;界面&#xff0c;功能分析。 目录 RABC RBAC0模型 那么对于RABC模型我们怎么创建数据库表&#xff1f; 订单模块的梳理 RABC RABC说的是在用户和权限之间多一个角色&#xff0c;用户与…

软件测试基础

⭐️前言⭐️ &#x1f349;博客主页&#xff1a; &#x1f341;【如风暖阳】&#x1f341; &#x1f349;精品Java专栏【JavaSE】、【备战蓝桥】、【JavaEE初阶】、【MySQL】、【数据结构】 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&…

Spring Cloud OpenFeign - - - >拦截器

源码地址&#xff1a;https://download.csdn.net/download/weixin_42950079/87209379 SpringMVC拦截器 和 OpenFeign拦截器 的区别 初学者很容易将 Spring MVC 拦截器 和 Spring Cloud OpenFeign 拦截器搞混&#xff0c;误以为OpenFeign拦截器就是SpringMVC拦截器&#xff1a; …

虹科分享 | 麦氏比浊仪在药敏试验中的应用

细菌是重要的病原微生物&#xff0c;人类针对不同的病原菌研发了各类抗菌药&#xff0c;这些药物对细菌性疾病的治疗与控制起到了关键作用。然而随着新型致病菌的不断出现&#xff0c;加上细菌在药物使用过程中逐渐产生了耐药性&#xff0c;抗菌药的防治效果越来越差。病原菌对…

Python 中的类与继承

类的定义以及实例的建立 Python中&#xff0c;类通过 class 关键字定义。 例如最简单的一个类定义可以为&#xff1a; class Person(object):pass Python 的编程习惯&#xff0c;类名以大写字母开头&#xff0c;紧接着是(object)&#xff0c;表示该类是从哪个类继承下来的。…

解决单文件组件里的跨域请求数据问题(使用vue单文件组件请求数据必会遇到的问题!!!)

为什么要解决跨域问题&#xff1a; 因为浏览器有限制&#xff0c;只有同域名同端口号下的数据才能拿来用&#xff1b;那如果想拿到不同域名不同端口号下的数据就不行了&#xff1b; 在单文件组件中如何去解决跨域问题&#xff1a; 因为服务器没有跨域限制&#xff0c;只有浏览…

PyTorch(四)Torchvision 与 Transforms

文章目录Log一、Torchvision1. CIFAR10① 介绍② 使用2. 与 Transforms 结合使用总结Log 2022.11.28接着开启新的一章2022.11.29继续学习 一、Torchvision 视频教程中 Torchvision v0.9.0文档Torchvision 官方文档Torchvision Datasets API 文档 1. CIFAR10 ① 介绍 CIFAR…

HBuilder X实现banner轮播图

第一步还是去仔细阅读官方文档&#xff0c;找到组件下面的内置组件里面的swiper&#xff0c;如图所示 官方是提供了一个用来制作轮播图的滑块视图容器&#xff1a;swiper 一般来说&#xff0c;轮播图的图片地址是由后端返回给前端遍历显示在页面上的&#xff0c;所以基本的结构…

世界杯的“中国元素”昂扬大国担当,点面科技全新推出的多模态多功能移动终端踏上卡塔尔征途!

本次世界杯中国足球队没有参加&#xff0c;但是在世界杯的赛场上到处都有中国元素。 中国承建的卢赛尔体育场 卡塔尔世界杯主体育场——卢赛尔球场由中国铁建以总承包身份承建&#xff0c;引发了全球瞩目。在本届世界杯赛事中&#xff0c;该场馆将承担包括决赛在内的10场比赛&…

idea手动创建webapp(在main文件夹下)

SSM自学笔记 文章目录一、Maven使用正常情况首先不使用骨架创建好Maven项目然后选择Project Structure...选择要创建webapp的模块修改路径二、Maven不正常工作时一、Maven使用正常情况 首先不使用骨架创建好Maven项目 然后选择Project Structure… 选择要创建webapp的模块 选好…

前置微小信号放大器在光声技术的血管识别研究中的应用

实验名称&#xff1a;前置微小信号放大器在光声技术的血管识别研究中的应用 研究方向&#xff1a;生物识别技术 测试目的&#xff1a; 利用MATLAB对光声血管进行识别&#xff1a;1、对光声血管图库的图像进行预处理包括归一化、二值化、平滑、细化和毛刺修剪得到细化图像&#…