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

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

liebian365 2024-11-22 17:16 3 浏览 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的集成。


相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: