Mall脚手架总结(一)——SpringSecurity实现鉴权认证

2023-10-07 05:44

本文主要是介绍Mall脚手架总结(一)——SpringSecurity实现鉴权认证,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

        在结束理论知识的学习后,荔枝开始项目学习,这个系列文章将围绕荔枝学习mall项目过程中总结的知识点来梳理。本篇文章主要涉及如何整合Spring Security和JWT实现鉴权认证的功能!希望能帮助到一起学习mall项目的小伙伴~~~


文章目录

前言

一、JWT和Spring Security的整合知识点

1.1 JWTtoken的生成流程

1.1.1 什么是CSRF:

1.1.2 为什么需要JWT 

1.1.3 JWTtoken的生成流程 

1.2 UserDetails接口

1.3 CollUtil类

1.4 PasswordEncoder接口

1.5 为什么要实现Serializable接口

总结


一、JWT和Spring Security的整合知识点

这部分会根据mall_learning中的后台用户管理实现类来做系统的梳理学习,目的主要是弄清楚JWT token的生成流程以及相应的类的使用。首先我们来看一下该实现类的demo:

package com.crj.crj_mall_learning.service.impl;import cn.hutool.core.collection.CollUtil;
import com.crj.crj_mall_learning.utils.JwtTokenUtil;
import com.crj.crj_mall_learning.domain.AdminUserDetails;
import com.crj.crj_mall_learning.domain.UmsResource;
import com.crj.crj_mall_learning.service.UmsAdminService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;/*** @auther lzddl* @description 后台用户管理Service实现类*/
@Slf4j
@Service
public class UmsAdminServiceImpl implements UmsAdminService {/*** 存放默认用户信息*/private List<AdminUserDetails> adminUserDetailsList = new ArrayList<>();/*** 存放默认资源信息*/private List<UmsResource> resourceList = new ArrayList<>();@Autowiredprivate JwtTokenUtil jwtTokenUtil;//spring security中的一种对密码的编译方法@Autowiredprivate PasswordEncoder passwordEncoder;// Spring IOC容器的初始化中就会执行该init方法@PostConstructprivate void init(){adminUserDetailsList.add(AdminUserDetails.builder().username("admin").password(passwordEncoder.encode("123456")).authorityList(CollUtil.toList("brand:create","brand:update","brand:delete","brand:list","brand:listAll")).build());adminUserDetailsList.add(AdminUserDetails.builder().username("lzddl").password(passwordEncoder.encode("123456")).authorityList(CollUtil.toList("brand:listAll")).build());resourceList.add(UmsResource.builder().id(1L).name("brand:create").url("/brand/create").build());resourceList.add(UmsResource.builder().id(2L).name("brand:update").url("/brand/update/**").build());resourceList.add(UmsResource.builder().id(3L).name("brand:delete").url("/brand/delete/**").build());resourceList.add(UmsResource.builder().id(4L).name("brand:list").url("/brand/list").build());resourceList.add(UmsResource.builder().id(5L).name("brand:listAll").url("/brand/listAll").build());}@Overridepublic AdminUserDetails getAdminByUsername(String username) {//在存放默认用户对象的集合中来查找指定的用户信息,如果有就返回符合条件的第一条数据;// 如果没有就会返回一个nullList<AdminUserDetails> findList = adminUserDetailsList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList());if(CollUtil.isNotEmpty(findList)){return findList.get(0);}return null;}@Overridepublic List<UmsResource> getResourceList() {return resourceList;}@Overridepublic String login(String username, String password) {String token = null;try {//这里的UserDetails是Spring Security中的一个接口,该接口实现仅仅存储用户的信息,后续会将该接口提供的用户信息封装到认证对象Authentication中去UserDetails userDetails = getAdminByUsername(username);if(userDetails==null){return token;}if (!passwordEncoder.matches(password, userDetails.getPassword())) {throw new BadCredentialsException("密码不正确");}UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authentication);token = jwtTokenUtil.generateToken(userDetails);} catch (AuthenticationException e) {log.warn("登录异常:{}", e.getMessage());}return token;}
}

1.1 JWTtoken的生成流程

        在正式了解整个认证鉴权的流程之前,我们首先需要弄清楚为什么需要使用JWT!在以往的前后端半分离的项目中我们经常使用会话来实现token值的缓存,由于前后端没有涉及过多的跨域操作,因此也就不会采用JWT来保证跨域请求的安全。那为什么跨域请求中需要采用JWT来认证授权呢?

1.1.1 什么是CSRF:

跨站请求伪造(Cross-site request forgery),也被称为 one-click attack 或者session riding,通常缩写为 CSRF 或者 XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。

1.1.2 为什么需要JWT 

        JWT的token值主要是由三部分组成的:header.payload.signature该token是用户访问正规网站会拿到的并缓存在用户浏览器的localStorage本地缓存里面header里面是加密算法的信息,里面放type=jwt,alg(加密算法)=HS512;payload里面存放的是用户名、token的创建时间和过期时间;signature通过使用head中定义的加密算法和后端已经设置好的加密密钥,将head+payload这两部分加密生成一个signature签名,最终拼接所有密钥数据加起来生成一个JWT令牌也就是token。由于JWT的token存在用户浏览器的localStorage本地缓存里面,下次这个用户发送请求给网站的后台时,就会把这个JWT发给正规网站的后台进行安全性校验,而不会向之前那种模式那样使用到cookie。这就可以避免用户在浏览网站的时候被恶意链接获取到cookies,从而绕开网站的安全性校验而导致用户的信息被窃取或造成其它损失。

1.1.3 JWTtoken的生成流程 

首先用户通过后端提供的校验授权的接口来执行登录的操作,这部分的功能主要有两个:校验和授权。首先当用户第一次登录的时候会根据账号和密码来获得JWT生成的token,在上面的介绍中我们已经知晓该token值的功能。

首次登录

        首先用户根据账号和密码进行校验,在Spring Security中我们可以选择在初始化接口实现类的时候就将符合条件的用户信息加载在一个UserDetails对象构成的列表中,并根据默认要求对密码采用passwordEncoder类方法进行encode。因此我们在校验密码的正误的时候也需要采用该类方法对用户输入的密码进行比较。完成身份校验后才会开始生成token,首先需要将用户信息对象UserDetails和用户的访问权限列表交给UsernamePasswordAuthenticationToken对象的构造方法构造一个UsernamePasswordAuthenticationToken对象,之后会交给一个SecurityContextHolder来管理该用户的信息,最后才会调用你自定义的token工具类生成JWT token值。

//用户校验完成,获取一个UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
//将UsernamePasswordAuthenticationToken对象交给SecurityContextHolder,表示用户已经通过身份验证,这一步是为了让SpringSecurity知晓用户对象是谁及其权限,并判断该用户是否有相应的访问资源的权限
SecurityContextHolder.getContext().setAuthentication(authentication);
//获取token
token = jwtTokenUtil.generateToken(userDetails);
token生成的具体流程

        在下面的demo中使用了一个map对象来设置主题信息,并在重载方法中通过setClaims方法来实现主题信息的声明,后续可以根据UserDetails对象的getSubject()方法来获得用户名。这个UserDetails对象的获取其实就是根据用户输入的用户名在已经缓存的用户信息List中找到对应的用户信息对象。

/*** 根据负载生成JWT的token*/private String generateToken(Map<String, Object> claims) {return Jwts.builder().setClaims(claims) // 设置JWT的声明(claims),即要在JWT中存储的信息。这个参数是一个Map<String, Object>,表示JWT的payload部分。.setExpiration(generateExpirationDate()) //过期时间.signWith(SignatureAlgorithm.HS512, secret) //根据后台设置的密钥来生成签名.compact();   //构建并返回最终的JWT字符串}
/*** 根据用户信息生成token*/public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}

        拿到token后用户每次调用接口都在http的headert中添加一个叫Authorization的头,值为WT的token,后台程序通过对Authorization头中信息的解码及数字签名校验来获取其中的用户信息,从而实现认证和授权。 

授权

        在上文中我们已经弄清楚了整个JWT的token令牌生成的流程了,接下来我们需要搞清楚如何整合Spring Security完成用户权限的授权。首先同样的我们事先需要获取到所有用户的权限以及信息,这部分内容并不是现在我们的重点。前面我们知道我们需要把用户信息存在一个个UserDetails对象,当用户成功登录时,UserDetails对象会被封装成Authentication对象,并且设置到Spring Security的安全上下文中(通常是SecurityContextHolder),以便在后续的请求中进行鉴权和授权。首先我们需要弄清楚Spring Security的配置类:

/*** @auther lzddl* @description SpringSecurity的配置*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig {@Autowiredprivate RestfulAccessDeniedHandler restfulAccessDeniedHandler;@Autowiredprivate RestAuthenticationEntryPoint restAuthenticationEntryPoint;@Autowiredprivate IgnoreUrlsConfig ignoreUrlsConfig;@BeanSecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();//不需要保护的资源路径允许访问for (String url : ignoreUrlsConfig.getUrls()) {registry.antMatchers(url).permitAll();}//允许跨域请求的OPTIONS请求,这是因为在跨域访问的正式请求发送前会发送一个Option请求,我们要开启跨域访问就必须允许所有的option请求registry.antMatchers(HttpMethod.OPTIONS).permitAll();httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf.disable().sessionManagement()// 基于token,所以不需要session,下面设置session为无状态的.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().anyRequest()// 除上面外的所有请求全部需要鉴权认证.authenticated();// 禁用缓存httpSecurity.headers().cacheControl();// 添加JWT filterhttpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);//添加自定义未授权和未登录结果返回httpSecurity.exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler).authenticationEntryPoint(restAuthenticationEntryPoint);return httpSecurity.build();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){return new JwtAuthenticationTokenFilter();}}

        我们需要使用SecurityFilterChain来配置相关的操作,首先我们需要获取到httpSecurity对象,然后配置相应的访问路径白名单、允许跨域的Option请求、禁用csrf和session、开启除白名单外的鉴权认证功能、禁用缓存并添加JWT过滤器、添加自定义未授权和未登录结果返回。以上就是我们需要整合SpringSecurity的内容。其中鉴权部分我们需要注意的是JwtAuthenticationTokenFilter 部分,因为我们在配置类中开启了将filter放置在登录验证的功能前,因此这个功能其实就是为我们提供一个可以通过token记住用户态的功能

/*** @auther lzddl* @description JWT登录授权过滤器*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.tokenHead}")private String tokenHead;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws ServletException, IOException {String authHeader = request.getHeader(this.tokenHeader);if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
//            获取tokenString authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
//            根据token获取用户名String username = jwtTokenUtil.getUserNameFromToken(authToken);LOGGER.info("checking username:{}", username);//若有,则获取用户信息并创建authentication对象if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);if (jwtTokenUtil.validateToken(authToken, userDetails)) {UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));LOGGER.info("authenticated user:{}", username);SecurityContextHolder.getContext().setAuthentication(authentication);}}}chain.doFilter(request, response);}
}

真正的授权其实在用户信息对象的加载过程中就完成了! 

当然了接口也需要使用@PreAuthorize注解来定义接口需要的权限,在配置类中我们还要@EnableGlobalMethodSecurity(prePostEnabled=true)注解来开启方法级的安全性!

1.2 UserDetails接口

        该接口和UsernamePasswordAuthenticationToken类其实都是Spring Security中core包下管理。该接口定义了一个用户信息管理的api,要想使用该接口,我们需要自定义实现一个实现类在Spring Security中按照我们自定义的来动态权限配置列表,这里荔枝将其命名为AdminUserDetails类。

/*** @auther lzddl* @description SpringSecurity用户信息封装类*/
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
public class AdminUserDetails implements UserDetails {private String username;private String password;private List<String> authorityList;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorityList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

