SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
RDS DuckDB + QuickBI 企业套餐,8核32GB + QuickBI 专业版
简介: SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘

SpringBoot 集成 SpringSecurity + MySQL + JWT 无太多理论,直接盘

一般用于Web管理系统

可以先看 SpringBoot SpringSecurity 基于内存的使用介绍

本文介绍如何整合 SpringSecurity + MySQL + JWT

数据结构

数据库脚本:https://gitee.com/VipSoft/VipBoot/blob/develop/vipsoft-security/sql/Security.sql

常规权限管理数据结构设计,三张常规表:用户、角色、菜单,通过用户和角色的关系,角色和菜单(权限)的关系,实现用户和菜单(按钮)的访问控制权

用户登录

  1. SecurityConfig 中添加登录接口匿名访问配置

.antMatchers("/auth/login", "/captchaImage").anonymous()

BCryptPasswordEncoder 密码加密方式

  1. POST 登录接口 /auth/login

调用 AuthorizationController.login 用户登录接口

做入参、图形验证码等验证。

  1. 实现 UserDetailsService 接口

根据用户名,去数据库获取用户信息、权限获取等

  1. 密码验证

AuthorizationService.login

调用 authenticationManager.authenticate(authenticationToken) 看密码是否正确

可以在此集合 Redis 做失败次数逻辑处理

  1. 通过JWT 生成 Token

调用 jwtUtil.generateToken(userId) 生成Token令牌

将 用户信息放入 Redis

剔除其它已登录的用户(如果需要)

  1. 返回Map对象给前端

接口权限认证

  1. 获取request.getHeader中的token信息

AuthenticationTokenFilter.doFilterInternal

解析 Token 中的用户ID 去 Redis 缓存中获取用户信息

将信息赋到 SecurityContextHolder.getContext().setAuthentication(authenticationToken) 中,供权限验证获取用户信息使用, SecurityContextHolder使用了ThreadLocal机制来保存每个使用者的安全上下文

  1. 接口权限配置

UserController 类的方法上,加了 @PreAuthorize("@ps.hasAnyPermi('system:user:list')") 用来做权限控制

  1. 访问权限控制

PermissionService.hasAnyPermi 判断,用户所拥有的权限,是否包含 @PreAuthorize("@ps.hasAnyPermi('system:user:list')") 中配置的权限,包含则有权访问

用户登录代码

SecurityConfig

package com.vipsoft.web.config;
import com.vipsoft.web.security.AuthenticationEntryPointImpl;
import com.vipsoft.web.security.AuthenticationTokenFilter;
import com.vipsoft.web.security.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;
    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;
    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;
    /**
     * token认证过滤器
     */
    @Autowired
    private AuthenticationTokenFilter authenticationTokenFilter;
    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    /**
     * 强散列哈希加密实现
     * 必须 Bean 的形式实例化,否则会报 :Encoded password does not look like BCrypt
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }
    /**
     * 配置用户身份的configure()方法
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
    /**
     * 配置用户权限的configure()方法
     *
     * @param httpSecurity
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 禁用 CSRF,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 验证码captchaImage 允许匿名访问
                .antMatchers("/auth/login", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/webSocket/**"
                ).permitAll()
                // swagger 文档
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/*/api-docs").permitAll()
                .antMatchers("/druid/**").permitAll()
                // 放行OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 所有请求都需要认证
                .anyRequest().authenticated()
                //        .and().apply(this.securityConfigurerAdapter());
                .and()
                //设置跨域, 如果不设置, 即使配置了filter, 也不会生效
                .cors()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

AuthenticationController.login

public Map<String, Object> login(SysUser user) {
    String username = user.getUserName();
    String password = user.getPassword();
    Authentication authentication;
    try {
        //该方法会去调用UserDetailsServiceImpl.loadUserByUsername
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        authentication = authenticationManager.authenticate(authenticationToken);
    } catch (AuthenticationException ex) {
        Long incr = 3L; // Redis 实现
        if (incr > 5) {
            logger.error("{} 账户连续{}次登录失败,账户被锁定30分钟", username, incr);
            throw new LockedException("密码连续输入错误次数过多,账户已被锁定!");
        }
        throw new BadCredentialsException("您输入的用户名、密码或验证码不正确,为保证账户安全,连续5次输入错误,系统将锁定您的账户30分钟,当前剩余:" + (PASSOWRD_MAX_ERROR_COUNT - incr) + "次", ex);
    }
    SecurityContextHolder.getContext().setAuthentication(authentication);
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    String userId = loginUser.getUser().getUserId().toString();
    // 生成令牌
    String token = jwtUtil.generateToken(userId);
    Map<String, Object> resultMap = new HashMap();
    resultMap.put("AccessToken", token);
    resultMap.put("UserId", userId);
    // Redis 保存上线信息
    // UserAgent userAgent
    // 踢掉已登录用户
    return resultMap;
}

UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private ISysUserService userService;
    @Autowired
    private ISysMenuService menuService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = userService.selectUserByUserName(username);
        if (user == null) {
            logger.info("登录用户:{} 不存在.", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        } else if ("1".equals(user.getDelFlag())) {
            logger.info("登录用户:{} 已被删除.", username);
            throw new CustomException("对不起,您的账号:" + username + " 已被删除");
        } else if ("1".equals(user.getStatus())) {
            logger.info("登录用户:{} 已被停用.", username);
            throw new CustomException("对不起,您的账号:" + username + " 已停用");
        }
        Set<String> perms = new HashSet<>();
        // 管理员拥有所有权限
        if (user.isAdmin()) {
            perms.add("*:*:*");
        } else {
            perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
        }
        return new LoginUser(user, perms);
    }
}

接口权限认证代码

AuthenticationTokenFilter

@Component
public class AuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        LoginUser loginUser = jwtUtil.getLoginUser(request);
        if (loginUser != null && SecurityUtils.getAuthentication() == null) {
            jwtUtil.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            //SecurityContextHolder使用了ThreadLocal机制来保存每个使用者的安全上下文,确保PermissionService判断权限时可以获得当前LoginUser信息
            SecurityUtils.setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

定义权限验证类 PermissionService

package com.vipsoft.web.security;
import cn.hutool.core.util.StrUtil;
import com.vipsoft.web.utils.SecurityUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Arrays;
import java.util.Set;
/**
 * 自定义权限实现
 */
@Service("ps")
public class PermissionService {
    /**
     * 所有权限标识
     */
    private static final String ALL_PERMISSION = "*:*:*";
    /**
     * 管理员角色权限标识
     */
    private static final String SUPER_ADMIN = "admin";
    private static final String ROLE_DELIMETER = ",";
    private static final String PERMISSION_DELIMETER = ",";
    /**
     * 对用户请求的接口进行验证,看接口所需要的权限,当前用户是否包括
     *
     * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表,如:system:user:add,system:user:edit
     * @return 用户是否具有以下任意一个权限
     */
    public boolean hasAnyPermi(String permissions) {
        if (StrUtil.isEmpty(permissions)) {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getCurrentUser(); //去SecurityContextHolder.getContext()中获取登录用户信息
        if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions())) {
            return false;
        }
        Set<String> authorities = loginUser.getPermissions();
        String[] perms = permissions.split(PERMISSION_DELIMETER);
        boolean hasPerms = Arrays.stream(perms).anyMatch(authorities::contains);
        //是Admin权限 或者 拥有接口所需权限时
        return permissions.contains(ALL_PERMISSION) || hasPerms;
    }
}

详细代码见:https://gitee.com/VipSoft/VipBoot/tree/develop/vipsoft-security

源代码摘自:若依后台管理系统

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
人工智能 运维 Java
SpringBoot+MySQL实现动态定时任务
这是一个基于Spring Boot的动态定时任务Demo,利用spring-context模块实现任务调度功能。服务启动时会扫描数据库中的任务表,将任务添加到调度器中,并通过固定频率运行的ScheduleUpdater任务动态更新任务状态和Cron表达式。核心功能包括任务的新增、删除与Cron调整,支持通过ScheduledFuture对象控制任务执行。项目依赖Spring Boot 2.2.10.RELEASE,使用MySQL存储任务信息,包含任务基类ITask及具体实现(如FooTask),便于用户扩展运维界面以增强灵活性。
405 10
|
8月前
|
Ubuntu 关系型数据库 MySQL
MySQL源码编译安装
本文详细介绍了MySQL 8.0及8.4版本的源码编译安装全过程,涵盖用户创建、依赖安装、cmake配置、编译优化等步骤,并提供支持多Linux发行版的一键安装脚本,适用于定制化数据库部署需求。
2051 4
MySQL源码编译安装
|
10月前
|
NoSQL 关系型数据库 MySQL
在Visual Studio Code中设置MySQL源码调试环境
以上步骤涵盖了在VS Code中设置MySQL源码调试环境的主要过程,是一个相对高级的任务,旨在为希望建立强大开发和调试环境的开发者提供指引。遵循这些步骤,将可以利用VS Code强大的编辑和调试功能来深入理解和改进MySQL数据库的底层实现。
651 0
|
Java 关系型数据库 MySQL
SpringBoot 通过集成 Flink CDC 来实时追踪 MySql 数据变动
通过详细的步骤和示例代码,您可以在 SpringBoot 项目中成功集成 Flink CDC,并实时追踪 MySQL 数据库的变动。
3552 45
|
监控 Java 关系型数据库
Spring Boot整合MySQL主从集群同步延迟解决方案
本文针对电商系统在Spring Boot+MyBatis架构下的典型问题(如大促时订单状态延迟、库存超卖误判及用户信息更新延迟)提出解决方案。核心内容包括动态数据源路由(强制读主库)、大事务拆分优化以及延迟感知补偿机制,配合MySQL参数调优和监控集成,有效将主从延迟控制在1秒内。实际测试表明,在10万QPS场景下,订单查询延迟显著降低,超卖误判率下降98%。
558 5
|
关系型数据库 MySQL PHP
源码编译安装LAMP(HTTP服务,MYSQL ,PHP,以及bbs论坛)
通过以上步骤,你可以成功地在一台Linux服务器上从源码编译并安装LAMP环境,并配置一个BBS论坛(Discuz!)。这些步骤涵盖了从安装依赖、下载源代码、配置编译到安装完成的所有细节。每个命令的解释确保了过程的透明度,使即使是非专业人士也能够理解整个流程。
479 18
|
XML JavaScript Java
SpringBoot集成Shiro权限+Jwt认证
本文主要描述如何快速基于SpringBoot 2.5.X版本集成Shiro+JWT框架,让大家快速实现无状态登陆和接口权限认证主体框架,具体业务细节未实现,大家按照实际项目补充。
1034 11
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
640 1
深入实践springboot实战 蓄势待发 我不是雷锋 我是知识搬运工
springboot,说白了就是一个集合了功能的大类库,包括springMVC,spring,spring data,spring security等等,并且提供了很多和可以和其他常用框架,插件完美整合的接口(只能说是一些常用框架,基本在github上能排上名次的都有完美整合,但如果是自己写的一个框架就无法实现快速整合)。
|
Java 数据安全/隐私保护
Neo4j【付诸实践 01】SpringBoot集成报错org.neo4j.driver.exceptions.ClientException:服务器不支持此驱动程序支持的任何协议版本(解决+源代码)
Neo4j【付诸实践 01】SpringBoot集成报错org.neo4j.driver.exceptions.ClientException:服务器不支持此驱动程序支持的任何协议版本(解决+源代码)
953 1

热门文章

最新文章

推荐镜像

更多