关于springcloud使用shiro的权限控制

Posted Mr. Dreamer Z

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于springcloud使用shiro的权限控制相关的知识,希望对你有一定的参考价值。

之前我在网上找了很久的使用基于springcloud用shiro来作为权限控制。但是都没有一个好的demo,所以自己查了资料然后问了一些人,写了一个简单的demo。好了,话不多少直接进入正题

由于是使用springcloud,那么关于springcloud的一些知识我在这就不一一去讲了。直接讲一些shiro关键的地方。不懂的朋友可以去看看这位大牛写的帖子https://www.fangzhipeng.com/archive/?tag=SpringCloud 真的写的很好,springcloud快速入门

这里说一下我的环境配置:JDK11,TOMCAT9

首先我们需要创建一个server项目,然后创建一个zuul项目(由于现在很多服务架构都会用到负载均衡,所以我没有把权限和zuul放在一起,而是由zuul转发)。

OK,创建好这两个项目之后,我们开始搭建权限项目。

在pom中引入这两个对应的jar包

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

然后去创建关于shiro的配置文件

这两个类在网上都可以搜到,千篇一律的配置,只是根据个人不同的需求去更改其中过滤的位置。

首先创建一个配置文件,开头使用@Configuration去注解,让其成为一个配置文件。这边说明一下,由于是使用分布式,而且我这边没有使用包含的页面,所以没有去设置任何界面有关的权限配置。

