前后端分离认证实践指南:Spring Security和JWT详解(下)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 前后端分离认证实践指南:Spring Security和JWT详解

8、过滤器

我们登陆成功,就可以在登陆状态随意访问其他接口了,只需要验证请求头中的token是否合法,是否在redis里面有数据即可

下面开始编写filter

/**
 * @author: QiJingJing
 * @create: 2022/4/5
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("token");
        if(!StringUtils.hasText(token)){
            // 如果没有token则进行放行
            filterChain.doFilter(request,response);
            return;
        }
            // 解析token
            String userId;
            try {
                Claims claims = JwtUtil.parseJwt(token);
                userId = claims.getSubject();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("token非法");
            }
            // 从redis中获取用户信息
        UserDetailsImpl userDetails = redisCache.getCacheObject("login" + userId);
            if(Objects.isNull(userDetails)){
                throw new RuntimeException("用户未登录");
            }
            // 存入SecurityContextHolder
            //  获取权限信息,封装到Authentication
            SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities()));
        // 放行
        filterChain.doFilter(request,response);
    }
}

然后把过滤器添加到配置类SecurityConfig里面,继续添加以下代码

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

这个时候我们可以携带token信息访问hello接口,成功访问到信息

9、退出登陆(redis里面删除用户数据即可),对应的LoginController,LoginService、LoginServiceImpl中添加以下代码

@RestController
public class LoginController {
    @RequestMapping("user/logout")
    public ResponseResult logout(){
        return loginService.logout();
    }
}
public interface LoginService {
    ResponseResult logout();
}
@Service
public class LoginServiceImpl implements LoginService {
    @Override
    public ResponseResult logout() {
        // 获取SecurityContextHolder中的用户id
        UsernamePasswordAuthenticationToken authentication =(UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
        UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
        Long id = user.getUser().getId();
        // 删除redis中的值
        redisCache.deleteObject("login"+id);
        return new ResponseResult<>(200,"注销成功");
    }
}

测试注销

成功注销。

授权

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验,在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息来判断当前用户是否具有某种权限。

首先我们需要在配置类SecurityConfig上开启相关配置,添加一个注解

@EnableGlobalMethodSecurity(prePostEnabled=true)

**具体实现:**这些权限我们要从数据库里面进行获取,基于RBAC权限模型来实现。

RBAC:RBAC权限模型(Role-Based Access Control) 基于角色的权限控制。这是目前最被开发者使用也是相对易用、通用的权限模型

准备工作:创建所需的权限表、角色表、权限角色关联表、用户角色关联表

sql代码如下:

CREATE TABLE `sys_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `menuName` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
  `path` varchar(200) DEFAULT NULL COMMENT '路由地址',
  `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '组件路径',
  `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示1隐藏)',
  `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
  `createBy` bigint DEFAULT NULL,
  `createTime` datetime DEFAULT NULL,
  `updateBy` bigint DEFAULT NULL,
  `updateTime` datetime DEFAULT NULL,
  `delFalg` int DEFAULT '0' COMMENT '是否删除(0未删除,1已删除)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='权限表';
CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL,
  `roleKey` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
  `status` char(1) DEFAULT '0' COMMENT '角色状态(0正常,1停用)',
  `createBy` bigint DEFAULT NULL,
  `createTime` datetime DEFAULT NULL,
  `updateBy` bigint DEFAULT NULL,
  `updateTime` datetime DEFAULT NULL,
  `delFalg` int DEFAULT '0' COMMENT '是否删除(0未删除,1已删除)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
CREATE TABLE `sys_role_menu` (
  `roleId` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `menuId` bigint NOT NULL DEFAULT '0' COMMENT '菜单id',
  PRIMARY KEY (`roleId`,`menuId`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';
CREATE TABLE `sys_user_role` (
  `userId` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `roleId` bigint NOT NULL DEFAULT '0' COMMENT '角色id',
  PRIMARY KEY (`userId`,`roleId`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

表中自行添加数据信息:

我们的perms字段就是存储的权限信息字符串,只需要根据用户id把perms查询出来就可以。

编写Menu实体类:

@AllArgsConstructor
@NoArgsConstructor
@Data
@TableName("sys_menu")
public class Menu {
    @TableId
    private Long id;
    @TableField("menuName")
    private String menuName;
    @TableField("path")
    private String path;
    @TableField("component")
    private String component;
    @TableField("visible")
    private String visible;
    @TableField("status")
    private String status;
    @TableField("icon")
    private String icon;
    @TableField("createBy")
    private Long createBy;
    @TableField("createTime")
    private Date createTime;
    @TableField("updateBy")
    private Long updateBy;
    @TableField("updateTime")
    private Date updateTime;
    @TableField("delFlg")
    private int delFlg;
    @TableField("remark")
    private String remark;
}

编写MenuMapper

@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
    List<String> selectPermsByUserId(@Param("userId") Long userId);
}

编写MenuMapper.xml文件(resources/mapper/)目录下面

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.lili.mapper.MenuMapper">
    <select id="selectPermsByUserId" resultType="String">
        SELECT DISTINCT sm.`perms`
        FROM sys_user_role sur
                 LEFT JOIN `sys_role` sr
                           ON sur.`roleId` = sr.`id`
                 LEFT JOIN `sys_role_menu` srm
                           ON srm.`roleId` = sr.`id`
                 LEFT JOIN `sys_menu` sm
                           ON sm.`id` = srm.`menuId`
        WHERE userId = #{userId}
          AND sr.`status` = 0
          AND sm.`status` = 0
    </select>
</mapper>

先在测试类测试是否可以正确查询到权限

@SpringBootTest
class Springsecurity1ApplicationTests {
    @Autowired
    MenuMapper menuMapper;
    @Test
    void contextLoads() {
        menuMapper.selectPermsByUserId(1L).forEach(System.out::println);
    }
}

结果:

system:dept:list
system:text:list

到这一步,我们的sql代码就编写完成了,继续其他工作。

UserDetailsServiceImpl代码里面完成我们前面没有完成的查询用户权限的代码,补充之后完整代码如下:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Autowired
    MenuMapper menuMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername,username);
        User user = userMapper.selectOne(queryWrapper);
        // 如果没有用户,就抛出异常
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码错误");
        }
        // 查询用户对应的权限信息
        List<String> permissions = menuMapper.selectPermsByUserId(user.getId());
        // 把数据封装为UserDetails返回
        return new UserDetailsImpl(user,permissions);
    }
}

并且要把权限信息封装到UserDetailsImpl对象中,所以完整UserDetailImpl代码如下

@Data
@NoArgsConstructor
public class UserDetailsImpl implements UserDetails {
    private User user;
    private List<String> permissions;
    /**
     * 不进行序列化
     */
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;
    public UserDetailsImpl(User user,List<String> permissions){
        this.user = user;
        this.permissions = permissions;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities != null){
            return authorities;
        }
        return permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }
    @Override
    public String getPassword() {
        return user.getPassword();
    }
    @Override
    public String getUsername() {
        return user.getUsername();
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

给我们需要的接口信息加上对应需要的权限

@RestController
public class HelloController {
   @PreAuthorize("hasAuthority('system:dept:list')")
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
}

运行测试:首先正常登陆用户,然后携带token去访问/hello接口

如果登陆成功,会将用户认证和权限信息存入redis,我们在获取redis里面数据的地方打个断点(JwtAuthenticationTokenFilter类里面),看是否可以获取到权限信息

首先进行登陆

然后携带这个token,去访问hello接口

我们可以看到,reids里面有这个权限信息。放行即可

成功访问以上接口,我们可以继续测试一个没有对应权限的,比如id为2的用户,首先进行登陆

携带token进行访问:

可以看到这个用户的权限只有system:text:list,没有system:dept:list自然不能成功访问,我们放行即可

自然也就访问不了对应的数据了。

统一异常处理

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到,在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成AuthenticationException调用AuthenticationEntryPoint对象的方法去进行异常处理。

如果是授权过程出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法进行异常处理。

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可

认证异常实现类

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用户认证失败,请重新登陆");
        String s = JSON.toJSONString(result);
        // 处理异常
        WebUtils.renderString(response,s);
    }
}

