SpringSecurity与JWT如何实现项目端分离认证与授权

✅SpringSecurity+JWT 实现项目前端分离认证授权✅


1. 简介

Spring Security是Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,杜区资源也比Shiro丰富。

一般来说中大型的项目都是使用Spring Security来做安全框架。小项目有Shiro的比较多,因为相比与Spring Security,Shiro的上手更加的简单,

一般Web应用的需要进行认证和授权,
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是Spring Security作为安全框架的核心功能。

没有引入Spring Security框架之前访问一个网页是直接访问的

Untitled

加入了Spring Security之后,默认跳转到登陆页面

Untitled

这个实际上就是一个认证的过程,登录成功之后才能够访问默认的接口

Untitled

是 有一个默认的退出接口的, logout

2. 登录校验流程

Untitled

前后端沟通使用token,判断是否携带对应的Token来判断是否是系统的用户,也可以拿到Token获取到加密之前的一些数据,用来判断是哪一用户

登录的时候

  1. 前端发送用户名密码给服务器进行一个比对,如果说一切的信息都是正确的,服务器后将一些用户信息比如说用户名,用户密码,创建时间生成一个JWT,然后将这个生成的JWT Token响应给前端,前端进行一个存储(Local Storage)
  2. 然后前端再去访问其他请求的时候,都会在请求头中携带Token数据,然后服务器可以获取请求头中的Token进行解析获取到Token中的信息然后进行比对,如果解析成功就可以获取到之前一些加密的数据比如说UserID, 密码之类的
  3. 拿到UserID之后就可以获取用户的其他信息,比如说用户的权限,能否访问对应的接口等等。

image-20211214151515385.png

  1. 接收用户凭据:当用户尝试登录时,他们的凭据(通常是用户名和密码)会被UsernamePasswordAuthenticationFilter接收,这是AbstractAuthenticationProcessingFilter的一个具体实现。
  2. 生成Authentication对象:过滤器将基于用户提供的信息创建一个未经认证的Authentication对象。
  3. AuthenticationManager进行认证:这个未经认证的Authentication对象会被传递给AuthenticationManager,具体是它的实现类ProviderManager,来进行验证。
  4. AuthenticationProvider进行认证ProviderManager会调用DaoAuthenticationProvider,它是AbstractUserDetailsAuthenticationProvider的一个实现。
  5. 加载用户详情DaoAuthenticationProvider会使用UserDetailsService接口来获取用户详情,这里具体使用了InMemoryUserDetailsManager实现。
  6. UserDetailsService返回用户信息UserDetailsService会加载用户的详细信息并返回一个UserDetails对象。
  7. 密码比对DaoAuthenticationProvider会使用PasswordEncoder来比对提交的密码和UserDetails中存储的密码。
  8. 构建经过认证的Authentication对象:如果用户凭据有效,AuthenticationProvider会构建一个包含用户权限的已认证Authentication对象。
  9. 保存Authentication对象:成功认证后,认证对象会被存入SecurityContext中,以供后续请求使用。
  10. 更新SecurityContext:最后,SecurityContextHolder的上下文会被更新,以包含新的认证信息。

3. SpringSecurity实现流程

原理上其实就是一个过滤器链,内部包含了各种功能的过滤器

  1. UsernamePasswordAuthentiationFilter : 负责处理在登录页面填写的用户名密码请求登录。
  2. ExceptionTranslationFilter: 认证授权这个流程出现的异常都会被这个过滤器所捕获。
  3. FilterSecurityInterceptor : 这个过滤器主要是判断授权的功能。

4. 解决方案

Untitled

登录

1️⃣ 自定义UserDetailService , 在这个实现类中去查询数据库

2️⃣ 自定义登录接口,调用ProviderManager的方法进行认证,如果生成通过生成JWT,把用户信息存入Redis中

校验

1️⃣ 定义基于JWT的认证过滤器

1️⃣ 获取Token

2️⃣ 解析Token,获取其中的UserID

3️⃣ 从Redis中获取用户信息

4️⃣ 存入SecurityContextHolder

Untitled

5. Spring Security密码加密存储

Spring Security 明文存储前面加 {noop} 采用默认加密

实际项目中不会吧密码明文存储到数据库中。