@Configuration
public class ShiroConfig 

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) 
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //拦截器.
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
        // 配置不会被拦截的链接 顺序判断
        //filterChainDefinitionMap.put("/static/**", "anon");
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");
        //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
        //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        //filterChainDefinitionMap.put("/userInfo/**", "authc");

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        //shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        //shiroFilterFactoryBean.setSuccessUrl("/index");
        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/response/responseMsg");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    

    @Bean
    public MyShiroRealm myShiroRealm()
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        return myShiroRealm;
    


    @Bean
    public SecurityManager securityManager()
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    


    /**
     *  开启shiro aop注解支持.
     *  使用代理方式;所以需要开启代码支持;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager)
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    

    @Bean(name="simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver
    createSimpleMappingExceptionResolver() 
        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
        mappings.setProperty("UnauthorizedException","/response/responseMsg");//无权限跳入的方法
        r.setExceptionMappings(mappings);  // None by default
        r.setDefaultErrorView("error");    // No default
        r.setExceptionAttribute("ex");     // Default is "exception"
        //r.setWarnLogCategory("example.MvcLogger");     // No default
        return r;
    

然后创建一个针对于登陆和权限验证的类,说明一下,我这边使用的fegin,数据库也没有在这个权限项目,是去调用的其他服务的接口。当然了,密码验证这个段代码,可以根据自己的需求去改写。使用MD5也好,不要盐也好。我这边没有写的那么复杂

public class MyShiroRealm extends AuthorizingRealm 


    @Autowired
    private UserService userService;

    /**
     * 角色
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) 
        System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        UserInfo userInfo  = (UserInfo)principals.getPrimaryPrincipal();
        for(SysRole role:userInfo.getRoleList())
            authorizationInfo.addRole(role.getRole());
            for(SysPermission p:role.getPermissions())
                authorizationInfo.addStringPermission(p.getPermission());
            
        
        return authorizationInfo;
    

    /*主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException 
        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
        //获取用户的输入的账号.
        String username = (String)token.getPrincipal();
        System.out.println(token.getCredentials());
        //通过username从数据库中查找 User对象,如果找到,没找到.
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        UserInfo userInfo = userService.findByUsername(username);
        System.out.println("----->>userInfo="+userInfo);
        if(userInfo == null)
            return null;
        


        if(!userInfo.getPassword().equals(new String((char[])token.getCredentials())))
        
            throw new IncorrectCredentialsException("密码错误--------------"); //如果密码错误
        

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userInfo, //用户名
                userInfo.getPassword(), //密码
                ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
                this.getName()  //realm name
        );

        return authenticationInfo;
    

这是我的业务层的代码,是对应我另一个项目的方法

这个是我登陆的接口,由权限这个项目直接去验证。好处是因为由于加入权限这个要求的话,所有的接口都需要从权限这个项目进行发送。

@RequestMapping("/login")
public UserInfo login(HttpServletRequest request, Map<String, Object> map, String username, String password) throws Exception
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(username,
            password);
    String error = null;
    UserInfo userInfo = null;
    try 
        subject.login(token);
        userInfo = (UserInfo) SecurityUtils.getSubject().getPrincipal();
        //token.setRememberMe(true);
     catch (UnknownAccountException e) 
        error = "用户名/密码错误";
     catch (IncorrectCredentialsException e) 
        error = "用户名/密码错误";
     catch (ExcessiveAttemptsException e) 
        // TODO: handle exception
        error = "登录失败多次,账户锁定10分钟";
     catch (AuthenticationException e) 
        // 其他错误,比如锁定,如果想单独处理请单独catch处理
        error = "其他错误:" + e.getMessage();
    
    if (error != null) // 出错了,返回登录页面
        //request.setAttribute("error", error);
        return null;
    
    if(userInfo!=null)
    
        return userInfo;
    

    return null;

执行到subject.login 这句话之后,会直接进入我们之前写的密码验证那个方法。

这个是无权限验证之后,会进入的方法

@RestController
@RequestMapping(value = "/response")
public class ResponseController 

    @RequestMapping(value = "/responseMsg")
    public String responseMsg()
    
        return "failure";
    

这个是user项目的实体类

@Entity
@Table(name = "userinfo")
public class UserInfo  implements Serializable 

    @Id
    @GeneratedValue
    private Integer uid;
    @Column(unique =true)
    private String username;//帐号
    private String name;//名称(昵称或者真实姓名,不同系统不同定义)
    private String password; //密码;
    private String salt;//加密密码的盐
    private byte state;//用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.

    @ManyToMany(fetch= FetchType.EAGER)//立即从数据库中进行加载数据;
    @JoinTable(name = "SysUserRole", joinColumns =  @JoinColumn(name = "uid") , inverseJoinColumns =@JoinColumn(name = "roleId") )
    private List<SysRole> roleList;// 一个用户具有多个角色

    public UserInfo() 
    

    public Integer getUid() 
        return uid;
    

    public void setUid(Integer uid) 
        this.uid = uid;
    

    public String getUsername() 
        return username;
    

    public void setUsername(String username) 
        this.username = username;
    

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    

    public String getPassword() 
        return password;
    

    public void setPassword(String password) 
        this.password = password;
    

    public String getSalt() 
        return salt;
    

    public void setSalt(String salt) 
        this.salt = salt;
    

    public byte getState() 
        return state;
    

    public void setState(byte state) 
        this.state = state;
    

    public List<SysRole> getRoleList() 
        return roleList;
    

    public void setRoleList(List<SysRole> roleList) 
        this.roleList = roleList;
    

    /**
     * 密码盐.
     * @return
     */
    public String getCredentialsSalt()
        return this.username+this.salt;
    
    //重新对盐重新进行了定义,用户名+salt,这样就更加不容易被破解


    @Override
    public String toString() 
        return "UserInfo" +
                "uid=" + uid +
                ", username='" + username + '\\'' +
                ", name='" + name + '\\'' +
                ", password='" + password + '\\'' +
                ", salt='" + salt + '\\'' +
                ", state=" + state +
                '';
    

@Entity
@Table(name = "sysrole")
public class SysRole 

    @Id
    @GeneratedValue
    private Integer id; // 编号
    private String role; // 角色标识程序中判断使用,如"admin",这个是唯一的:
    private String description; // 角色描述,UI界面显示使用
    private Boolean available = Boolean.FALSE; // 是否可用,如果不可用将不会添加给用户

    //角色 -- 权限关系:多对多关系;
    @ManyToMany(fetch= FetchType.EAGER)
    @JoinTable(name="SysRolePermission",joinColumns=@JoinColumn(name="roleId"),inverseJoinColumns=@JoinColumn(name="permissionId"))
    private List<SysPermission> permissions;

    public Integer getId() 
        return id;
    

    public void setId(Integer id) 
        this.id = id;
    

    public String getRole() 
        return role;
    

    public void setRole(String role) 
        this.role = role;
    

    public String getDescription() 
        return description;
    

    public void setDescription(String description) 
        this.description = description;
    

    public Boolean getAvailable() 
        return available;
    

    public void setAvailable(Boolean available) 
        this.available = available;
    

    public List<SysPermission> getPermissions() 
        return permissions;
    

    public void setPermissions(List<SysPermission> permissions) 
        this.permissions = permissions;
    


@Entity
@Table(name = "syspermission")
public class SysPermission implements Serializable 

    @Id
    @GeneratedValue
    private Integer id;//主键.
    private String name;//名称.
    @Column(columnDefinition="enum('menu','button')")
    private String resourceType;//资源类型,[menu|button]
    private String url;//资源路径.
    private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
    private Long parentId; //父编号
    private String parentIds; //父编号列表
    private Boolean available = Boolean.FALSE;

    public Integer getId() 
        return id;
    

    public void setId(Integer id) 
        this.id = id;
    

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    

    public String getResourceType() 
        return resourceType;
    

    public void setResourceType(String resourceType) 
        this.resourceType = resourceType;
    

    public String getUrl() 
        return url;
    

    public void setUrl(String url) 
        this.url = url;
    

    public String getPermission() 
        return permission;
    

    public void setPermission(String permission) 
        this.permission = permission;
    

    public Long getParentId() 
        return parentId;
    

    public void setParentId(Long parentId) 
        this.parentId = parentId;
    

    public String getParentIds() 
        return parentIds;
    

    public void setParentIds(String parentIds) 
        this.parentIds = parentIds;
    

    public Boolean getAvailable() 
        return available;
    

    public void setAvailable(Boolean available) 
        this.available = available;
    



这里都是多对多的关系,这里建议使用单向多对多。不然从数据取出值的时候会报出内存溢出的异常,网上说是由于双向关联的关系造成的,所以我把改成了单向的了。由于我在权限项目调用了user项目的方法,所以这里需要去写一个通过用户名查询的语句。这个简单的hql语句我就不贴出来了。

这里有个重点的地方是权限项目必须也拥有user项目的实体bean类。只需要单纯的类即可。

ok 我们试一下 这个我的几个项目

zuul转发,security权限控制,user用户

登陆

说一下我报这个错的问题,是由于userinfo这个多对多的原因。zuul转发之后,userinfo成功发送回来。但是无法转换格式,所以报错了。我之前是想通过通过和token的键值对形式传入redis中(token和userinfo)。但是多对多关系的类好像不行。我准备换成一对多形式。如果哪位朋友有解决方案也可以告诉我一下,在这谢谢了。

你们可以忽略这个问题,直接zuul转发即可,不需要去做其他操作。

这个是我的权限控制。我这个账户只有  userInfo:add 权限 ,没有  userInfo:del 权限

看下结果

ok成功了,这里提一点。由于分布式的原因,zuul每次转发的seesion都不一致,所以会和shiro的机制冲突。导致shiro无法去执行操作。需要在zuul配置文件中加一句

zuul.routes.oaUser.sensitiveHeaders="*"  //会过滤客户端请求中的和该配置项匹配的headers

demo地址如下:

springcloud-shiro_zuul整合shiro-Java代码类资源-CSDN下载

以上是关于关于springcloud使用shiro的权限控制的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot项目+Shiro(权限框架)+Redis(缓存)集成

springcloud vue.js 前后分离 微服务 分布式 activiti工作流 集成代码生成器 shiro权限

springcloud vue.js 微服务分布式 前后分离 集成代码生成器 shiro权限 activiti工作流

springcloud vue.js 微服务分布式 activiti工作流 前后分离 集成代码生成器 shiro权限

springcloud vue.js 微服务 分布式 activiti工作流 前后分离 shiro权限 集成代码生成器

shiro原理实践+++