百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

Springboot Security框架快速集成(适用大部分业务项目)

liebian365 2024-11-22 17:16 19 浏览 0 评论

请关注我!分享文章皆源于项目实践。坚持原创,共同进步。

概述

一提到用户认证框架,很多人第一时间可能想到shiro,因为它小巧和易于理解。而用户认证的另一框架Spring security,却因为它的庞大与复杂,往往让初学者望而却步。但随着springboot单体应用的到来,突然间发现Spring security框架集成原来也可以很简单。本文向大家分享Spring Security在springboot项目中如何快速集成。希望对有这类应用场景需求的你有所帮助和价值。

用户认证框架,一般分为用户认证和用户授权两部分。本文将按这两个点来介绍。

用户认证

用户认证,就是用户的身份信息证明。认证过程即判定用户在系统中是否合法、有效或存在。

针对用户认证流程,一般系统的常用做法:

用户输入账号和密码,提交登录请求。后端接口验证密码合法性。若合法,生成登录token,并把token和用户信息(用户id、用户名等)作为key-value缓存到redis。为提高安全性,可以把token通过jwt加密后返回前端。前端拿到token加密串本地缓存,后续请求放到header中一起传给后端。后端通过检查token是否合法和超时,来决定是否需要用户重新登录认证。

了解了用户认证的流程。我们来看看springboot项目中如何集成。

maven依赖添加

   <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

security bean config配置

/**
 * spring security配置
 */
@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 JwtAuthenticationTokenFilter authenticationTokenFilter;
    
    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity
                // CRSF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/captchaImage").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    
    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

上述配置类继承spring WebSecurityConfigurerAdapter父类,并覆盖对应方法。

添加@EnableGlobalMethodSecurity注解,prePostEnabled=true属性开启用户权限注解(后续涉及)。

UserDetailsServiceImpl用户信息服务(需自定义实现,后续涉及),定义业务用户账号信息,一般通过db获取。

AuthenticationEntryPointImpl认证失败处理类,需继承框架AuthenticationEntryPoint接口,定义认证失败时,返回数据格式(HttpServletResponse根据业务自行定义返回错误提示,这里不再赘述)。

LogoutSuccessHandlerImpl登出处理类,需继承框架LogoutSuccessHandler接口,定义用户成功登出是的业务逻辑,比如登录成功redis缓存清除、登出日志记录等。这里不再赘述。

JwtAuthenticationTokenFilter认证token检查过滤器,需继承spring框架OncePerRequestFilter接口。后续涉及。

configure(HttpSecurity httpSecurity)方法中配置url拦截处理,细节参见代码注释。其中注意authenticationTokenFilter认证检查拦截器要添加到框架默认UsernamePasswordAuthenticationFilter.class拦截器前,只有这样才能生效。

BCryptPasswordEncoder bean定义用户账号密码加密方式,采用SHA-256 +随机盐+密钥对密码进行加密。

configure(AuthenticationManagerBuilder auth)方法中定义用户认证时,用户信息的获取来源以及密码加密方式。


UserDetailsServiceImpl定义

@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    //业务service,从DB获取用户账号信息
    @Autowired
    private ISysUserService userService;

    //业务service,获取用户对于菜单权限信息
    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        SysUser user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已停用");
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user)
    {
        return new LoginUser(user, permissionService.getMenuPermission(user));
    }
}

继承框架UserDetailsService接口,并实现loadUserByUsername(String username)方法,通过用户名获取用户账号身份信息。注意该方法返回为框架定义UserDetails对象。这里是LoginUser业务自定义实现。以下为参考实现,可根据业务自行定义相关属性并实现接口UserDetails方法。

/**
 * 登录用户身份权限对象
 */
public class LoginUser implements UserDetails
{
    /**
     * 用户唯一标识
     */
    private String token;

    /**
     * 登陆时间
     */
    private Long loginTime;

    /**
     * 过期时间
     */
    private Long expireTime;

    /**
     * 登录IP地址
     */
    private String ipaddr;

    /**
     * 权限列表
     */
    private Set<String> permissions;

    /**
     * 用户信息
     */
    private SysUser user;

