【Java杂货铺】用Security做权限极简入门

原来大多数单体项目都是用的shiro,随着分布式的逐渐普及以及与Spring的天生自然的结合。Spring Security安全框架越受大家的青睐。本文会教你用SpringSecurity设计单项目的权限,关于如何做分布式的权限,后续会跟进。

为什么选择SpringSecurity?

现如今,在JavaWeb的世界里Spring可以说是一统江湖,随着微服务的到来,SpringCloud可以说是Java程序员必须熟悉的框架,就连阿里都为SpringCloud写开源呢。(比如大名鼎鼎的Nacos)作为Spring的亲儿子,SpringSecurity很好的适应了了微服务的生态。你可以非常简便的结合Oauth做认证中心服务。本文先从最简单的单体项目开始,逐步掌握Security。更多可达官方文档

准备

我准备了一个简单的demo,具体代码会放到文末。提前声明,本demo没有用JWT,因为我想把token的维护放到服务端,更好的维护过期时间。(当然,如果将来微服务认证中心的形式,JWT也可以做到方便的维护过期时间,不做过多讨论)如果想了解Security+JWT简易入门,请戳

本项目结构如下

另外,本demo使用了MybatisPlus、lombok。

核心代码

首先需要实现两个类,一个是UserDetails的实现类SecurityUser,一个是UserDetailsService的实现类SecurityUserService。

**
 * Security 要求需要实现的User类
 * */
@Data
public class SecurityUser implements UserDetails {
    @Autowired
    private SysRoleService sysRoleService;
    //用户登录名(注意此处的username和SysUser的loginName是一个值)
    private String username;
    //登录密码
    private String password;
    //用户id
    private SysUser sysUser;
    //该用户的所有权限
    private List<SysMenu> sysMenuList;
    /**构造函数*/
    public SecurityUser(SysUser sysUser){
        this.username = sysUser.getLoginName();
        this.password = sysUser.getPassword();
        this.sysUser = sysUser;
    }
    public SecurityUser(SysUser sysUser,List<SysMenu> sysMenuList){
        this.username = sysUser.getLoginName();
        this.password = sysUser.getPassword();
        this.sysMenuList = sysMenuList;
        this.sysUser = sysUser;
    }
    /**需要实现的方法*/
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for(SysMenu menu : sysMenuList) {
            authorities.add(new SimpleGrantedAuthority(menu.getPerms()));
        }
        return authorities;
    }
    @Override
    public String getPassword() {
        return this.password;
    }
    @Override
    public String getUsername() {
        return this.username;
    }
    //默认账户未过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    //默认账户没有带锁
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    //默认凭证没有过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    //默认账户可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}

这个类包含着某个请求者的信息,在Security中叫做主体。其中这个方法是必须实现的,可以获取用户的具体权限。我们这边权限的颗粒度达到了菜单级别,而不是很多开源项目中角色那级别,我觉得颗粒度越细越方便(个人觉得...)

/**
 * Security 要求需要实现的UserService类
 * */
@Service
public class SecurityUserService implements UserDetailsService{

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private HttpServletRequest httpServletRequest;

    @Override
    public SecurityUser loadUserByUsername(String loginName) throws UsernameNotFoundException {
        LambdaQueryWrapper<SysUser> condition = Wrappers.<SysUser>lambdaQuery().eq(SysUser::getLoginName, loginName);
        SysUser sysUser = sysUserService.getOne(condition);
        if (Objects.isNull(sysUser)){
            throw new UsernameNotFoundException("未找到该用户!");
        }
        Long projectId = null;
        try{
            projectId = Long.parseLong(httpServletRequest.getHeader("projectId"));
        }catch (Exception e){

        }
        SysMenuModel sysMenuModel;
        if (sysUser.getUserType()){
            sysMenuModel = new SysMenuModel();
        }else {
            sysMenuModel = new SysMenuModel().setUserId(sysUser.getId());
        }
        sysMenuModel.setProjectId(projectId);
        List<SysMenu> menuList = sysMenuService.getList(sysMenuModel);
        return new SecurityUser(sysUser,menuList);
    }
}

