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

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

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


相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: