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

    用户登录

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

      .antMatchers("/auth/login", "/captchaImage").anonymous()
      BCryptPasswordEncoder 密码加密方式

    2. POST 登录接口 /auth/login

      调用 AuthorizationController.login 用户登录接口
      做入参、图形验证码等验证。

    3. 实现 UserDetailsService 接口

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

    4. 密码验证

      AuthorizationService.login
      调用 authenticationManager.authenticate(authenticationToken) 看密码是否正确
      可以在此集合 Redis 做失败次数逻辑处理

    5. 通过JWT 生成 Token

      调用 jwtUtil.generateToken(userId) 生成Token令牌
      将 用户信息放入 Redis
      剔除其它已登录的用户(如果需要)

    6. 返回Map对象给前端

    接口权限认证

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

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

    2. 接口权限配置

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

    3. 访问权限控制

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

    用户登录代码

    image

    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 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 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 perms = new HashSet<>();
            // 管理员拥有所有权限
            if (user.isAdmin()) {
                perms.add("*:*:*");
            } else {
                perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
            }
            return new LoginUser(user, perms);
        }
    }
    

    接口权限认证代码

    image
    image

    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 authorities = loginUser.getPermissions();
            String[] perms = permissions.split(PERMISSION_DELIMETER);
            boolean hasPerms = Arrays.stream(perms).anyMatch(authorities::contains);
            //是Admin权限 或者 拥有接口所需权限时
            return permissions.contains(ALL_PERMISSION) || hasPerms;
        }
    }
    

    image
    image

    详细代码见:https://gitee.com/VipSoft/VipBoot/tree/develop/vipsoft-security
    源代码摘自:若依后台管理系统

  • 相关阅读:
    springboot中Configuration注解和Component注解功能的区别和联系?
    Rust 登上了开源头条「GitHub 热点速览」
    国产开源无头CMS,MyCms v4.7 快捷生成接口开发后台
    【项目搭建】SpringBoot多模块项目搭建
    《模拟联合国2.9—团队协作》
    工作记录-------java文件的JVM之旅(学习篇)---好理解
    PID整定方法
    【react】react 使用 Context 的简单示例
    Java版工程行业管理系统源码-专业的工程管理软件- 工程项目各模块及其功能点清单
    【十问十答】回归模型知识点
  • 原文地址:https://www.cnblogs.com/vipsoft/p/17349031.html