springboot整合shiro——前后端分离模式(上篇)

980 阅读5分钟

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。

之前已经把我们的系统用户部分开发完了,详情见:

# springboot开发银行秒杀后端系统——用户篇(上篇)

# springboot开发银行秒杀后端系统——用户篇(下篇)

接下来我打算把shiro整合进我的项目里,因为我的老师跟我说年后有个项目需要用到shiro让我先学起来,干脆我就直接把shiro用在我的这个项目里面,先看了看官方文档了解了大概,然后我找了很多文章但是很少有涉及到前后端分离的,其中最大的问题就是这行代码

shiroFilterFactoryBean.setLoginUrl("/login");

这是在shiroConfig中配置登陆页面的,如果认证失败的话就会跳转到这个登陆页面,但是我一个后端哪来的登陆页面呢??理想的状态就是认证失败的话直接返回错误信息给前端然后让前端跳转到登陆页面去,所以我目前的解决办法就是重写了一下AuthenticatingFilter这个过滤器,如果有错误就直接返回错误信息给前端,而不是重定向到登陆页面,shiro有很多自带的过滤器但是我一般都是走这个过滤器所以直接重写这个就OK了。

在这之前先简单介绍一下整合的流程,shiro作为轻量级的安全框架用起来确实很方便,主要功能一个是认证(判断你登陆了没),另一个就是授权(判断你能不能干这个)。我们的用户表之前已经有了,涉及到授权的话还需要角色表和权限表,这也是授权的三要素

  • 权限
  • 角色
  • 用户 shiro的官方文档中介绍到他的权限的粒度可以划分的非常细致,简单来说就是把角色赋予用户,然后权限适合角色挂钩的,给你什么角色,你就拥有了这个角色的权限,可以进行对应的操作;当然也可以直接把权限给用户,我这里是按照官方文档写的只把权限分配个角色,然后将用户与角色关联,用户可以传递性地“拥有”分配给他们角色的权限。可以看作是树状的继承的关系,这样分配权限粒度确实很细致。

image.png 然后我们设计一下最简单的角色表和权限表,不考虑过多的其他要素,角色表如下

image.png

权限表:

image.png

角色与用户关联表:

image.png

角色与权限关联表:

image.png

具体的可以看GitHub上的代码,我会实时更新进度->秒杀系统源代码,先引入一下依赖,我这里使用gradle构建的,引入的版本是1.4:

implementation group: 'org.apache.shiro', name: 'shiro-spring', version: '1.4.0'

接下来要干的就是自定义一个realm,然后写一下shiroConfig配置类,今天先讲一下realm,是shiro中非常重要的一个东西,但是他的中文直译过来是领域,国度的意思,感觉跟他的功能不是很匹配,所以这里就叫他realm,可以看一下官方是怎么形容的:

image.png Realms担当Shiro和我们的应用程序的安全数据之间的“桥梁”或“连接器”。当它实际上与安全相关的数据如用来执行身份验证(登录)及授权(访问控制)的用户帐户交互时,Shiro从一个或多个为应用程序配置的Realm 中寻找结果,一句话总结就是再realm中我们要实现对用户的认证和授权,所以我们在自定义realm时会重写的两个方法就是授权和认证。

public class UserRealm extends AuthorizingRealm {

    @Autowired
    private SysUserServiceImpl sysUserService;
    @Autowired
    SysUserMapper sysUserMapper;
    @Autowired
    RedisTemplate redisTemplate;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取登录用户名
        SysUser sysUser = (SysUser) principalCollection.getPrimaryPrincipal();
        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        List<Map<String, Object>> powerList = sysUserService.getUserPower(sysUser.getUserId());
        System.out.println(powerList.toString());
        for (Map<String, Object> powerMap : powerList) {
            //添加角色
            simpleAuthorizationInfo.addRole(String.valueOf(powerMap.get("roleName")));
            //添加权限
            simpleAuthorizationInfo.addStringPermission(String.valueOf(powerMap.get("permissionsName")));
        }
        return simpleAuthorizationInfo;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取token信息
        String accessToken  = authenticationToken.getPrincipal().toString();
        //根据token获取用户信息
        SysUser sysUser = (SysUser) redisTemplate.opsForValue().get(accessToken);

        if (sysUser == null) {
            //这里返回后会报出对应异常
            return null;
        } else {
            //这里验证authenticationToken和simpleAuthenticationInfo的信息
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(sysUser, accessToken , getName());
            return simpleAuthenticationInfo;
        }
    }
}

doGetAuthenticationInfo方法就是进行用户认证的,传入的authenticationToken是被我自定义过的的,这里只要知道是一个token然后是前端传过来的的就行,下一篇会具体说到,这里只需要知道我是根据这个token查到用户信息的就行(在登陆的时候会把这个token返回给前端,跟当前的登录用户是绑定在一起的,信息是存在redis中的,讲用户篇的时候说过,下一篇也会具体说一下),然后要做的就是把用户信息放到simpleAuthenticationInfo中返回就好了,simpleAuthenticationInfo可以理解为是用户的身份信息,第一个参数可以把用户名传进去,也可以把整个用户对象传进去,只要能根据这个知道当前用户的名字就可以了,第二个参数就是身份认证的的标志了,我是基于token来认证用户的我就把token传进去,具体是怎么认证的会在下一篇说,第三个参数就是当前的realm对象,认证的其他事情shiro都会帮我们干的。

认证说完了就是授权了,doGetAuthorizationInfo中我们要做的就是去数据库里查询当前用户拥有哪些角色,哪些权限然后添加进去即可,具体的查询语句如下:

<select id="getUserPower" resultType="java.util.HashMap" parameterType="String">
    SELECT user.user_id ,user.tel,role.roleName,role.roleId,per.permissionsName ,per.perId,per.perRemarks
    FROM sys_user AS user,
         sys_role AS role,
         sys_permissions AS per,
         t_role_permiss,
         t_user_role
    WHERE user.user_id=#{userId}
      AND user.user_id=t_user_role.userId
      AND t_user_role.roleId=role.roleId
      AND t_role_permiss.roleId=role.roleId
      AND t_role_permiss.perId=per.perId
</select>

可能表设计的不是很合理,要查五张表。。。查出来的结果如下:

image.png

然后把角色和权限依次添加到simpleAuthorizationInfo中返回即可。可以直接使用注解的方式来实现接口的访问限制。@RequiresRoles("xxx")表示需要的角色,@RequiresPermissions("xxx")表示需要的权限,直接注释在接口上就可以使用了。比如这样就表明访问这个接口的话用户需要有resetPassword这个权限,没有这个权限就会返回错误信息

image.png

效果如下,使用起来非常的方便

image.png

错误信息可以用全局异常捕获然后自定义

image.png