        this.authorityList.stream()是将authorityList列表对象转化成一个Stream方便我们操作集合对象,map()定义了authorityList中的每个字符串对象到SimpleGrantedAuthority 对象的映射关系!SimpleGrantedAuthority 是Spring Security提供的一个实现了GrantedAuthority 接口的简单权限授予类,它表示用户的权限。最后就是借助collect将SimpleGrantedAuthority 对象收集在一个List对象中。也就是说,这个List包含了用户具有的权限,可以被Spring Security用于进行身份验证和授权。

1.3 CollUtil类

CollUtil类是Hutool插件中有关集合操作的工具类,里面封装了大量的操作集合数据的方法。

中文文档:https://www.hutool.cn/docs/#/

API手册:https://apidoc.gitee.com/dromara/hutool/

1.4 PasswordEncoder接口

该接口是一个密码解析器,一般用来加密。Spring Security 要求容器中必须有 PasswordEncoder 实例,因此我们在用户的信息中必须使用该解析器来对密码进行解析。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.security.crypto.password;public interface PasswordEncoder {String encode(CharSequence rawPassword);boolean matches(CharSequence rawPassword, String encodedPassword);default boolean upgradeEncoding(String encodedPassword) {return false;}
}

 当然了该接口有很多实现类,我们可以创建该接口的实现类对象来指定加密的规则。但是如果我们使用的是@AutoWired注解的方式来将接口进行依赖注入,那么默认使用加密算法BCrypt。如果要修改加密算法,我们可以在配置类声明bean对象的时候修改:

/*** @auther lzddl* @description SpringSecurity的配置*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig {@Autowiredprivate RestfulAccessDeniedHandler restfulAccessDeniedHandler;@Autowiredprivate RestAuthenticationEntryPoint restAuthenticationEntryPoint;@Autowiredprivate IgnoreUrlsConfig ignoreUrlsConfig;@BeanSecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {......}@Beanpublic PasswordEncoder passwordEncoder() {//使用MD4加密算法return new Md4PasswordEncoder();}@Beanpublic JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){return new JwtAuthenticationTokenFilter();}}

1.5 为什么要实现Serializable接口

说起Serializable接口,我们一定会提到的一个名词就是序列化,那么什么是序列化呢?

        Serializable接口是一个语义级别的一个接口,属于Java的io包中定义的接口,该接口没有定义任何的方法,只有实现了该接口的类才会有序列化和反序列化的状态。序列化和反序列化时Java在执行底层的IO读写操作,也就是实现对象数据和文件字节流转化的时候,告诉JVM的一种方式。实现了Serializable接口的类可以被ObjectOutputStream转换为字节流,同时也可以通过ObjectInputStream再将其解析为对象。

可序列化是一个可以被继承的状态。同时为了保证不可序列化类的子类被序列化,子类必须有一个无参的构造方法

serialVersionUID

        序列化运行时与每个可序列化类关联一个版本号,称为serialVersionUlD。在反序列化期间使用它来验证序列化对象的发送方和接收方已经为该对象加载了类与序列化兼容。类的一个类对象的serialVersionUlD与相应发送方的serialVersionUlD不同时会导致在反序列化时抛出异常InvalidclassException。所以我们在实现Serializable接口的时候,一般还会要去尽量显示地定义serialVersionUID就比如:

private static final long serialVersionUID = 1L; 

 记住一定要是final类型的!


总结

        在正式开发项目中我个人认为可能鉴权这部分的功能会更加复杂一点,因为脚手架只是一个快速让我们接触上手的内容,因此这部分token拼接和请求头的生成其实是在Swagger中手动输入的哈哈哈。然后最重要的还是要弄清楚整个整合的流程,需要自定义的配置以及相应的鉴权流程,当然了JWTtoken的结构也是比较重要的!

今朝已然成为过去,明日依然向往未来!我是荔枝,在技术成长之路上与您相伴~~~

如果博文对您有帮助的话,可以给荔枝一键三连嘿,您的支持和鼓励是荔枝最大的动力!

如果博文内容有误,也欢迎各位大佬在下方评论区批评指正!!!

这篇关于Mall脚手架总结(一)——SpringSecurity实现鉴权认证的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/156410

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P