显而易见,这个类实现了唯一的方法loadUserByUsername,从而可以拿到某用户的所有权限,并生成主体,在后面的filter中就可以见到他的作用了。

在看配置和filter之前,还有一个类需要说明一下,此类提供方法,可以让用户未登录、或者token失效的情况下进行统一返回。

@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    private static final long serialVersionUID = 1L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"token失效,请登陆后重试");
    }
}

ok,接下来看配置,实现了WebSecurityConfigurerAdapter的SecurityConfig类,特别说明,本demo算是前后端分离的前提下写的,所以实现过多的方法,其实这个类可以实现三个方法,具体请戳

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
    @Autowired
    SecurityFilter securityFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //禁止csrf
                .csrf().disable()
                //异常处理
                .exceptionHandling().authenticationEntryPoint(securityAuthenticationEntryPoint).and()
                //Session管理方式
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                //开启认证
                .authorizeRequests()
                .antMatchers("/login/login").permitAll()
                .antMatchers("/login/register").permitAll()
                .antMatchers("/login/logout").permitAll()
                .anyRequest().authenticated();
        http
                .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

异常处理就是上面那个类,Session那几种管理方式我在那篇Security+JWT的文章中也有所讲解,比较简单,然后是几个不用验证的登录路径,剩下的都需要经过我们下面这个filter。

@Slf4j
@Component
public class SecurityFilter extends OncePerRequestFilter {

    @Autowired
    SecurityUserService securityUserService;
    @Autowired
    SysUserService sysUserService;
    @Autowired
    SysUserTokenService sysUserTokenService;