默认使用的PasswordEncoder 要求数据库的密码格式为:{id}password。它会根据id去判断密码的加密方式,但是一般不会采用这种方式,所以就需要替换PasswordEncoder.

一般使用Spring Security中的BCryptPasswordEncoder

Untitled

在使用BCyptPasswordEncoder加密的时候,虽然加密的原文是一致的,但是由于每次生成的盐值不同,所以每次产生的密文也不相同

BCyptPasswordEncoder加密格式为: $2a$10$ + 盐(22位) + 密文

6. 登录接口

自定义登录接口的话肯定是要定义Controller

对于登录接口肯定是要Spring Security进行放行操作的。需要在对应的Spring Security配置类里面进行相应的配置,重写configure方法

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        //关闭csrf
        .csrf().disable()
        //不通过Session获取SecurityContext
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        // 对于登录接口 允许匿名访问
        .antMatchers("/user/login").anonymous()
        // 除上面外的所有请求全部需要鉴权认证
        .anyRequest().authenticated();
}

实际上的操作实在Service当中

 		/**
     * 用户登录
     * @param user
     * @return
     */
    @Override
    public ResponseResult login(User user) {

//        AuthenticationManager 进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

//        如果认证没有通过,给出对应的提示
        if(Objects.isNull(authenticate)){
           throw new RuntimeException("登录失败");
        }

//        如果认证通过了,使用UserID生成一个JWT JWT存入ResponseResult进行返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);

//        把完整的用户信息存入Redis userId作为key
        redisCache.setCacheObject("login:" + userId, loginUser);
        return new ResponseResult(200, "登录成功", map);
    }

7. 退出登录

退出登录实际上就是携带之前的Token登录访问,现在的用户相当于是一个未认证的状态,需要重新登录,可以将SecurityContextHolder中的对象删除掉

当用户携带着他的Token访问注销接口的时候,服务器会获取到SecurityContextHolder中的userId, 然后在Redis中删除对应的用户信息,当用户再次携带之前的Token访问其他服务的时候,会被第一个JwtToken拦截器解析出Token中的用户ID,在Redis中查询不到,抛出异常,授权不成功

8. 授权

在Spring Security中,会使用默认的FilterSecurityInterceptor类进行权限校验。在FilterSecirutyInterceptor中会从SecurityContextHolder中获取其中的Authentication, 然后获取其中的信息, 当前用户是否拥有访问当前资源所需的权限。

所以在项目中只需要把当前登录用户的权限信息也存入Authentication

  1. 将用户的权限信息封装到Authentication这个对象当中
  2. 设置对应接口的一些访问权限
  3. 实际的权限信息应该从数据库进行一个查询

Untitled

关于这个注解判断用户是否有hasAuthority()括号里面的权限,实际上是执行下面的这个方法,用户必须拥有sys:user:info这个权限才能够访问这个接口

Untitled

8. 权限

RBAC权限模型(Role-Based Access Control) 基于角色的权限控制。这是目前最常杯卡法这使用也是相对容易,通用的权限模型。

一个用户可以有多个权限,但是如果只是用用户表以及权限表的话就会有一个问题,当有一个复杂的系统用户肯定是非常的多,那给每一个用户设置权限的时候就会显得非常的麻烦,这个权限粒度是非常小的,所以一个一个配置的话就会非常的麻烦,甚至产生数据冗余的问题。所以可以一次性配置一组权限信息,一系列的权限信息直接配置给用户,所以引入了一个角色的概念

  1. 用户表——user
  2. 权限表——menu
  3. 角色表(权限组)——role

权限和角色是有关联的。一个角色是可以具有多个权限的,同时一个权限是可以对应多个角色,其中是一个多对多的关系。需要一个角色权限关联表(role_menu)

  1. 角色权限关联表——role_menu

同理,用户表和角色表也是多对多的关系,这也需要一个关联表: 用户角色关联表(user-role)

  1. 用户角色关联表——user_role
SELECT DISTINCT
	m.perms 
FROM
	sys_user_role ur
	LEFT JOIN sys_role r ON ur.role_id = r.id
	LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
	LEFT JOIN sys_menu m ON m.id = rm.menu_id 
WHERE
	user_id = 1 
	AND r.`status` = 0 
	AND m.`status` = 0
posted @   叫授_pront  阅读(147)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示