关于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的权限控制的主要内容,如果未能解决你的问题,请参考以下文章