    /**
     * 认证授权
     * */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        log.info("访问的链接是:{}",httpServletRequest.getRequestURL());
        try {
            final String token = httpServletRequest.getHeader("token");
            LambdaQueryWrapper<SysUserToken> condition = Wrappers.<SysUserToken>lambdaQuery().eq(SysUserToken::getToken, token);
            SysUserToken sysUserToken = sysUserTokenService.getOne(condition);
            if (Objects.nonNull(sysUserToken)){
                SysUser sysUser = sysUserService.getById(sysUserToken.getUserId());
                if (Objects.nonNull(sysUser)){
                    SecurityUser securityUser = securityUserService.loadUserByUsername(sysUser.getLoginName());
                    //将主体放入内存
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    //放入内存中去
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }catch (Exception e){
            log.error("认证授权时出错:{}", Arrays.toString(e.getStackTrace()));
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

判断用户是否登录,就是从数据库中查看是否有未过期的token,如果存在,就把主体信息放进到项目的内存中去,特别说明的是,每个请求链结束,SecurityContextHolder.getContext()的数据都会被clear的,所以,每次请求的时候都需要set。

以上就完成了Security核心的创建,为了业务代码方便获取内存中的主体信息,我特意加了一个获取用户信息的方法

/**
 * 获取Security主体工具类
 * @author pjjlt
 * */
public class SecurityUserUtil {
    public static SysUser getCurrentUser(){
        SecurityUser securityUser = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (Objects.nonNull(securityUser) && Objects.nonNull(securityUser.getSysUser())){
            return securityUser.getSysUser();
        }
        return null;
    }
}

业务代码

以上是Security核心代码,下面简单加两个业务代码,比如登录和某个接口的权限访问测试。

万物之源登录登出

首先,不被filter拦截的那三个方法注册、登录、登出,我都写在了moudle.controller.LoginController这个路径下,注册就不用说了,就是一个insertUser的方法,做好判断就好,密码通过AES加个密。

下面看下登录代码,controller层就不说了,反正就是个验参。

    /**
     * 登录,返回登录信息,前端需要缓存
     * */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public JSONObject login(SysUserModel sysUserModel) throws Exception{
        JSONObject result = new JSONObject();
        //1. 验证账号是否存在、密码是否正确、账号是否停用
        Wrapper<SysUser> sysUserWrapper = Wrappers.<SysUser>lambdaQuery()
                .eq(SysUser::getLoginName,sysUserModel.getLoginName()).or()
                .eq(SysUser::getEmail,sysUserModel.getEmail());
        SysUser sysUser = baseMapper.selectOne(sysUserWrapper);
        if (Objects.isNull(sysUser)){
            throw new Exception("用户不存在!");
        }
        String password = CipherUtil.encryptByAES(sysUserModel.getPassword());
        if (!password.equals(sysUser.getPassword())){
            throw new Exception("密码不正确!");
        }
        if (sysUser.getStatus()){
            throw new Exception("账号已删除或已停用!");
        }
        // 2.更新最后登录时间
        sysUser.setLoginIp(ServletUtil.getClientIP(request));
        sysUser.setLoginDate(LocalDateTime.now());
        baseMapper.updateById(sysUser);
        // 3.封装token,返回信息
        String token = UUID.fastUUID().toString().replace("-","");
        LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeSeconds);
        SysUserToken sysUserToken = new SysUserToken()
                .setToken(token).setUserId(sysUser.getId()).setExpireTime(expireTime);
        sysUserTokenService.save(sysUserToken);
        result.putOpt("token",token);
        result.putOpt("expireTime",expireTime);
        return result;
    }

首先验证下用户是否存在,登录密码是否正确,然后封装token,值得一提的是,我并没有从数据库(sysUserToken)中获取用户已经登录的token,然后更新过期时间的形式做登录,而是每次登录都获取新token,这样就可以做到多端登录了,后期还可以做账号登录数量的控制。

然后就是登出,删除库中存在的token

    /**
     * 登出,删除token
     * */
    @Override
    public void logout() throws Exception{
        String token = httpServletRequest.getHeader("token");
        if (Objects.isNull(token)){
            throw new LoginException("token不存在",ResultEnum.LOGOUT_ERROR);
        }
        LambdaQueryWrapper<SysUserToken> sysUserWrapper = Wrappers.<SysUserToken>lambdaQuery()
                .eq(SysUserToken::getToken,token);
        baseMapper.delete(sysUserWrapper);
    }

权限验证

这边我维护了两个账号,一个是超级管理员majian,拥有所有权限。一个是普通人员_pjjlt,只有一些权限,我们看一下访问接口的效果。

我们访问的接口是moudle.controller.LoginController路径下的

@PreAuthorize("hasAnyAuthority('test')")
@GetMapping("test")
public String test(){
    return "test";
}

其中hasAnyAuthority(‘test‘)就是权限码

我们模拟用不同账号访问,就是改变请求header中的token值,就是登录阶段返回给前端的token。

首先是超级管理员验证

然后是普通管理员访问

接着没有登录(token不存在或者已过期)访问

demo地址

https://github.com/majian1994/easy-file-back

结束语

本文简单讲解了,主要是将Security相关的东西,具体实现角色的三要素,用户、角色、权限(菜单)可以看我的代码,都写完测完了,本来想写个文档管理系统,帮助我司更好的管理接口文档,but有位小伙伴找了一个不错的开源的了,所以这代码就成了我的一个小demo。

原文地址:https://www.cnblogs.com/pjjlt/p/11829426.html

时间: 2024-12-19 12:08:44

【Java杂货铺】用Security做权限极简入门的相关文章

反向传播神经网络极简入门

反向传播神经网络极简入门 我一直在找一份简明的神经网络入门,然而在中文圈里并没有找到.直到我看到了这份162行的Python实现,以及对应的油管视频之后,我才觉得这就是我需要的极简入门资料.这份极简入门笔记不需要突触的图片做装饰,也不需要赘述神经网络的发展历史:要推导有推导,要代码有代码,关键是,它们还对得上.对于欠缺的背景知识,利用斯坦福大学的神经网络wiki进行了补全. 单个神经元 神经网络是多个“神经元”(感知机)的带权级联,神经网络算法可以提供非线性的复杂模型,它有两个参数:权值矩阵{W

.NET Core实战项目之CMS 第七章 设计篇-用户权限极简设计全过程

写在前面 这篇我们对用户权限进行极简设计并保留其扩展性.首先很感谢大家的阅读,前面六章我带着大家快速入门了ASP.NET Core.ASP.NET Core的启动过程源码解析及配置文件的加载过程源码解析并引入依赖注入的概念.Git的快速入门.Dapper的快速入门.Vue的快速入门.不知道大伙掌握的怎么样了!如果你有兴趣的话可以加入我们的.NET Core实战项目群637326624跟更多的小伙伴共同进行交流下. 接下来我们就正式进入.NET Core实战项目之CMS的设计篇了.在设计篇呢,我们

Nginx 极简入门教程!

上篇文章和大家聊了 Spring Session 实现 Session 共享的问题,有的小伙伴看了后表示对 Nginx 还是很懵,因此有了这篇文章,算是一个 Nginx 扫盲入门吧! 基本介绍 Nginx 是一个高性能的 HTTP 和反向代理 web 服务器,同时也提供了 IMAP/POP3/SMTP 服务. Nginx 是由伊戈尔·赛索耶夫为俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日. Nginx 特点是占有内存少,

Dockerfile极简入门与实践

前文中,罗列了docker使用中用到的基本命令 此文,将会对怎样使用Dockerfile去创建一个镜像做简单的介绍 Dockerfile命令 要开始编写Dockerfile,首先要对相关的命令有个清晰的认识 下面列出了部分Dockerfile命令的功能以及使用方法,供参考: 1. FROM Dockerfile的第一条指定必须是FROM,用于指定基础镜像. 用法: FROM [image]:[tag] 2. MAINTAINER 用于指定此Dockerfile维护者信息. 用法: MAINTAI

ActiveMQ + NodeJS + Stomp 极简入门

前提 安装ActiveMQ和Nodejs 测试步骤 1.执行bin\win32\activemq.bat启动MQ服务 2. 打开http://localhost:8161/admin/topics.jsp 用户名和密码都是 admin 3. 下载Stomp npm install stomp-client 4. js的测试代码 var Stomp = require('stomp-client'); var destination = '/topic/myTopic'; var client =

Express4+Mongodb极简入门实例

一.准备工作: 1.启动mongodb:bin目录下运行 2.在test数据库里插入一条数据: 二.正式开始: 1.通过应用生成器工具 express 快速创建一个应用的骨架,参考Express中文网http://www.expressjs.com.cn/starter/generator.html: 2.这里我创建了一个名叫firstapp的应用: 通过Express生成器就快速生成了如下的应用骨架: 3.express4默认以jade为模板,这里我改用ejs,在package.json文件中

Node.js 极简入门Helloworld版服务器例子

粗浅得很,纯属备忘. // 内置http模块,提供了http服务器和客户端功能(path模块也是内置模块,而mime是附加模块) var http=require("http"); // 创建服务器,创建HTTP服务器要调用http.createServer()函数,它只有一个参数,是个回调函数,服务器每次收到http请求后都会调用这个回调函数.服务器每收到一条http请求,都会用新的request和response对象触发请求函数. var server=http.createSer

通过极简模拟框架让你了解ASP.NET Core MVC框架的设计与实现[上篇]

<200行代码,7个对象--让你了解ASP.NET Core框架的本质>让很多读者对ASP.NET Core管道有了真实的了解.在过去很长一段时间中,有很多人私信给我:能否按照相同的方式分析一下MVC框架的设计与实现原理,希望这篇文章能够满足你们的需求.在对本章内容展开介绍之前,顺便作一下广告:<ASP.NET Core 3框架揭秘>已经开始销售,现时5折优惠还有最后4天,有兴趣的从这里入群购买. 目录一.Action元数据的解析     ActionDescriptor    

Java 线程第三版 第五章 极简同步技巧 读书笔记

一.能避免同步吗? 取得锁会因为以下原因导致成本很高: 取得由竞争的锁需要在虚拟机的层面上运行更多的程序代码. 要取得有竞争锁的线程总是必须等到锁被释放后. 1. 寄存器的效应 计算机有一定数量的主寄存器用来存储与程序有关的数据. 从逻辑上的观点来看,每个Thread都有自己的一组寄存器.当操作系统将某个Thread分配给CPU时,它会把该Thread特有的信息加载到CPU的寄存器中.在分配不同的Thread给CPU之前,它会将寄存器的信息存下来.所以Thread间绝不会共享保存在寄存器的数据.