授权异常实现类

@Component
public class AccessDeniedHandlerImpl implements org.springframework.security.web.access.AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "你的权限不足");
        String s = JSON.toJSONString(result);
        WebUtils.renderString(response,s);
    }
}

配置给SpringSecurity配置类,原有代码进行添加以下代码

@EnableGlobalMethodSecurity(prePostEnabled=true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    AccessDeniedHandlerImpl accessDeniedHandler;
    @Autowired
    AuthenticationEntryPointImpl authenticationEntryPoint;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置异常处理器
        http.exceptionHandling()
                //认证失败过滤器
                .authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
    }
}

测试登陆失败

测试授权失败(例如id为2没有访问hello接口的权限)

跨域

浏览器出于安全考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的,同源策略要求源相同才能正常通信,即协议、域名、端口号都完全一致。

前后端分离项目肯定会存在跨域请求的问题。所以需要进行配置处理

编写一个配置类

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 摄制允许的请求方式
                .allowedMethods("GET","POST","DELETE","PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域访问时间
                .maxAge(3600);
    }
}

在SpringSecurity配置类开启跨域访问

原有代码继续添加以下配置

@EnableGlobalMethodSecurity(prePostEnabled=true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 允许跨域
        http.cors();
    }
}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
27天前
|
安全 Java 数据库
安全无忧!在 Spring Boot 3.3 中轻松实现 TOTP 双因素认证
【10月更文挑战第8天】在现代应用程序开发中,安全性是一个不可忽视的重要环节。随着技术的发展,双因素认证(2FA)已经成为增强应用安全性的重要手段之一。本文将详细介绍如何在 Spring Boot 3.3 中实现基于时间的一次性密码(TOTP)双因素认证,让你的应用安全无忧。
63 5
|
26天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
130 1
|
10天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
88 62
|
5天前
|
JSON 安全 数据安全/隐私保护
Python认证新风尚:OAuth遇上JWT,安全界的时尚Icon👗
在当今互联网世界中,数据安全和隐私保护至关重要。Python 作为 Web 开发的主流语言,其认证机制也在不断进步。OAuth 2.0 和 JSON Web Tokens (JWT) 是当前最热门的安全认证方案,不仅保障数据安全传输,还简化用户认证流程。本文介绍如何在 Python 中结合 OAuth 2.0 和 JWT,打造一套既安全又高效的认证体系。通过 Flask-HTTPAuth 和 PyJWT 等库,实现授权和验证功能,确保每次请求的安全性和便捷性。
17 3
|
8天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
21 2
|
12天前
|
JSON 算法 安全
JWT Bearer 认证在 .NET Core 中的应用
【10月更文挑战第30天】JWT(JSON Web Token)是一种开放标准,用于在各方之间安全传输信息。它由头部、载荷和签名三部分组成,用于在用户和服务器之间传递声明。JWT Bearer 认证是一种基于令牌的认证方式,客户端在请求头中包含 JWT 令牌,服务器验证令牌的有效性后授权用户访问资源。在 .NET Core 中,通过安装 `Microsoft.AspNetCore.Authentication.JwtBearer` 包并配置认证服务,可以实现 JWT Bearer 认证。具体步骤包括安装 NuGet 包、配置认证服务、启用认证中间件、生成 JWT 令牌以及在控制器中使用认证信息
|
27天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用
【10月更文挑战第8天】本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,通过 Spring Initializr 创建并配置 Spring Boot 项目,实现后端 API 和安全配置。接着,使用 Ant Design Pro Vue 脚手架创建前端项目,配置动态路由和菜单,并创建相应的页面组件。最后,通过具体实践心得,分享了版本兼容性、安全性、性能调优等注意事项,帮助读者快速搭建高效且易维护的应用框架。
38 3
|
6天前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
23 0
|
3月前
|
SQL Java 测试技术
在Spring boot中 使用JWT和过滤器实现登录认证
在Spring boot中 使用JWT和过滤器实现登录认证
211 0
|
16天前
|
JSON 安全 算法