    public LoginUser(SysUser user, Set<String> permissions)
    {
        this.user = user;
        this.permissions = permissions;
    }

    @JsonIgnore
    @Override
    public String getPassword()
    {
        return user.getPassword();
    }

    @Override
    public String getUsername()
    {
        return user.getUserName();
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired()
    {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     * 
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked()
    {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     * 
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired()
    {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     * 
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isEnabled()
    {
        return true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        return null;
    }
    // 省略各属性set/get方法...
}

JwtAuthenticationTokenFilter定义

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    //业务service,获取用户登录token信息。
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        //从http header获取token,通过redis key=token值得到用户登录缓存信息。
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser))
        {
            //校验token是否失效
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

UsernamePasswordAuthenticationToken为框架定义认证类,通过构造参数把业务用户账号和授权信息建立关联。

最后设置通过框架工具类SecurityContextHolder.getContext().setAuthentication(authenticationToken);设置登录用户token上下文。该上下文若存在,表明用户认证状态有效,框架默认的认证检查filter会自动跳过。

最后定义用户登录接口认证判断。

/**
     * 登录验证
     * 
     * @param username 用户名
     * @param password 密码
     * @param code 验证码
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid)
    {
        //省略基本参数校验逻辑... 
        // 用户验证
        Authentication authentication = null;
        try
        {
            //authenticationManager是框架AuthenticationManager类,在上面config配置类有定义。
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                //用户名/密码错误逻辑
                throw new UserPasswordNotMatchException();
            }
            else
            {
                //失败异常逻辑
                throw new CustomException(e.getMessage());
            }
        }
        //获取认证成功用户信息
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token、缓存loginUser到redis、jwt加密token
        return tokenService.createToken(loginUser);
    }

至此用户认证部分的主要逻辑定义完成。各业务逻辑请跟进具体项目场景自行定义。

用户授权

用户授权,即用户认证成功(身份合法)后,针对系统功能所拥有的权限。一般为系统菜单和页面按钮访问权限。后端系统层面,实际就是一个个不同的api接口访问权限。这里通过框架预置的注解@PreAuthorize来定义不同接口访问能力。

以获取菜单列表为例。

/**
     * 获取菜单列表
     */
     //接口权限注解定义
    @PreAuthorize("@ss.hasPermi('system:menu:list')")
    @GetMapping("/list")
    public AjaxResult list(SysMenu menu)
    {
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        Long userId = loginUser.getUser().getUserId();
        List<SysMenu> menus = menuService.selectMenuList(menu, userId);
        return AjaxResult.success(menus);
    }

接口方法添加注解,设置相关权限定义:@PreAuthorize("@ss.hasPermi('system:menu:list')")

注解值为一个公共权限处理方法类,该方法接收一个权限字符串,只有拥有该字符串的用户才能访问该接口。

公共权限处理方法定义

@Service("ss")
public class PermissionService
{
  /**
     * 验证用户是否具备某权限
     * 
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission)
    {
        if (StringUtils.isEmpty(permission))
        {
            return false;
        }
        //http header获取token,并从redis中拿到用户登录缓存信息
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
        {
            return false;
        }
        Set<String> permissions = loginUser.getPermissions();
        //用户权限判断
        return permissions.contains(StringUtils.trim(permission));
    }
}  

总结

本文介绍了springboot security快速集成的方法,分为用户认证和用户授权两部分。集合框架接口定义业务数据来源和规则处理完成项目集成。

整个实现逻辑参考了开源项目RuoYi-Vue的代码实现。特别感谢项目作者!让我有机会从项目中了解spring security的集成。


相关推荐

“版本末期”了?下周平衡补丁!国服最强5套牌!上分首选

明天,酒馆战棋就将迎来大更新,也聊了很多天战棋相关的内容了,趁此机会,给兄弟们穿插一篇构筑模式的卡组推荐!老规矩,我们先来看10职业胜率。目前10职业胜率排名与一周前基本类似,没有太多的变化。平衡补丁...

VS2017 C++ 程序报错“error C2065:“M_PI”: 未声明的标识符&quot;

首先,程序中头文件的选择,要选择头文件,在文件中是没有对M_PI的定义的。选择:项目——>”XXX属性"——>配置属性——>C/C++——>预处理器——>预处理器定义,...

东营交警实名曝光一批酒驾人员名单 88人受处罚

齐鲁网·闪电新闻5月24日讯酒后驾驶是对自己和他人生命安全极不负责的行为,为守护大家的平安出行路,东营交警一直将酒驾作为重点打击对象。5月23日,东营交警公布最新一批饮酒、醉酒名单。对以下驾驶人醉酒...

Qt界面——搭配QCustomPlot(qt platform)

这是我第一个使用QCustomPlot控件的上位机,通过串口精确的5ms发送一次数据,再将读取的数据绘制到图表中。界面方面,尝试卡片式设计,外加QSS简单的配了个色。QCustomPlot官网:Qt...

大话西游2分享赢取种族坐骑手办!PK趣闻录由你书写

老友相聚,仗剑江湖!《大话西游2》2021全民PK季4月激燃打响,各PK玩法鏖战齐开,零门槛参与热情高涨。PK季期间,不仅各种玩法奖励丰厚,参与PK趣闻录活动,投稿自己在PK季遇到的趣事,还有机会带走...

测试谷歌VS Code AI 编程插件 Gemini Code Assist

用ClaudeSonnet3.7的天气测试编码,让谷歌VSCodeAI编程插件GeminiCodeAssist自动编程。生成的文件在浏览器中的效果如下:(附源代码)VSCode...

顾爷想知道第4.5期 国服便利性到底需优化啥?

前段时间DNF国服推出了名为“阿拉德B计划”的系列改版计划,截至目前我们已经看到了两项实装。不过关于便利性上,国服似乎还有很多路要走。自从顾爷回归DNF以来,几乎每天都在跟我抱怨关于DNF里面各种各样...

掌握Visual Studio项目配置【基础篇】

1.前言VisualStudio是Windows上最常用的C++集成开发环境之一,简称VS。VS功能十分强大,对应的,其配置系统较为复杂。不管是对于初学者还是有一定开发经验的开发者来说,捋清楚VS...

还嫌LED驱动设计套路深?那就来看看这篇文章吧

随着LED在各个领域的不同应用需求,LED驱动电路也在不断进步和发展。本文从LED的特性入手,推导出适合LED的电源驱动类型,再进一步介绍各类LED驱动设计。设计必读:LED四个关键特性特性一:非线...

Visual Studio Community 2022(VS2022)安装图文方法

直接上步骤:1,首先可以下载安装一个VisualStudio安装器,叫做VisualStudioinstaller。这个安装文件很小,很快就安装完成了。2,打开VisualStudioins...

Qt添加MSVC构建套件的方法(qt添加c++11)

前言有些时候,在Windows下因为某些需求需要使用MSVC编译器对程序进行编译,假设我们安装Qt的时候又只是安装了MingW构建套件,那么此时我们该如何给现有的Qt添加一个MSVC构建套件呢?本文以...

Qt为什么站稳c++GUI的top1(qt c)

为什么现在QT越来越成为c++界面编程的第一选择,从事QT编程多年,在这之前做C++界面都是基于MFC。当时为什么会从MFC转到QT?主要原因是MFC开发界面想做得好看一些十分困难,引用第三方基于MF...

qt开发IDE应该选择VS还是qt creator

如果一个公司选择了qt来开发自己的产品,在面临IDE的选择时会出现vs或者qtcreator,选择qt的IDE需要结合产品需求、部署平台、项目定位、程序猿本身和公司战略,因为大的软件产品需要明确IDE...

Qt 5.14.2超详细安装教程,不会来打我

Qt简介Qt(官方发音[kju:t],音同cute)是一个跨平台的C++开库,主要用来开发图形用户界面(GraphicalUserInterface,GUI)程序。Qt是纯C++开...

Cygwin配置与使用(四)——VI字体和颜色的配置

简介:VI的操作模式,基本上VI可以分为三种状态,分别是命令模式(commandmode)、插入模式(Insertmode)和底行模式(lastlinemode),各模式的功能区分如下:1)...

取消回复欢迎 发表评论: